最近在写一个面向 React 初学者的系列教程玩转 React,内容对有 React 开发经验的同窗来讲可能太过于基础和啰嗦,不太感兴趣。因此我打算同时开始另一个系列文章《React 开发实战》。该系列主要面向有 React 开发经验的同窗,更侧重 React 实战,每一篇文章会跟你们一块儿开发一个 React 组件或者一个简单有趣的 React 应用,这些组件或者应用每每知足以下特色:javascript
若是这些组件能直接应用到你们的实际开发中去,那再好不过了;若是不能,能给你们一点启发,我以为这件事情也是颇有价值的。css
另外,每一篇文章后面都会附有本篇文章的完整示例和代码。java
你们应该都见过这种应用场景,页面上的某一部分,须要可以让用户添加任意多项。web
多是表单中的一个字段,以下所示。segmentfault
也多是表单的一部分,以下所示,用户能够在一个表单内增长多个用户信息,而后将用户信息批量进行保存。api
还有更变态的,以下所示,一个表单内用户信息部分能够添加多份,每个用户信息中地址也能够添加多份。(Oh, My God. PM,你杀了我吧。)数组
还好,React 应付这种需求,仍是小菜一碟。可是在一个 web 应用中有这么多的类似场景的话,若是咱们挨个实现一遍,那真是太枯燥了,与搬砖无异。遇到这种状况,就须要咱们把相同的功能抽象出来,作成组件,这将极大地提高你的开发效率。数据结构
基于这个场景,咱们今天就开发一个能让其 children 重复任意多份的组件,咱们就称之为 Repeat 吧。函数
在开发一个组件的时候,不要着急写代码,先想一想你要把这个组件作成什么样子,例如这个 Repeat 组件,我但愿有以下特性:ui
而后在代码中我指望能够这样来用 Repeat 这个组件:
class App extends React.Component {
handleChange(items) {
console.info(items);
}
render() {
<Repeat onChange={items => this.handleChange(items)}>
<input type="text" /> </Repeat>
}
}复制代码
OK,就是这么简单,这样 Input 组件就能够重复加添多份了。基于这个构想,咱们来实现 Repeat 这个组件。
class Repeat extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [''],
};
}
handleChange(e, index) {
const items = [...this.state.items];
items[index] = e.target.value;
this.setState({ items });
this.props.onChange(items);
}
handleAddItem(e, index) {
e.preventDefault();
const items = [...this.state.items];
items.splice(index, 0, '');
this.setState({ items });
}
handleRemoveItem(e, index) {
e.preventDefault();
if (this.state.items.length === 1) return;
const items = [...this.state.items];
items.splice(index, 1);
this.setState({ items });
}
render() {
const children = React.Children.only(this.props.children);
const elementItems = this.state.items.map((item, index) => (
<div key={index}> { React.cloneElement(children, { onChange: e => this.handleChange(e, index), value: item, }) } <div> <a href="#" onClick={e => this.handleAddItem(e, index)}>添加</a> <a href="#" onClick={e => this.handleRemoveItem(e, index)}>移除</a> </div> </div>
));
return <div>{elementItems}</div>;
}
}复制代码
代码很简单,简单解释一下:
state
中持有 items
字段来保存每个项的数据。children
,而后 map 组件 state
中的 items
,将每一项映射为 children
的一个副本。并为这个副本传入两个属性,onChange
接收每一项的数据变化,value
传递每一项当前应展现的值。Repeat
为每一项准备了一个“添加”按钮和一个“移除”按钮,用来在当前项位置新增一项或者移除当前项。原理就是将 this.state.items
中对应下标处的数组元素删掉就行了。到此,Repeat
是否是大体有模有样了呢。须要提醒你们的是,React.cloneElement
和 React.Children.xxx
这些 api 一般只会在这种公共组件中使用,在大部分场景,尽可能少用。
有些同窗可能已经发现了,上面例子中, Repeat
的 children
是个 input
,那若是是一个其余的组件不就完蛋了嘛。
这是第一个问题,为了解决这个问题呢,Repeat 须要对它的 children
提两个条件:
属性上必需要接收一个 onChange
回调函数,函数接收一个对象参数,参数结构以下:
{
target: {
value: 'xxxx'
}
}复制代码
value
的值为当前项产出的数据,多是个对象也多是字符串或者数值。没错,我就是为了兼容 input event 的数据结构。你固然能够用任何你喜欢的且方便处理的数据结构。
children
组件须要接收一个 value 属性,以展现其拥有的值。也就是说 children
组件应当是一个受控的(controlled)组件。
这就是一个协议,你但愿某个组件内经过 Repeat
组件方便地添加多份并能获取到一组数据,那就必需要遵照这个协议。有同窗可能会说为何不搞的智能一点呢?嗯,这里我想分享一点我的经验:有些时候,尤为是在业务开发过程当中,把公共部分抽取出来复用便可,点到为止,没有必要搞得那么“强大”,剩下的事情让一个很容易遵照的协议来完成,其实效率会更高,更容易让人理解。
其实在计算机的世界中到处充满了协议,例如你想让 HTTP Server 返回正确的响应,你必需要遵循 http 协议来和它通讯;你生产的显卡能买的出去,必需要遵照相应的协议,要能插到别人家生产的主板上。
扯远了!收!
对,有了上面这个约定之后,Repeat
一行代码未加,是否是感受功能完善了许多?嗯,就是这个目的。如今咱们来实现一下文章开始时候说的第二个场景。
聪明的你必定已经知道该怎么作了,没错,只要咱们实现一个 UserForm
组件,并让他知足上面的约定便可。请看代码:
class UserForm extends React.Component {
handleFieldChange(e) {
const { name, value } = e.target;
const formData = {
...this.props.value,
[name]: value,
}
this.props.onChange({
target: {
value: formData,
}
});
}
render() {
const formData = this.props.value || {};
return (
<div>
<div>
<label for="">姓名</label>
<input
type="text"
name="name"
value={formData.name}
onChange={e => this.handleFieldChange(e)}
/>
</div>
<div>
<label for="">地址</label>
<input
type="text"
name="addr"
value={formData.addr}
onChange={e => this.handleFieldChange(e)}
/>
</div>
</div>
)
}
}复制代码
为了让代码更简洁,我把 UserForm
这个组件实现为了一个支持受控的组件,可是在目前的业务场景下已经足够了,在实际状况下,你能够按需调整。
经过这个例子,还但愿你们能体会到组件拆分的一个好处。就是,UserForm
和 Repeat
拆分红两个组件之后,UserForm
的复用性会更强。能够想象一下,当用户被批量添加之后,是否是有可能在编辑单个用户的时候,能够继续使用这个组件。
好啦,关于第三个场景我想就没有必要再实现一遍了,Repeat 嵌套多少层其实都是能够的。
实际上在实际应用中,Repeat 这个组件还须要作进一步完善,其中一个就是样式,还有可能在不一样的场景下,虽然交互都是这样,但样式会有所差别。另外默认是“添加”、“移除”两个文字按钮,说不定实际业务场景中是两个 +,- 的图标按钮;还有可能“添加”、“移除”的位置为有所变化。
这些问题怎么处理呢?下面给你们描述下思路,具体代码就不写了,若是有什么疑问能够给我留言。
关于样式,你能够给 Repeat
添加 itemClassName
和 buttonsClassName
两个属性分别为每一项和按钮区域的 css class。这样你就能够在不一样的场景下指定不一样的样式了。
关于如何将文字按钮改成图标按钮,你能够给 Repeat
添加 renderButtons
这样一个函数属性,若是未指定则用默认的方式渲染按钮,若是有则勇气返回值渲染属性。
这是本篇文章的代码:codepen.io/Sarike/pen/…
好啦,文章就到这吧,若是有什么疑问能够给我留言。谢谢你们,祝你们国庆、中秋节快乐。
PS:本系列的全部文章将在 segmentfault 和 掘金同步发布。
本做品保留全部权利。未得到许可人许可前,不容许他人复制、发行、展览和表演做品。不容许他人基于该做品创做演绎做品 。