手把手教你寫一個滑動條組件

前言

你們常常使用的滑動條組件以下圖所示
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 值,一切井井有理。這值得咱們在項目中寫業務組件時借鑑。設計

相關文章
相關標籤/搜索