Blazor Web Assembly (WASM) 开关






4.70/5 (4投票s)
符合 ARIA 规范的普通和 EditForm 切换开关,内置浅色和深色主题支持。包含六种额外自定义皮肤。
目录
引言
这是 Blazor 系列文章中的第二篇,重点关注内置浅色和深色主题的控件开发。请查看第一篇文章,它为本文及后续文章的主题支持奠定了基础。
灵感
以前,我写过一篇关于C# 和 VB 中灵活的 WPF ToggleSwitch 无外观控件的文章,因此切换开关自然成为本系列 Blazor 组件的首选。
设计
要求
组件需要支持
- 在标准 HTML 和 Blazor EditForm 中使用
- 控制状态
- Enabled
- 焦点
- 悬停
- Checked
- 禁用
- 只读
- 控制部件的显示
- 定位
- Visibility
- 所有文本均可更改和自定义标记
- 类、样式和自定义属性
- ARIA 合规性
- 主题支持
- 浅色主题
- 深色主题
- 自定义主题(使用 CSS 自定义属性(变量)- CSS:层叠样式表 | MDN)
- C# 可空合规性
- 可在多个项目之间重用
- 如果无法避免,则尽量减少 JavaScript
- 最新的 Blazor 和 CSS3 编码技术
- BEM —(块元素修饰符)CSS 类命名约定
在各种 Blazor 第三方库中,有许多不同的切换开关设计。对于此控件,我将外观基于标准 Windows 外观。
浅色主题
深色主题
自定义主题
我们将探讨两种不同的实现方式来切换主色主题
- 通过样式直接在 HTML 标签上应用 CSS 变量
- 使用预设的 CSS 类名
下面是一个示例,我们已将红色主色方案应用于基色和悬停色。
自定义 UI
在本文的稍后部分,我们将探讨如何通过六种不同的设计来自定义外观。尽管每个组件的外观和动画都不同,但底层 HTML 保持不变。
实现
ARIA 合规性
虽然没有具体的文档说明要求,但由于它基于 input type="checkbox"
,我们可以遵循双状态要求。还提供了一个示例。
切换开关 UI
切换开关控件有三个部分
- 标签/标题 - 文本,告知用户选择的用途
- 切换 - 可点击的开/关开关
- 状态 - 指示切换状态的文本
对于自定义 UI,我添加了第四部分。这不用于默认组件。当控件渲染时,标记如下
<div class="c-toggle">
<div id="id_piLBO02PjEqg5wHdYvv4gA" class="c-toggle__label">
Enabled and checked
</div>
<div class="c-toggle__container">
<input type="checkbox" role="switch"
id="id_VfH38gwO8UOP45Y_g7v5bg"
class="c-toggle__pill"
aria-labelledby="id_nB-HBiD9DUORGBYQXFU2sA"
aria-checked="true"
aria-readonly="false">
<label class="c-toggle__thumb"
for="id_VfH38gwO8UOP45Y_g7v5bg"
aria-labelledby="id_piLBO02PjEqg5wHdYvv4gA"
data-label="On" data-label-on="On" data-label-off="Off"
tabindex="0">
<span class="c-toggle__thumb-inner"></span>
</label>
<span id="id_nB-HBiD9DUORGBYQXFU2sA"
class="c-toggle__state-text">On</span>
</div>
</div>
按类名分解
c-toggle
- 根容器c-toggle__label
- 标题标签c-toggle__container
- 包含开关和状态。允许灵活定位c-toggle__pill
- 包含组件的状态值。它用于 HTMLForm
和 BlazorEditForm
输入控制要求。readonly
、disabled
、checked
的 UI 属性和onchange
事件跟踪在此标签上连接。c-toggle__thumb
- 组件的默认 UI。::after
选择器用于渲染开和关状态的开关滑块位置。还有data-label
(已删除状态)、data-label-on
(开状态文本)和data-label-off
(关状态文本)用于自定义 UI 支持。tabindex
属性用于告诉浏览器将焦点设置在哪里。c-toggle__thumb-inner
- 这不用于默认渲染,但用于自定义 UI 支持。c-toggle__state-text
- 开和关状态的文本标签。
组件样式表
切换开关被实现为两个独立的组件 - 一个用于与 Blazor EditForm
配合使用,另一个用于一般用途。为了提供灵活性并减少重复,CSS 隔离未被使用,但 Razor 类库 (RCL) 中包含一个默认样式表。要使用,我们需要在 index.html 头部中包含它。
<link href="_content/Blazor.Toggle/css/styles.css" rel="stylesheet" />
样式表的顺序对于 CSS 特异性要求很重要
- 基本 CSS 框架 - 例如:Bootstrap
- 库样式表
- 应用程序样式表
因此,例如,对于演示项目,使用了以下内容
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="_content/Blazor.Toggle/css/styles.css" rel="stylesheet" />
<link href="ToggleDemo.styles.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
主题
CSS 变量用于在浅色和深色模式之间切换。请查看上一篇文章了解其实现方式。
我尽量不使用特定于组件的CSS 变量名称,而是使用通用名称,以便将来用于其他控件。
浅色主题
--primary-fill: #0078D4;
--primary-fill-hover: #006CBE;
--primary-foreground: #FFFFFF;
--neutral-fill: #EDEDED;
--neutral-fill-hover: #E5E5E5;
--neutral-outline: #646464;
--neutral-outline-hover: #3B3B3B;
--neutral-foreground: #2B2B2B;
--neutral-focus-visual: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
--neutral-shadow-visual: 0 10px 20px -8px #3B3B3B;
--neutral-border: #BEBEBE;
--neutral-color1: #767676;
--neutral-color2: #FFFFFF;
--neutral-background1: #FFFFFF;
--neutral-background2: #F7F7F7;
--icon-backkground: #b6d2e8;
深色主题
--primary-fill: #006CBE;
--primary-fill-hover: #0078D4;
--primary-foreground: #FFFFFF;
--neutral-fill: #363636;
--neutral-fill-hover: #3D3D3D;
--neutral-outline: #646464;
--neutral-outline-hover: #8A8A8A;
--neutral-foreground: #F5F5F5;
--neutral-focus-visual: 0 0 0 0.25rem rgba(120,120,120,0.25);
--neutral-shadow-visual: 0 10px 20px -8px #8A8A8A;
--neutral-border: #323232;
--neutral-color1: #8D8D8D;
--neutral-color2: #929292;
--neutral-background1: #202020;
--neutral-background2: #424242;
--icon-backkground: #0066B4
禁用
对于禁用状态,使用不透明度来减少使用的 CSS 变量数量。
--disabled-opacity: 0.3;
主色主题
包含一些备用颜色。
.c-toggle__primary-indigo {
--primary-fill: #6610f2;
--primary-fill-hover: #580ED1;
}
.c-toggle__primary-purple {
--primary-fill: #6f42c1;
--primary-fill-hover: #633BAD;
}
.c-toggle__primary-red {
--primary-fill: #dc3545;
--primary-fill-hover: #C72F3E;
}
.c-toggle__primary-orange {
--primary-fill: #fd7e14;
--primary-fill-hover: #EB7512;
}
.c-toggle__primary-green {
--primary-fill: #198754;
--primary-fill-hover: #177A4C;
}
作为演示代码的一部分,我演示了如何使用两种常用方法添加自己的自定义主主题颜色:使用 Styles 和 Classes。
自定义 UI
演示项目中还包含六个自定义 UI,除了 CSS 类之外,所有都使用相同的组件属性。CSS 类用于应用更改。
正如您从上面的屏幕截图可以看到的,Toggle
和 ToggleInput
组件的渲染标记是相同的,因此自定义 UI CSS 将与两者无缝配合。
这是渲染 Toggle
组件的代码
<div class="@CssSection">
<h3>Standard <b>Toggle</b> Component</h3>
<div class="o-custom__sections">
@foreach (CustomToggleModel model in Toggles)
{
<section class="o-section__custom">
<Toggle Value="@true"
Class="@model.CssClass"
OnText="@model.YesChoice"
OffText="@model.NoChoice" />
</section>
}
</div>
</div>
这是渲染 ToggleInput
组件的代码
<EditForm class="@CssSection" Model="Toggles">
<h3>EditForm <b>ToggleInput</b> Component</h3>
<div class="o-custom__sections">
@foreach (CustomToggleModel model in Toggles)
{
<section class="o-section__custom">
<ToggleInput @bind-Value="model.IsChecked"
Class="@model.CssClass"
OnText="@model.YesChoice"
OffText="@model.NoChoice" />
</section>
}
</div>
</EditForm>
用于分配 CSS 类、默认选择和名称的 C# 后台代码
#region BEM
private readonly string CssSection
= "o-section".JoinName("body");
#endregion
#region Fields
private const string NoChoiceValue = "No";
private const string YesChoiceValue = "Yes";
#endregion
#region Properties
private List<CustomToggleModel> Toggles { get; } = new()
{
// [..trimmed..]
new ()
{
NoChoice = NoChoiceValue,
YesChoice = YesChoiceValue,
CssClass = "custom__toggle
custom__toggle-2
custom__toggle--position"
},
// [..trimmed..]
};
注意:上述自定义 UI 选自 Free Frontend 网站。值得一看,因为他们有很多可供选择。
代码
标记
Razor 相当不言自明
- 每个部分的位置可以通过属性和方法选择:
HasLabel
、HasOnOffLabel()
和Position
- 通过
LabelContent
支持标题标签的自定义内容。 - 开和关状态文本通过
OnText
和OffText
属性设置。活动状态使用StateLabel
私有属性设置。 - 组件 UI 的
disable
、enabled
和checked
UI 属性通过input
标签上的private
和public
组件属性设置。
<div class="@Classname" style="@Style">
@if (HasLabel())
{
<div id="@LabelId" class="@CSS.Label">
@if (LabelContent is not null)
{
@LabelContent
}
else
{
@Label
}
</div>
}
<div class="@CSS.Container">
@if (Position == TogglePosition.Left)
{
<input type="checkbox" role="switch"
id="@PillId" class="@CSS.Pill"
aria-labelledby="@StateLabelId"
aria-checked="@Value.ToString().ToLower()"
aria-readonly="@Disabled.ToString().ToLower()"
checked="@Value" disabled="@Disabled"
onchange="@OnChange"
@onclick:preventDefault="@ReadOnly"/>
<label class="@CSS.Thumb" for="@PillId"
aria-labelledby="@LabelId"
data-label="@StateLabel"
data-label-on="@OnText"
data-label-off="@OffText"
tabindex="0"
@onkeydown="@(OnKeyDownAsync)"
@onkeydown:preventDefault="true"
@onkeydown:stopPropagation="true">
<span class="@CSS.ThumbInner"></span>
</label>
@if (HasOnOffLabel())
{
<span id="@StateLabelId" class="@CSS.State">
@StateLabel
</span>
}
}
else
{
@if (HasOnOffLabel())
{
<span id="@StateLabelId" class="@CSS.State">
@StateLabel
</span>
}
<input type="checkbox" role="switch"
id="@PillId" class="@CSS.Pill"
aria-labelledby="@StateLabelId"
aria-checked="@Value.ToString().ToLower()"
aria-readonly="@Disabled.ToString().ToLower()"
checked="@Value" disabled="@Disabled"
onchange="@OnChange"
@onclick:preventDefault="@ReadOnly"/>
<label class="@CSS.Thumb" for="@PillId"
aria-labelledby="@LabelId"
data-label="@StateLabel"
data-label-on="@OnText"
data-label-off="@OffText"
tabindex="0"
@onkeydown="@(OnKeyDownAsync)"
@onkeydown:preventDefault="true"
@onkeydown:stopPropagation="true">
<span class="@CSS.ThumbInner"></span>
</label>
}
</div>
</div>
Toggle
和 ToggleInput
组件之间主要的 razor 代码区别在于 input
标签。
对于 Toggle
组件
<input type="checkbox" role="switch"
id="@PillId" class="@CSS.Pill"
aria-labelledby="@StateLabelId"
aria-checked="@Value.ToString().ToLower()"
aria-readonly="@Disabled.ToString().ToLower()"
checked="@Value" disabled="@Disabled"
onchange="@OnChange" @onclick:preventDefault="@ReadOnly"/>
对于 Toggle
组件,我们手动绑定到 Value
属性并挂钩 onchange
事件。当 onchange
事件触发时,我们手动更新 Value
属性并调用 StateHasChanged()
来更新 UI。
对于 ToggleInput
组件
<input type="checkbox" role="switch"
id="@PillId" class="@CSS.Pill"
aria-labelledby="@StateLabelId"
aria-checked="@CurrentValue.ToString().ToLower()"
aria-readonly="@Disabled.ToString().ToLower()"
@bind=CurrentValue disabled="@Disabled"
@onclick:preventDefault="@ReadOnly"/>
对于 ToggleInput
,我们双向绑定到基类 InputBase
,基类将在值更改时处理 UI 刷新。
后台代码
我将重点介绍关键逻辑,并跳过属性。属性是直接的。
组件部分 ID
ARIA 和 HTML 标记要求各个部分相互指向。例如:aria-labelledby
和 for
。我们只需要在组件创建时设置这些一次。
protected override void OnInitialized()
{
_labelId = GetUniqueId();
_pillId = GetUniqueId();
_stateLabelId = GetUniqueId();
if (DefaultValue)
Value = true;
base.OnInitialized();
}
唯一的 ID 使用了一个辅助方法。可在 Blazor.Common
库中找到。
在组件基类中
public string GetUniqueId()
=> "id_" + Guid.NewGuid().ToShortString();
扩展方法
public static class GuidExtensions
{
public static string ToShortString(this Guid guid)
=> Convert.ToBase64String(guid
.ToByteArray())
.Replace('+', '-').Replace('/', '_')[..22];
}
组件配置
CSS 类名用于根据设置的属性配置 UI
private string Classname
{
get
{
CssBuilder builder = new CssBuilder(CSS.Root);
if (Disabled)
builder.AddClass(CSS.Modifier.Disabled);
if (InlineLabel)
builder.AddClass(CSS.Modifier.InlineLabel);
if (!HasOnOffLabel())
builder.AddClass(CSS.Modifier.NoOnOffLabel);
if (!string.IsNullOrEmpty(Class))
builder.AddClass(Class);
return builder.Build();
}
}
我正在使用 Ed Charbeneau · GitHub 的辅助类来构建 CSS 类——这是干净代码的必备工具!
ToggleInput 验证
ToggleInput
具有 InputBase
所需的额外代码,用于解析 Value
和验证
protected override bool TryParseValueFromString
(
string? value,
out bool result,
out string validationErrorMessage
)
{
if (bool.TryParse(value, out bool parsedValue))
{
result = parsedValue;
validationErrorMessage = string.Empty;
return true;
}
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
键盘支持
ARIA 对键盘支持的要求只有 space
键。但是,我还添加了对 enter
键的支持。
首先,我们需要挂接 onkeydown
事件。我们还需要消费按键
<label class="@CSS.Thumb" for="@PillId" aria-labelledby="@LabelId"
data-label="@StateLabel" data-label-on="@OnText" data-label-off="@OffText"
tabindex="0"
@onkeydown="@(OnKeyDownAsync)"
@onkeydown:preventDefault="true"
@onkeydown:stopPropagation="true">
现在我们可以处理事件了。对于 Toggle
组件,我们手动处理 Value
更改事件。对于 ToggleInput
,InputBase
基类处理更改事件,所以我们需要采用不同的方法。
对于 Toggle
组件
private void OnKeyDownAsync(KeyboardEventArgs arg)
{
switch (arg.Code)
{
case "Space":
case "Enter":
OnChange();
break;
}
}
private void OnChange()
{
// check is here for browsers that do not manage the input disabled state
if (_disabled)
return;
Value = !Value;
InvokeAsync(async () => await ValueChanged.InvokeAsync(Value));
}
对于 ToggleInput
组件
private void OnKeyDownAsync(KeyboardEventArgs arg)
{
//Console.WriteLine($"** KEY: {arg.Code} | {arg.Key}");
switch (arg.Code)
{
case "Space":
case "Enter":
CurrentValue = !CurrentValue;
break;
}
}
用法
切换
示例项目有四个示例
- 基本用法与主色主题
- EditForm 用法与主色主题
- 自定义布局
- 自定义 UI 设计
Basic
<Toggle Label="Enabled and checked"
DefaultValue="true"
OnText="On" OffText="Off"
Style="@CustomStyle"/>
自定义标签
<Toggle InlineLabel="true"
OnText="On" OffText="Off"
Class="@CustomCss"
ValueChanged=@OnCheckedAsync>
<LabelContent>
Custom inline label
</LabelContent>
</Toggle>
ToggleInput
如果 ToggleInput
未包含在 EditForm
组件中,它将抛出异常。
<EditForm class="o-editform"
Model=MyModel
OnValidSubmit="@HandleValidSubmit">
<ToggleInput @bind-Value=MyModel.BoundChecked2
Class="@CustomCss"
Label="Are you sure?"
InlineLabel="true"
OnText="Yes"
OffText="No" />
</EditForm>
自定义布局
有一个设置演示展示了自定义布局。我将 C# 和 VB 中灵活的 WPF ToggleSwitch 无外观控件文章中使用的示例进行了现代化改造。
在这里,我们在 LabelContent
中设置图标、标题和选定状态,并使用 display: flex
来定位元素
<ToggleInput @bind-Value="model.IsChecked"
Class="@CssSenderItem"
Position="TogglePosition.Right"
OnText="@model.YesChoice"
OffText="@model.NoChoice"
Disabled="@NotificationsDisabled">
<LabelContent>
@if (!string.IsNullOrEmpty(model.IconType))
{
<box-icon name='@model.IconName'
type='@model.IconType'
class="@CssSenderItemIcon">
</box-icon>
}
else
{
<box-icon name='@model.IconName'
class="@CssSenderItemIcon">
</box-icon>
}
<div class="@CssSenderItemText">
<span class="@CssSenderItemTitle">
@model.Title
</span>
<span class="@CssSenderItemState">
@GetStateLabel(model.IsChecked)
</span>
</div>
</LabelContent>
</ToggleInput>
这是用于布局和调整部件大小的 CSS
.c-setting {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.c-sender__item {
display: flex;
align-items: center;
}
.c-sender__item .c-toggle__label {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.c-sender__item-text {
display: flex;
flex-direction: column;
line-height: 1.35;
}
.c-sender__item-title {
font-size: 1.15em;
}
.c-sender__item-state {
font-size: 0.85em;
}
ColorSelect - 额外组件
为了演示主色主题,可以使用标准的 Select
标签下拉菜单。但我想要一个更可定制的,能显示正在选择的颜色。快速搜索了 Code Pen,找到了这个 纯 CSS 选择框,我将其改编并制作成一个带有浅色和深色主题的 Blazor 组件。
总结
我们已经将简单的 input type="checkbox"
变成了用于普通和 EditForm
的切换开关。在您自己的项目中,只需少量属性和单个事件即可轻松实现。
我们还添加了浅色、深色和主色主题支持。我们也看到了如何自定义部件的布局。最后,我们还演示了如何将其提升到更高水平,具有酷炫的动画自定义 UI。
作为额外奖励,包含了一个 ColorSelect
控件。
下载代码并查看实际效果。
尽情享用!
历史
- v1.0 - 2022年2月7日 - 首次发布