介绍
构建 Web 应用程序通常涉及为用户交互做准备。为用户交互做准备的重要方式之一是通过表单。不同的表单组件用于从用户那里获取不同类型的输入。例如,密码组件从用户那里获取敏感信息并将其屏蔽,使其不可见。
大多数时候,您需要从用户那里获取的信息是类似布尔值的——例如,是或否、真或假、启用或禁用、开或关等。传统上,复选框表单组件用于获取这些类型的输入。然而,在现代界面设计中,拨动开关通常用作复选框替代品,尽管存在一些可访问性问题。
在本教程中,您将看到如何使用 React 构建自定义切换开关组件。在本教程结束时,您将拥有一个演示 React 应用程序,该应用程序使用您的自定义切换开关组件。
这是您将在本教程中构建的最终应用程序的演示:
先决条件
在开始之前,您需要以下内容:
-
您的机器上安装了Node.js和 npm 5.2 或更高版本。要安装 Node 并检查您的 npm 版本,请参阅如何安装 Node.js 并为您的环境创建本地开发环境指南。使用 npm 5.2 或更高版本将允许您利用该
npx
命令。npx
将允许您在create-react-app
不全局下载包的情况下运行。 -
本教程假设您已经熟悉 React。如果没有,您可以查看如何在 React.js教程系列中编码或阅读React 文档以了解有关 React 的更多信息。
第 1 步 – 入门
首先,使用npx
和创建一个新的 React 应用程序create-react-app
。您可以随意命名应用程序,但本教程将使用react-toggle-switch
:
- npx create-react-app react-toggle-switch
接下来,您将安装应用程序所需的依赖项。使用终端窗口导航到项目目录:
- cd react-toggle-switch
运行以下命令安装所需的依赖项:
注意:node-sass
通过参考最低支持的快速指南,确保您安装的版本与您的环境兼容。
您将bootstrap
软件包安装为应用程序的依赖项,因为您将需要一些默认样式。要在应用程序中包含 Bootstrap,请编辑该src/index.js
文件并在每个其他import
语句之前添加以下行:
import "bootstrap/dist/css/bootstrap.min.css";
通过运行以下命令来启动应用程序npm
:
- npm start
随着应用程序的启动,开发就可以开始了。请注意,为您打开了一个带有实时重新加载功能的浏览器选项卡。实时重新加载将在您开发时与应用程序的更改保持同步。
此时,应用程序视图应类似于以下屏幕截图:
接下来,您将创建切换组件。
第 2 步 – 创建ToggleSwitch
组件
在构建组件之前,components
在src
您的项目目录中创建一个名为的新目录。
- mkdir -p src/components
接下来,创建另一个ToggleSwitch
在components
目录内命名的新目录。
- mkdir -p src/components/ToggleSwitch
您将在 中创建两个新文件src/components/ToggleSwitch
,即:index.js
和index.scss
. index.js
使用您喜欢的文本编辑器创建并打开文件:
- nano src/components/ToggleSwitch/index.js
将以下内容添加到src/components/ToggleSwitch/index.js
文件中:
import PropTypes from 'prop-types';
import classnames from 'classnames';
import isString from 'lodash/isString';
import React, { Component } from 'react';
import isBoolean from 'lodash/isBoolean';
import isFunction from 'lodash/isFunction';
import './index.scss';
class ToggleSwitch extends Component {}
ToggleSwitch.propTypes = {
theme: PropTypes.string,
enabled: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func
]),
onStateChanged: PropTypes.func
}
export default ToggleSwitch;
在此代码片段中,您创建了ToggleSwitch
组件并为其某些道具添加了类型检查。
theme
:string
表示拨动开关的样式和颜色。enabled
: 可以是返回 a 的aboolean
或 afunction
,boolean
它决定了渲染时切换开关的状态。onStateChanged
: 是一个回调函数,当切换开关的状态改变时会被调用。这对于在切换开关时触发父组件上的操作很有用。
初始化 ToggleSwitch 状态
在以下代码片段中,您初始化ToggleSwitch
组件的状态并定义一些用于获取切换开关状态的组件方法。
// ...
class ToggleSwitch extends Component {
state = { enabled: this.enabledFromProps() }
isEnabled = () => this.state.enabled
enabledFromProps() {
let { enabled } = this.props;
// If enabled is a function, invoke the function
enabled = isFunction(enabled) ? enabled() : enabled;
// Return enabled if it is a boolean, otherwise false
return isBoolean(enabled) && enabled;
}
}
// ...
在这里,该enabledFromProps()
方法解析enabled
传递的prop 并返回一个boolean
指示是否应在呈现时启用开关。如果enabled
prop 是 a boolean
,它返回布尔值。如果是 a function
,则在确定返回值是否为 a 之前首先调用该函数boolean
。否则,它返回false
。
请注意,您使用了 from 的返回值enabledFromProps()
来设置初始enabled
状态。您还添加了isEnabled()
获取当前enabled
状态的方法。
切换 ToggleSwitch
让我们继续添加在单击时切换开关的方法。将以下代码添加到文件中:
// ...
class ToggleSwitch extends Component {
// ...other class members here
toggleSwitch = evt => {
evt.persist();
evt.preventDefault();
const { onClick, onStateChanged } = this.props;
this.setState({ enabled: !this.state.enabled }, () => {
const state = this.state;
// Augument the event object with SWITCH_STATE
const switchEvent = Object.assign(evt, { SWITCH_STATE: state });
// Execute the callback functions
isFunction(onClick) && onClick(switchEvent);
isFunction(onStateChanged) && onStateChanged(state);
});
}
}
// ...
由于此方法将作为click
事件侦听器触发,因此您已使用evt
参数对其进行了声明。首先,此方法enabled
使用逻辑NOT
( !
) 运算符切换当前状态。当状态更新后,它会触发传递给onClick
和onStateChanged
props的回调函数。
请注意,由于onClick
需要一个事件作为其第一个参数,因此您使用SWITCH_STATE
包含新状态对象的附加属性扩充了该事件。但是,onStateChanged
回调是用新的状态对象调用的。
渲染 ToggleSwitch
最后,让我们实现组件的render()
方法ToggleSwitch
。将以下代码添加到文件中:
// ...
class ToggleSwitch extends Component {
// ...other class members here
render() {
const { enabled } = this.state;
// Isolate special props and store the remaining as restProps
const { enabled: _enabled, theme, onClick, className, onStateChanged, ...restProps } = this.props;
// Use default as a fallback theme if valid theme is not passed
const switchTheme = (theme && isString(theme)) ? theme : 'default';
const switchClasses = classnames(
`switch switch--${switchTheme}`,
className
)
const togglerClasses = classnames(
'switch-toggle',
`switch-toggle--${enabled ? 'on' : 'off'}`
)
return (
<div className={switchClasses} onClick={this.toggleSwitch} {...restProps}>
<div className={togglerClasses}></div>
</div>
)
}
}
// ...
这种render()
方法发生了很多事情,所以让我们把它分解一下:
- 首先,
enabled
从组件状态解构状态。 - 接下来,您解构组件 props 并提取
restProps
将传递给 switch 的props 。这使您能够拦截和隔离组件的特殊道具。 - 接下来,根据组件的状态和状态,使用类名来构造开关和内部切换器的类。
theme
enabled
- 最后,您使用适当的道具和类呈现 DOM 元素。请注意,您
this.toggleSwitch
作为click
交换机上的事件侦听器传入。
保存并关闭文件。
您现在已经创建了ToggleSwitch
.
第 3 步 — 设计样式 ToggleSwitch
现在您拥有了ToggleSwitch
组件及其所需的功能,您可以继续为它编写样式。
index.scss
使用您喜欢的文本编辑器打开文件:
- nano src/components/ToggleSwitch/index.scss
将以下代码片段添加到文件中:
// DEFAULT COLOR VARIABLES
$ball-color: #ffffff;
$active-color: #62c28e;
$inactive-color: #cccccc;
// DEFAULT SIZING VARIABLES
$switch-size: 32px;
$ball-spacing: 2px;
$stretch-factor: 1.625;
// DEFAULT CLASS VARIABLE
$switch-class: 'switch-toggle';
/* SWITCH MIXIN */
@mixin switch($size: $switch-size, $spacing: $ball-spacing, $stretch: $stretch-factor, $color: $active-color, $class: $switch-class) {}
在这里,您定义了一些默认变量并创建了一个switch
mixin。在下一节中,您将实现 mixin,但首先,让我们检查switch
mixin的参数:
$size
:开关元素的高度。它必须有一个长度单位。它默认为32px
.$spacing
:圆球与开关容器之间的空间。它必须有一个长度单位。它默认为2px
.$stretch
:用于确定开关元素的宽度应被拉伸的程度的因素。它必须是一个无单位数。它默认为1.625
.$color
:处于活动状态时开关的颜色。这必须是有效的颜色值。请注意,无论这种颜色如何,圆形球始终为白色。$class
: 标识开关的基类。这用于动态创建开关的状态类。它默认为'switch-toggle'
. 因此,默认状态类是.switch-toggle--on
和.switch-toggle--off
。
实现 Switch Mixin
下面是switch
mixin的实现:
// ...
@mixin switch($size: $switch-size, $spacing: $ball-spacing, $stretch: $stretch-factor, $color: $active-color, $class: $switch-class) {
// SELECTOR VARIABLES
$self: '.' + $class;
$on: #{$self}--on;
$off: #{$self}--off;
// SWITCH VARIABLES
$active-color: $color;
$switch-size: $size;
$ball-spacing: $spacing;
$stretch-factor: $stretch;
$ball-size: $switch-size - ($ball-spacing * 2);
$ball-slide-size: ($switch-size * ($stretch-factor - 1) + $ball-spacing);
// SWITCH STYLES
height: $switch-size;
width: $switch-size * $stretch-factor;
cursor: pointer !important;
user-select: none !important;
position: relative !important;
display: inline-block;
&#{$on},
&#{$off} {
&::before,
&::after {
content: '';
left: 0;
position: absolute !important;
}
&::before {
height: inherit;
width: inherit;
border-radius: $switch-size / 2;
will-change: background;
transition: background .4s .3s ease-out;
}
&::after {
top: $ball-spacing;
height: $ball-size;
width: $ball-size;
border-radius: $ball-size / 2;
background: $ball-color !important;
will-change: transform;
transition: transform .4s ease-out;
}
}
&#{$on} {
&::before {
background: $active-color !important;
}
&::after {
transform: translateX($ball-slide-size);
}
}
&#{$off} {
&::before {
background: $inactive-color !important;
}
&::after {
transform: translateX($ball-spacing);
}
}
}
在这个 mixin 中,您首先根据传递给 mixin 的参数设置一些变量。接下来,您创建样式。请注意,您正在使用::after
和::before
伪元素来动态创建开关的组件。::before
在::after
创建圆形球的同时创建开关容器。
另外,请注意您如何从基类构造状态类并将它们分配给变量。该$on
变量映射到选择为启用状态,而$off
变量映射到选择为禁用状态。
您还确保基类 ( .switch-toggle
) 必须与状态类 (.switch-toggle--on
或.switch-toggle--off
)一起使用才能使样式可用。因此,您使用了&#{$on}
和&#{$off}
选择器。
创建主题开关
现在您有了switch
mixin,您将继续为切换开关创建一些主题样式。您将创建两个主题:default
和graphite-small
.
将以下代码片段附加到src/components/ToggleSwitch/index.scss
文件中:
// ...
@function get-switch-class($selector) {
// First parse the selector using `selector-parse`
// Extract the first selector in the first list using `nth` twice
// Extract the first simple selector using `simple-selectors` and `nth`
// Extract the class name using `str-slice`
@return str-slice(nth(simple-selectors(nth(nth(selector-parse($selector), 1), 1)), 1), 2);
}
.switch {
$self: &;
$toggle: #{$self}-toggle;
$class: get-switch-class($toggle);
// default theme
&#{$self}--default > #{$toggle} {
// Always pass the $class to the mixin
@include switch($class: $class);
}
// graphite-small theme
&#{$self}--graphite-small > #{$toggle} {
// A smaller switch with a `gray` active color
// Always pass the $class to the mixin
@include switch($color: gray, $size: 20px, $class: $class);
}
}
在这里,您首先创建一个名为 Sass 的函数get-switch-class
,该函数将 a$selector
作为参数。它$selector
通过一系列 Sass 函数运行并尝试提取第一个类名。例如,如果它收到:
.class-1 .class-2, .class-3 .class-4
,它返回class-1
。.class-5.class-6 > .class-7.class-8
,它返回class-5
。
接下来,您定义.switch
类的样式。您将切换类动态设置为.switch-toggle
并将其分配给$toggle
变量。请注意,您将从get-switch-class()
函数调用返回的类名分配给$class
变量。最后,您将包含switch
必要参数的mixin包含在内,以创建主题类。
注意,该选择器用于主题的开关看起来像这样的结构:&#{$self}--default > #{$toggle}
(使用默认主题作为一个例子)。将所有内容放在一起,这意味着元素的层次结构应如下所示,以便应用样式:
<!-- Use the default theme: switch--default -->
<element class="switch switch--default">
<!-- The switch is in enabled state: switch-toggle--on -->
<element class="switch-toggle switch-toggle--on"></element>
</element>
这是一个演示,显示了切换开关主题的外观:
第 4 步 – 构建示例应用程序
现在您已经拥有ToggleSwitch
带有必要样式的React 组件,让我们继续并开始创建您在本教程开头看到的示例应用程序。
将src/App.js
文件修改为类似于以下代码片段:
import classnames from 'classnames';
import snakeCase from 'lodash/snakeCase';
import React, { Component } from 'react';
import Switch from './components/ToggleSwitch';
import './App.css';
// List of activities that can trigger notifications
const ACTIVITIES = [
'News Feeds', 'Likes and Comments', 'Live Stream', 'Upcoming Events',
'Friend Requests', 'Nearby Friends', 'Birthdays', 'Account Sign-In'
];
class App extends Component {
// Initialize app state, all activities are enabled by default
state = { enabled: false, only: ACTIVITIES.map(snakeCase) }
toggleNotifications = ({ enabled }) => {
const { only } = this.state;
this.setState({ enabled, only: enabled ? only : ACTIVITIES.map(snakeCase) });
}
render() {
const { enabled } = this.state;
const headingClasses = classnames(
'font-weight-light h2 mb-0 pl-4',
enabled ? 'text-dark' : 'text-secondary'
);
return (
<div className="App position-absolute text-left d-flex justify-content-center align-items-start pt-5 h-100 w-100">
<div className="d-flex flex-wrap mt-5" style={{width: 600}}>
<div className="d-flex p-4 border rounded align-items-center w-100">
<Switch theme="default"
className="d-flex"
enabled={enabled}
onStateChanged={this.toggleNotifications}
/>
<span className={headingClasses}>Notifications</span>
</div>
{/* ... Notification options here ... */}
</div>
</div>
);
}
}
export default App;
在这里,您ACTIVITIES
使用可以触发通知的活动数组初始化常量。接下来,您使用两个属性初始化了应用程序状态:
enabled
:boolean
指示是否启用通知。only
: 一个array
包含所有可以触发通知的活动。
在更新状态之前,您使用了Lodash的snakeCase
实用程序将活动转换为蛇形。因此,变成。'News Feeds'
'news_feeds'
接下来,您定义了toggleNotifications()
根据从通知开关接收到的状态更新应用程序状态的方法。这用作传递给onStateChanged
切换开关道具的回调函数。请注意,当应用程序启用时,默认情况下将启用所有活动,因为only
state 属性填充了所有活动。
最后,您为应用程序渲染了 DOM 元素,并为通知选项留下了一个插槽,很快就会添加。此时,应用程序应类似于以下屏幕截图:
接下来,继续查找具有此注释的行:
{/* ... Notification options here ... */}
并将其替换为以下内容以呈现通知选项:
// ...
{ enabled && (
<div className="w-100 mt-5">
<div className="container-fluid px-0">
<div className="pt-5">
<div className="d-flex justify-content-between align-items-center">
<span className="d-block font-weight-bold text-secondary small">Email Address</span>
<span className="text-secondary small mb-1 d-block">
<small>Provide a valid email address with which to receive notifications.</small>
</span>
</div>
<div className="mt-2">
<input type="text" placeholder="[email protected]" className="form-control" style={{ fontSize: 14 }} />
</div>
</div>
<div className="pt-5 mt-4">
<div className="d-flex justify-content-between align-items-center border-bottom pb-2">
<span className="d-block font-weight-bold text-secondary small">Filter Notifications</span>
<span className="text-secondary small mb-1 d-block">
<small>Select the account activities for which to receive notifications.</small>
</span>
</div>
<div className="mt-5">
<div className="row flex-column align-content-start" style={{ maxHeight: 180 }}>
{ this.renderNotifiableActivities() }
</div>
</div>
</div>
</div>
</div>
) }
您可能会注意到您调用了this.renderNotifiableActivities()
来呈现活动。让我们继续实现这个方法和其他剩余的方法。
将以下方法添加到App
组件中:
// ...
class App extends Component {
// ...
toggleActivityEnabled = activity => ({ enabled }) => {
let { only } = this.state;
if (enabled && !only.includes(activity)) {
only.push(activity);
return this.setState({ only });
}
if (!enabled && only.includes(activity)) {
only = only.filter(item => item !== activity);
return this.setState({ only });
}
}
renderNotifiableActivities() {
const { only } = this.state;
return ACTIVITIES.map((activity, index) => {
const key = snakeCase(activity);
const enabled = only.includes(key);
const activityClasses = classnames(
'small mb-0 pl-3',
enabled ? 'text-dark' : 'text-secondary'
);
return (
<div key={index} className="col-5 d-flex mb-3">
<Switch theme="graphite-small"
className="d-flex"
enabled={enabled}
onStateChanged={ this.toggleActivityEnabled(key) }
/>
<span className={activityClasses}>{ activity }</span>
</div>
);
})
}
// ...
}
在这里,您已经实现了该renderNotifiableActivities
方法。您遍历所有活动,ACTIVITIES.map()
并使用切换开关为每个活动进行渲染。请注意,切换开关使用graphite-small
主题。您还可以enabled
通过检查每个活动是否已存在于only
状态变量中来检测它的状态。
最后,您定义了toggleActivityEnabled
用于为onStateChanged
每个活动的切换开关的道具提供回调函数的方法。您将其定义为高阶函数,以便您可以将活动作为参数传递并返回回调函数。它检查活动是否已启用并相应地更新状态。
现在,该应用程序应类似于以下屏幕截图:
如果您希望默认禁用所有活动,而不是如初始屏幕截图所示启用,那么您可以对App
组件进行以下更改:
[src/App.js]
// ...
class App extends Component {
// Initialize app state, all activities are disabled by default
state = { enabled: false, only: [] }
toggleNotifications = ({ enabled }) => {
const { only } = this.state;
this.setState({ enabled, only: enabled ? only : [] });
}
}
在这一步中,您已经完成了拨动开关的构建。在下一步中,您将学习如何提高应用程序的可访问性。
步骤 5 — 解决可访问性问题
在您的应用程序中使用切换开关而不是传统的复选框可以让您创建更整洁的界面,特别是因为按照您的喜好设置传统复选框的样式具有挑战性。
但是,使用切换开关而不是复选框存在一些可访问性问题,因为用户代理可能无法正确解释组件的功能。
可以做一些事情来提高切换开关的可访问性并使用户代理能够正确理解角色。例如,您可以使用以下 ARIA 属性:
<switch-element tabindex="0" role="switch" aria-checked="true" aria-labelledby="#label-element"></switch-element>
您还可以在切换开关上侦听更多事件,以创建用户与组件交互的更多方式。
结论
在本教程中,您为 React 应用程序创建了一个自定义切换开关,具有支持不同主题的正确样式。您已经探索了如何在您的应用程序中使用它而不是传统的复选框。此外,您探索了所涉及的可访问性问题以及您可以做些什么来进行改进。
有关本教程的完整源代码,请查看GitHub 上的react-toggle-switch-demo存储库。您还可以在 Code Sandbox 上获得本教程的现场演示。