目录
介绍
这是Blazor文章系列中的第二篇,该文章专注于内置明暗主题的控件开发。查看第一篇文章,该文章为本篇文章和后续文章的主题支持奠定了基础。
设计
要求
该组件需要支持:
- 在标准HTML和Blazor EditForm中使用
- 控制状态:
- Enabled
- Focus
- Hover
- Checked
- Disabled
- Read-only
- 控制部件的显示
- Positioning
- Visibility
- 所有文本可更改和自定义标记
- 类、样式和自定义属性
- Aria compliance
- 主题支持
- Light主题
- Dark主题
- 自定义主题(使用CSS自定义属性(变量)- CSS:层叠样式表 | MDN)
- C# Nullable合规性
- 可跨多个项目重用
- 如果不可避免,最少的JavaScript
- 最新的Blazor和CSS3编码技术
- BEM —(块元素修饰符) CSS类命名约定
在各种Blazor第三方库中有许多不同的拨动开关设计。对于这个控件,我的外观基于标准的Windows外观。
Light主题
Dark主题
自定义主题
我们将探索两种不同的切换主题颜色的实现:
- 通过样式直接在HTML标签上应用CSS变量
- 使用预设的CSS类名称
下面是一个示例,我们为基础颜色和悬停颜色应用了红色原色方案。
自定义用户界面
在本文后面,我们将探讨如何使用六种不同的设计自定义外观。虽然每个组件的外观和动画都不同,但底层HTML保持不变。
实现
Aria Compliance
虽然没有具体的要求文档,但是,由于它基于input type="checkbox",我们可以遵守Dual-state requirements。也有一个例子。
切换用户界面
拨动开关由三个部分控制:
- 标签/标题——通知用户选择的内容的文本
- Toggle——可点击的开/关开关
- 状态——指示切换状态的文本
对于自定义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——保存组件的状态值。它用于HTML Form和Blazor EditForm输入控制要求。readonly、disabled、checked和onchange事件跟踪的UI属性已连接到此标记上。
- c-toggle__thumb——组件的默认UI。::after选择器用于呈现打开和关闭状态的开关拇指位置。还有data-label(已删除状态)、data-label-on(打开状态文本)和data-label-off(关闭状态文本)用于自定义UI支持。tabindex属性用于告诉浏览器在哪里设置焦点。
- c-toggle__thumb-inner——这不用于默认渲染,但用于自定义UI支持。
- c-toggle__state-text——开启和关闭状态的文本标签。
组件样式表
Toggle开关实现为两个独立的组件——一个用于Blazor EditForm,另一个用于一般用途。为了提高灵活性并减少重复,没有使用CSS Isolation,但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变量来启用在Light和Dark模式之间的切换。查看之前的文章,了解这是如何实现的。
我尝试不使用特定于组件的CSS变量名称,而是使用通用名称,以便将来与其他控件一起使用。
Light主题
--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;
Dark主题
--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
Disabled
对于禁用状态,不透明度用于减少使用的CSS变量的数量。
--disabled-opacity: 0.3;
Primary Color Theming
包括一些替代颜色。
.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,除了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支持自定义内容。
- On和Off状态文本通过OnText和OffText属性设置。活动状态是使用StateLabel私有属性设置的。
- 组件UI disable,enabled>,and <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和ToggleInputinput组件之间的关键Razor代码差异是标签。
对于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,我们可以双向绑定到基InputBae<T>类中,当值发生变化时,基类将处理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有InputBase<TValue>解析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 change事件。对于ToggleInput,InputBase<TValue>基类处理change事件,因此我们需要以不同的方式处理它。
对于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用法
- 自定义布局
- 自定义用户界面设计
基本的:
<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如果没有包含在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>
自定义布局
有一个显示自定义布局的设置演示。
在这里,我们在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——Bonus组件
为了演示Primary Color Theming,可以使用标准Select标签下拉菜单。相反,我想要一些更可定制的东西,显示正在选择的颜色。快速的Code Pen搜索并找到了这个纯CSS选择框,我对其进行了改编并变成了带有浅色和深色主题的Blazor组件。
概括
我们已经采取了谦逊的input type="checkbox",并把它变成一个Toggle开关的正常和EditForm使用。在您自己的项目中使用很容易实现,只需几个属性和一个事件。
我们还添加了浅色、深色和原色主题支持。我们还看到了如何自定义部件的布局。最后,我们还演示了如何将它提升到一个新的水平,将炫酷的动画自定义UI。
作为bonus,包括一个ColorSelect控制。
下载代码并查看它的实际效果。
https://www.codeproject.com/Articles/5324473/Blazor-Web-Assembly-WASM-Toggle-Switch