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