手把手教你写一个滑动条组件

前言

你们常常使用的滑动条组件以下图所示
clipboard.png
下面我教你们如何本身写一个滑动条组件。react

组件分析

在写组件的第一步,咱们先作一个组件的拆分,思考一下一个滑动条锁必备的基本要素是什么。
从图上咱们能够看出,一个滑动条分为左右两个部分:左边一个 Range 组件,右边是一个 input输入框。Range组件又能够细分为 Container 组件(总体长度)和 Track 组件(进度条部分,在 Container 组件内部,children 传进去)还有一个 Point 组件(鼠标点的那个点)。
组件设计以下图所示git

clipboard.png

看完组件的设计,咱们能够考虑下组件须要传入什么参数:github

参数 说明 是否必填
value 输入值
onChange change事件
range 选择范围
max 最大范围
min 最小范围
step 步长
withInput 是否带输入框
disabled 禁用
className 自定义额外类名
width 宽度
prefix 自定义前缀

开始开发

Slider.js

主要代码segmentfault

export default class Slider extends (PureComponent || Component) {
  static propTypes = {
    className: PropTypes.string,
    prefix: PropTypes.string,
    max: PropTypes.number,
    min: PropTypes.number,
    value: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.arrayOf(PropTypes.number),
    ]).isRequired,
    disabled: PropTypes.bool,
    range: PropTypes.bool,
    step: PropTypes.number,
    withInput: PropTypes.bool,
    onChange: PropTypes.func,
    width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  };

  static defaultProps = {
    min: 0,
    max: 100,
    step: 1,
    prefix: 'zent',
    disabled: false,
    withInput: true,
    range: false,
    value: 0,
  };

  constructor(props) {
    super(props);
  }

  onChange = value => {
    const { range, onChange } = this.props;
    value = range
      ? value.map(v => Number(v)).sort((a, b) => a - b)
      : Number(value);
    onChange && onChange(value);
  };

  render() {
    const { withInput, className, width, ...restProps } = this.props;
    const wrapClass = classNames(
      `${restProps.prefix}-slider`,
      { [`${restProps.prefix}-slider-disabled`]: restProps.disabled },
      className
    );
    return (
      <div className={wrapClass} style={getWidth(width)}>
        <Range {...restProps} onChange={this.onChange} />
        {withInput &&
           (
            <InputField onChange={this.onChange} {...restProps} />
          )}
      </div>
    );
  }
}

主要逻辑和上文讲的同样,组件主要构成是一个 Range 和 一个 Input,咱们主要看下 Range 的实现ide

export default class Range extends (PureComponent || Component) {
  clientWidth = null;

  getClientWidth = () => {
    if (this.clientWidth === null) {
      this.handleResize();
    }
    return this.clientWidth;
  };

  handleResize = () => {
    const $root = ReactDOM.findDOMNode(this);
    this.clientWidth = $root.clientWidth;
  };

  render() {
    const { value, ...restProps } = this.props;
    const warpClass = cx(`${restProps.prefix}-slider-main`, {
      [`${restProps.prefix}-slider-main-with-marks`]: marks,
    });
    return (
      <div className={warpClass}>
        <Container
          getClientWidth={this.getClientWidth}
          {...restProps}
          value={value}
        >
          <Track {...restProps} value={value} />
        </Container>
        <Point
          dots={dots}
          marks={marks}
          getClientWidth={this.getClientWidth}
          {...restProps}
          value={value}
        />
        <WindowEventHandler eventName="resize" callback={this.handleResize} />
      </div>
    );
  }
}

Range 组件里的 Point 就是滑动条鼠标能够拖动的小点, container 是滑动条主要部分,传进去的 track组件,是滑动条的有效部分。这里有一个WindowEventHandler 组件,这个组件的目的是在组件mount的时候给 window 绑定了一个 {eventName} 事件,而后unmount的时候中止监听 {eventName} 事件。更加优雅的实如今这篇文章中有介绍,如何在react组件中监听事件oop

咱们看下 Container 组件内部实现, 其实很简单,只须要作两件事
1.处理点击滑动条事件
2.渲染 Track 组件ui

export default class Container extends (PureComponent || Component) {
  handleClick = e => {
    const {
      getClientWidth,
      dots,
      range,
      value,
      onChange,
      max,
      min,
      step,
    } = this.props;
    let newValue;

     let pointValue =
        (e.clientX - e.currentTarget.getBoundingClientRect().left) /
        getClientWidth();
      pointValue = getValue(pointValue, max, min);
      pointValue = toFixed(pointValue, step);
      newValue = pointValue;
      if (range) {
        newValue = getClosest(value, pointValue);
      }
      onChange && onChange(newValue);
    
  };

  render() {
    const { disabled, prefix } = this.props;
    return (
      <div
        className={`${prefix}-slider-container`}
        onClick={!disabled ? this.handleClick : noop}
      >
        {this.props.children}
      </div>
    );
  }
}

