你们常常使用的滑动条组件以下图所示
下面我教你们如何本身写一个滑动条组件。react
在写组件的第一步,咱们先作一个组件的拆分,思考一下一个滑动条锁必备的基本要素是什么。
从图上咱们能够看出,一个滑动条分为左右两个部分:左边一个 Range 组件,右边是一个 input输入框。Range组件又能够细分为 Container 组件(总体长度)和 Track 组件(进度条部分,在 Container 组件内部,children 传进去)还有一个 Point 组件(鼠标点的那个点)。
组件设计以下图所示git
看完组件的设计,咱们能够考虑下组件须要传入什么参数:github
参数 | 说明 | 是否必填 |
---|---|---|
value | 输入值 | 是 |
onChange | change事件 | 否 |
range | 选择范围 | 否 |
max | 最大范围 | 否 |
min | 最小范围 | 否 |
step | 步长 | 否 |
withInput | 是否带输入框 | 否 |
disabled | 禁用 | 否 |
className | 自定义额外类名 | 否 |
width | 宽度 | 否 |
prefix | 自定义前缀 | 否 |
主要代码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 值,一切井井有理。这值得咱们在项目中写业务组件时借鉴。设计