在前端开发工作中少不了输入框的身影,但简单的一个输入框已经满足不了设计的需求,往往需要添加点前缀Icon或者后缀Icon,等其他功能的复合型输入框。我们可以看到每个UI库都会为我们提供多功能的Input组件,今天我也实现一个自己的复合型Input组件。
在实现之前我先介绍一个需求:
可以自定义前后缀icon
可在输入框前后添加按钮或者其他自定义组件
当有input有值的时候,有一个快速清除的按钮
回车回调函数
效果展示1-1
完整组件代码:
import React, { PureComponent } from 'react';import classNames from 'classnames';import { getOtherProps } from 'utils/tool';import styles from './Input.less';export default class Input extends PureComponent { constructor(props) { super(props); this.state = { isHasCloseBtn: false, }; this.input = React.createRef(); } emptyValue = () => { this.input.current.value = ''; this.input.current.focus(); this.setState({ isHasCloseBtn: false, }); } onChange = (e) => { var value = e.target.value; if (value) { if (!this.state.value) { this.setState({ isHasCloseBtn: true, }); } } else { this.setState({ isHasCloseBtn: false, }); } if (isFunction(onChange)) { onChange(e); } } handleOnPressEnter = (e) => { const { onPressEnter } = this.props; if (e.key === 'Enter') { if (isFunction(onPressEnter)) { onPressEnter({ value: e.target.value, }, e); } } } renderLabeledInput(children) { const { addonBefore, addonAfter } = this.props; if (!addonBefore && !addonAfter) { return children; } const addonAfterGroupWrapperCls = classNames({ [styles.addonAfterGroupWrapper]: true, [styles.isString]: isString(addonAfter), }); const addonBeforeGroupWrapper = classNames({ [styles.addonBeforeGroupWrapper]: true, [styles.isString]: isString(addonBefore), }); const _addonBefore = addonBefore ? ( <span className={addonBeforeGroupWrapper}> {addonBefore} </span> ) : null; const _addonAfter = addonAfter ? ( <span className={addonAfterGroupWrapperCls}> {addonAfter} </span> ) : null; return ( <span className={styles.inputWrapper}> {_addonBefore} {React.cloneElement(children)} {_addonAfter} </span> ) } renderLabeledIcon = (children) => { const { prefix, suffix } = this.props; const _prefix = prefix ? ( <span className={styles.prefix} onClick={this.emptyValue}>{prefix}</span> ) : null; const closeBtn = (<i className={styles.closeBtn} onClick={this.emptyValue}></i>) const _suffix = this.state.isHasCloseBtn ? closeBtn : ( <span className={styles.suffix}> { suffix ? suffix : null } </span> ) return ( <span className={styles.inputGroupWrapper}> {_prefix} {React.cloneElement(children)} {_suffix} </span> ); } renderInput = () => { const { type, value, size='default', } = this.props; // 这里只对text和password做处理,因为其他type会自带一些功能,像number、date可以基于这个基础input开发 const _type = type === 'password' ? 'password' : 'text'; // 控制input的尺寸,提高了small、large、default, 具体大小 const inputCls = classNames({ [styles.input]: true, [styles.small]: (size === 'small'), [styles.large]: (size === 'large'), [styles.default]: (size === 'default'), }); // 定义了getOhterProps方法,用来获取除了第二个参数包含的其他props const otherProps = getOtherProps(this.props, ['size', 'addonAfter', 'addonBefore', 'prefix', 'suffix', 'type', 'onPressEnter', 'className', 'onChange']); if ('value' in otherProps) { otherProps.value = fixControlledValue(value); } return this.renderLabeledIcon( <input className={inputCls} type={_type} onChange={this.onChange} onKeyPress={this.handleOnPressEnter} {...otherProps} ref={this.input} /> ) } render() { return this.renderLabeledInput(this.renderInput()); } } function isFunction(el) { if (getType(el) === "[object Function]") { return true; } return false; } function isString(el) { if (getType(el) === '[object String]') { return true; } return false; } function getType(el) { return Object.prototype.toString.call(el); } function fixControlledValue(value) { if (typeof value === 'undefined' || value === null) { return ''; } return value; }
完整样式index.less
@borderColor: #d9d9d9;@disabledColor: #edeeef;@addonColor: #fafafa;@closeBtnW: 20px;.small { height: 40px; } .large { height: 60px; } .default { height: 50px; } // base css .inputWrapper { position: relative; display: table; width: 100%; .inputGroupWrapper { &:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } &:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } } } .inputGroupWrapper { position: relative; width: 100%; height: 100%; min-height: 100%; border: 1px solid @borderColor; border-radius: 4px; overflow: hidden; display: table; } .input { display: table-cell; position: relative; border: none; width: 100%; padding: 6px 12px; transition: all .3s; outline: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); &:disabled { background-color: @disabledColor; cursor: not-allowed; } &:not(:first-child) { padding-left: 32px; } &:not(:last-child) { padding-right: 32px; } &::placeholder { color: #c3c3c3; } } .suffix, .prefix, .closeBtn { position: absolute; max-height: 100%; overflow: hidden; top: 50%; transform: translateY(-50%); z-index: 2; font-size: 12px; } .suffix { right: 10px; } .prefix { left: 10px; } .closeBtn { right: 10px; width: @closeBtnW; height: @closeBtnW; border-radius: 50%; color: #fff; text-align: center; cursor: pointer; background-color: #ccc; transition: all .3s; animation: scale .2s ease-in; &::before { content: "×"; display: block; position: absolute; font-size: 12px; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; } &:active { background-color: #666; } }@keyframes scale { 0% { opacity: 0; } 100% { opacity: 1; } } .addonBeforeGroupWrapper, .addonAfterGroupWrapper { display: table-cell; position: relative; width: 1px; white-space: nowrap; vertical-align: middle; background-color: @addonColor; &.isString { padding: 0 6px; } } .addonBeforeGroupWrapper { border-top-left-radius: 4px; border-bottom-left-radius: 4px; border: 1px solid @borderColor; border-right: none; } .addonAfterGroupWrapper { border-top-right-radius: 4px; border-bottom-right-radius: 4px; border: 1px solid @borderColor; border-left: none; }
作者:内孤
链接:https://www.jianshu.com/p/60ad4f1af96d