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

介绍

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

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

在禁用和启用状态下显示复选框与切换开关的表格

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

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

通知的动画 Gif 切换开关打开和显示电子邮件地址字段和过滤器通知并关闭新闻提要、喜欢和评论以及帐户登录

先决条件

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

  • 您的机器上安装了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语句之前添加以下行

源代码/索引.js
import "bootstrap/dist/css/bootstrap.min.css";

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

  • npm start

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

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

初始视图

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

第 2 步 – 创建ToggleSwitch组件

在构建组件之前,componentssrc您的项目目录中创建一个名为的新目录

  • mkdir -p src/components

接下来,创建另一个ToggleSwitchcomponents目录命名的新目录。

  • 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: 可以是返回 a 的aboolean或 a functionboolean它决定了渲染时切换开关的状态。
  • 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传递prop 并返回一个boolean指示是否应在呈现时启用开关。如果enabledprop 是 a boolean,它返回布尔值。如果是 a function,则在确定返回值是否为 a 之前首先调用该函数boolean否则,它返回false

请注意,您使用了 from 的返回值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参数对其进行了声明首先,此方法enabled使用逻辑NOT( !) 运算符切换当前状态当状态更新后,它会触发传递给onClickonStateChangedprops的回调函数

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

渲染 ToggleSwitch

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

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. 接下来,您解构组件 props 并提取restProps将传递给 switch 的props 这使您能够拦截和隔离组件的特殊道具。
  3. 接下来,根据组件状态状态,使用类名来构造开关和内部切换器的类themeenabled
  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) {}

在这里,您定义了一些默认变量并创建了一个switchmixin。在下一节中,您将实现 mixin,但首先,让我们检查switchmixin的参数

  • $size:开关元素的高度。它必须有一个长度单位。它默认为32px.
  • $spacing:圆球与开关容器之间的空间。它必须有一个长度单位。它默认为2px.
  • $stretch:用于确定开关元素的宽度应被拉伸的程度的因素。它必须是一个无单位数。它默认为1.625.
  • $color:处于活动状态时开关的颜色。这必须是有效的颜色值。请注意,无论这种颜色如何,圆形球始终为白色。
  • $class: 标识开关的基类。这用于动态创建开关的状态类。它默认为'switch-toggle'. 因此,默认状态类是.switch-toggle--on.switch-toggle--off

实现 Switch Mixin

下面是switchmixin的实现

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}选择器。

创建主题开关

现在您有了switchmixin,您将继续为切换开关创建一些主题样式。您将创建两个主题: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);

  }
}

在这里,您首先创建一个名为 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>

这是一个演示,显示了切换开关主题的外观:

默认和 Graphite-Small Toggle Switches 打开和关闭的动画 Gif

第 4 步 – 构建示例应用程序

现在您已经拥有ToggleSwitch带有必要样式React 组件,让我们继续并开始创建您在本教程开头看到的示例应用程序。

src/App.js文件修改为类似于以下代码片段:

源代码/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包含所有可以触发通知的活动。

在更新状态之前,您使用了LodashsnakeCase实用程序将活动转换为蛇形。因此,变成'News Feeds''news_feeds'

接下来,您定义了toggleNotifications()根据从通知开关接收到的状态更新应用程序状态方法。这用作传递给onStateChanged切换开关道具的回调函数请注意,当应用程序启用时,默认情况下将启用所有活动,因为onlystate 属性填充了所有活动。

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

通知切换开关打开和关闭的动画 Gif

接下来,继续查找具有此注释的行:

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

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

源代码/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="[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组件中:

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

最后,您定义了toggleActivityEnabled用于为onStateChanged每个活动的切换开关道具提供回调函数方法您将其定义为高阶函数,以便您可以将活动作为参数传递并返回回调函数。它检查活动是否已启用并相应地更新状态。

现在,该应用程序应类似于以下屏幕截图:

通知的动画 Gif 切换开关打开和显示电子邮件地址字段和过滤器通知并关闭新闻提要、喜欢和评论以及帐户登录

如果您希望默认禁用所有活动,而不是如初始屏幕截图所示启用,那么您可以对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 上获得本教程现场演示

觉得文章有用?

点个广告表达一下你的爱意吧 !😁