React封装一个复合型Input组件
2018-08-11 本文已影响2人
悟C
在前端开发工作中少不了输入框的身影,但简单的一个输入框已经满足不了设计的需求,往往需要添加点前缀Icon或者后缀Icon,等其他功能的复合型输入框。我们可以看到每个UI库都会为我们提供多功能的Input组件,今天我也实现一个自己的复合型Input组件。
在实现之前我先介绍一个需求:
- 可以自定义前后缀icon
- 可在输入框前后添加按钮或者其他自定义组件
- 当有input有值的时候,有一个快速清除的按钮
- 回车回调函数
完整组件代码:
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;
}