如何使用React构建自定义切换开关

来自菜鸟教程
跳转至:导航、​搜索

介绍

构建 Web 应用程序通常涉及为用户交互做准备。 为用户交互做准备的重要方式之一是通过表单。 存在用于从用户获取不同类型的输入的不同表单组件。 例如,密码组件从用户那里获取敏感信息并将其屏蔽,使其不可见。

大多数情况下,您需要从用户那里获取的信息是 boolean-like — 例如,yesnotruefalseenabledisableonoff等。 传统上,复选框表单组件用于获取这些类型的输入。 然而,在现代界面设计中,拨动开关通常用作复选框的替代品,尽管存在一些可访问性问题。

在本教程中,您将看到如何使用 React 构建自定义切换开关组件。 在本教程结束时,您将拥有一个使用自定义切换开关组件的演示 React 应用程序。

这是您将在本教程中构建的最终应用程序的演示:

先决条件

在开始之前,您需要以下内容:

第 1 步 — 入门

首先,使用 npxcreate-react-app 创建一个新的 React 应用程序。 您可以随意命名应用程序,但本教程将使用 react-toggle-switch

npx create-react-app react-toggle-switch

接下来,您将安装应用程序所需的依赖项。 使用终端窗口导航到项目目录:

cd react-toggle-switch

运行以下命令以安装所需的依赖项:

npm install bootstrap@4.5.0 lodash@4.17.15 prop-types@15.7.2 classnames@2.2.6 node-sass@4.14.1

注意:通过参考最低支持快速指南,确保您正在安装的node-sass版本与您的环境兼容。


您安装了 bootstrap 包作为应用程序的依赖项,因为您需要一些默认样式。 要在应用程序中包含 Bootstrap,请编辑 src/index.js 文件并在每个其他 import 语句之前添加以下行:

src/index.js

import "bootstrap/dist/css/bootstrap.min.css";

通过使用 npm 运行以下命令来启动应用程序:

npm start

随着应用程序的启动,开发就可以开始了。 请注意,使用 实时重新加载 功能为您打开了一个浏览器选项卡。 实时重新加载将与您开发时对应用程序的更改保持同步。

此时,应用程序视图应类似于以下屏幕截图:

接下来,您将创建切换组件。

第 2 步 — 创建 ToggleSwitch 组件

在构建组件之前,在项目的 src 目录中创建一个名为 components 的新目录。

mkdir -p src/components

接下来,在 components 目录中创建另一个名为 ToggleSwitch 的新目录。

mkdir -p src/components/ToggleSwitch

您将在 src/components/ToggleSwitch 中创建两个新文件,即:index.jsindex.scss。 使用您喜欢的文本编辑器创建并打开 index.js 文件:

nano src/components/ToggleSwitch/index.js

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:可以是 boolean 或返回 booleanfunction,它决定了切换开关在渲染时的状态。
  • onStateChanged:是一个回调函数,当拨动开关的状态发生变化时会被调用。 这对于在切换开关时触发父组件上的操作很有用。

初始化 ToggleSwitch 状态

在下面的代码片段中,您初始化了 ToggleSwitch 组件的状态,并定义了一些组件方法来获取拨动开关的状态。

src/components/ToggleSwitch/index.js

// ...

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 属性,并返回一个 boolean 指示在渲染时是否应启用开关。 如果 enabled 属性是 boolean,它返回布尔值。 如果是function,则在判断返回值是否为boolean之前先调用函数。 否则,它返回 false

请注意,您使用 enabledFromProps() 的返回值来设置初始 enabled 状态。 您还添加了 isEnabled() 方法来获取当前的 enabled 状态。

切换 ToggleSwitch

让我们继续添加在单击时切换开关的方法。 将以下代码添加到文件中:

src/components/ToggleSwitch/index.js

// ...

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 参数声明它。 首先,此方法使用逻辑 NOT (!) 运算符切换当前 enabled 状态。 当状态更新时,它会触发传递给 onClickonStateChanged 属性的回调函数。

请注意,由于 onClick 需要一个事件作为其第一个参数,因此您使用包含新状态对象的附加 SWITCH_STATE 属性来扩充事件。 但是,使用新状态对象调用 onStateChanged 回调。

渲染 ToggleSwitch

最后,让我们实现 ToggleSwitch 组件的 render() 方法。 将以下代码添加到文件中:

src/components/ToggleSwitch/index.js

// ...

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() 方法发生了很多事情,所以让我们分解一下:

  1. 首先,enabled 状态从组件状态中解构。
  2. 接下来,您解构组件道具并提取将传递给交换机的 restProps。 这使您能够拦截和隔离组件的特殊道具。
  3. 接下来,根据组件的 themeenabled 状态,使用 classnames 构造开关和内部切换器的类。
  4. 最后,使用适当的道具和类来渲染 DOM 元素。 请注意,您传入 this.toggleSwitch 作为交换机上的 click 事件侦听器。

保存并关闭文件。

您现在已经创建了 ToggleSwitch

第 3 步 — 设置 ToggleSwitch 的样式

现在您已经有了 ToggleSwitch 组件及其所需的功能,您可以继续为它编写样式。

使用您喜欢的文本编辑器打开 index.scss 文件:

nano src/components/ToggleSwitch/index.scss

将以下代码段添加到文件中:

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 的实现:

src/components/ToggleSwitch/index.scss

// ...

@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,您将继续为切换开关创建一些主题样式。 您将创建两个主题:defaultgraphite-small

将以下代码片段附加到 src/components/ToggleSwitch/index.scss 文件中:

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);

  }
}

在这里,您首先创建一个名为 get-switch-class 的 Sass 函数,它以 $selector 作为参数。 它通过一系列 Sass 函数运行 $selector 并尝试提取第一个类名。 例如,如果它收到:

  • .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 文件修改为如下代码片段:

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 状态属性填充了所有活动。

最后,您为应用程序渲染了 DOM 元素,并为通知选项留了一个槽,这将很快添加。 此时,应用程序应类似于以下屏幕截图:

接下来,继续查找包含此注释的行:

{/* ... Notification options here ... */}

并将其替换为以下内容以呈现通知选项:

src/App.js

// ...

{ 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="mail@domain.com" 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 组件中添加以下方法:

src/App.js

// ...

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 主题。 您还可以通过检查 only 状态变量中是否已存在每个活动来检测每个活动的 enabled 状态。

最后,您定义了 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 上获得本教程的 现场演示。