Track 组件也很简单,其实就是根据传入的参数,计算出 left 值和有效滑动条长度this

export default class Track extends (PureComponent || Component) {
  getLeft = () => {
    const { range, value, max, min } = this.props;
    return range ? getLeft(value[0], max, min) : 0;
  };

  getWidth = () => {
    const { max, min, range, value } = this.props;
    return range
      ? (value[1] - value[0]) * 100 / (max - min)
      : getLeft(value, max, min);
  };

  render() {
    const { disabled, prefix } = this.props;
    return (
      <div
        style={{ width: `${this.getWidth()}%`, left: `${this.getLeft()}%` }}
        className={calssNames(
          { [`${prefix}-slider-track-disabled`]: disabled },
          `${prefix}-slider-track`
        )}
      />
    );
  }
}

Point 组件则着重处理了拖动状态的变化,以及拖动边界的处理, 代码比较简单易读spa

export default class Points extends (PureComponent || Component) {
  constructor(props) {
    super(props);
    const { range, value } = props;
    this.state = {
      visibility: false,
      conf: range ? { start: value[0], end: value[1] } : { simple: value },
    };
  }

  getLeft = point => {
    const { max, min } = this.props;
    return getLeft(point, max, min);
  };

  isLeftButton = e => {
    e = e || window.event;
    const btnCode = e.button;
    return btnCode === 0;
  };

  handleMouseDown = (type, evt) => {
    evt.preventDefault();
    if (this.isLeftButton(evt)) {
      this.left = evt.clientX;
      this.setState({ type, visibility: true });
      let { value } = this.props;

      if (type === 'start') {
        value = value[0];
      } else if (type === 'end') {
        value = value[1];
      }
      this.value = value;
      return false;
    }
  };

  getAbsMinInArray = (array, point) => {
    const abs = array.map(item => Math.abs(point - item));
    let lowest = 0;
    for (let i = 1; i < abs.length; i++) {
      if (abs[i] < abs[lowest]) {
        lowest = i;
      }
    }
    return array[lowest];
  };

  left = null;

  handleMouseMove = evt => {
    const left = this.left;
    if (left !== null) {
      evt.preventDefault();
      const { type } = this.state;
      const {
        max,
        min,
        onChange,
        getClientWidth,
        step,
        dots,
        marks,
        range,
      } = this.props;
      let newValue = (evt.clientX - left) / getClientWidth();
      newValue = (max - min) * newValue;
      newValue = Number(this.value) + Number(newValue);
      if (dots) {
        newValue = this.getAbsMinInArray(keys(marks), newValue);
      } else {
        newValue = Math.round(newValue / step) * step;
      }
      newValue = toFixed(newValue, step);
      newValue = checkValueInRange(newValue, max, min);
      let { conf } = this.state;
      conf[type] = newValue;
      this.setState({ conf });
      onChange && onChange(range ? [conf.start, conf.end] : newValue);
    }
  };

  handleMouseUp = () => {
    this.left = null;
    this.setState({ visibility: false });
  };

  componentWillReceiveProps(props) {
    const { range, value } = props;
    if (this.left === null) {
      this.setState({
        conf: range ? { start: value[0], end: value[1] } : { simple: value },
      });
    }
  }

  render() {
    const { visibility, type, conf } = this.state;
    const { disabled, prefix } = this.props;
    return (
      <div className={`${prefix}-slider-points`}>
        {map(conf, (value, index) => (
          <ToolTips
            prefix={prefix}
            key={index}
            content={value}
            visibility={index === type && visibility}
            left={this.getLeft(value)}
          >
            <span
              onMouseDown={
                !disabled ? this.handleMouseDown.bind(this, index) : noop
              }
              className={classNames(
                { [`${prefix}-slider-point-disabled`]: disabled },
                `${prefix}-slider-point`
              )}
            />
          </ToolTips>
        ))}
        {!disabled && (
          <WindowEventHandler
            eventName="mousemove"
            callback={this.handleMouseMove}
          />
        )}
        {!disabled && (
          <WindowEventHandler
            eventName="mouseup"
            callback={this.handleMouseUp}
          />
        )}
      </div>
    );
  }
}

结语

以上代码采样自 zent,从组件的设计能够看出,组件的设计采用了单一指责原则,把一个滑动条拆分为 Range 和 Input,Range 有拆分为 Point、 Container、 Track 三个子组件,每一个组件互不干扰,作本身组件的事情,状态都在组件内部维护,状态改变统一触发根组件 onchange 事件经过 props 改变其余受影响的组件,例如点击 Container 改变了value的同时触发了 onchange 改变了 Points 的 left 值,一切井井有理。这值得咱们在项目中写业务组件时借鉴。设计

相关文章
相关标签/搜索