[回到首页](../) | [上一章：3.数组渲染与操作](./3.数组渲染与操作.md)

# 4.表单渲染与操作

这一章，我们来看看如何在 pastate 里渲染和操作表单元素。

* [使用原生表单元素](#使用原生表单元素)
* [使用 pastate 双向绑定输入组件](#使用-pastate-双向绑定输入组件)
    * [Input 文本框](#input-文本框)
    * [Checkbox 复选框](#checkbox-复选框)
    * [RadioGroup 单选框选项组](#radiogroup-单选框选项组)
    * [Select 下拉选择框](#select-下拉选择框)
* [pastate 表单组件的类型定义](#pastate-表单组件的类型定义)
* [对现有组件库进行双向数据绑定](#对现有组件库进行双向数据绑定)
    * [使用 Bind 高阶组件](#使用-bind-高阶组件)
    * [使用 makeBindable 函数](#使用-makebindable-函数)
* [对于绑定的值为空的情况](#对于绑定的值为空的情况)

## 使用原生表单元素
我们在  `BasicInfoView` 组件的两个按钮下面添加一个输入框用于输入姓名，并添加一个勾选框用于选择性别，更改如下：
```javascript
class BasicInfoView extends PureComponent {
    render() {
        ...
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                <div>
                    <button onClick={this.decreaseAge}> decrease age </button>
                    <button onClick={this.increaseAge}> increase age </button>
                </div>
                <div>
                    name: <input type="text" value={state.name} onChange={this.handleNameChange}/> <br />
                    Is boy: <input type="checkbox" checked={state.isBoy == true} onChange={this.handleIsBoyChange}/>
                </div>
            </div>
        )
    }
}
```
上面添加两个了 input 标签，第一个 input 使用 name 数据, 第二个 input 使用 isBoy 数据。同时我们也先指定两个 input 的 onChange 处理函数。    
注意：如前面章节所提及，对于 imState 布尔值，请记得使用显式布尔值方式：`checked={state.isBoy == true}`。  

接下来看看如何实现两个 onChange 处理函数：
```javascript
class BasicInfoView extends PureComponent {
    ...
    handleNameChange(e){
        state.basicInfo.name = e.target.value
        store.sync() // 编辑中的输入框，需手动同步store
    }
    handleIsBoyChange(e){
        state.basicInfo.isBoy = e.target.checked
    }
    ...
}
```
非常熟悉和简单！你只需把更新的值赋给目标state节点即可。我们会发现多了个 `store.sync() `，这个函数是让 pastate 立刻执行数据同步更新。由于很多输入法会在输入过程中会把带下划线的 “拼音字母” 输入到 input 中，如下：  

![把拼音值输入文本框的输入法](http://upload-images.jianshu.io/upload_images/1234637-84a3bc353d847def.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

如果 pastate 执行异步更新会使带下划线 “拼音字母” 中断，因此我们在更新 **编辑中的输入框** 时，需要简单地使用  `store.sync() ` 执行同步更新，让 “拼音” 连续。如果不是对 **编辑中的输入框**  进行修改或不需要支持输入法输入（如密码等），无需使用 `store.sync() ` 。

初步完成！我们试着往输入框输入文字，或点击勾选框, 可以看到文字区域的值与 input 区域的视图都正常地进行更新！

与普通 react 表单的处理模式一样，我们可以通过 onChange 函数的实时控制输入的内容，如转化大小写、控制字符串长度等：  
```javascript
class BasicInfoView extends PureComponent {
    ...
    // 把输入值都转化为大写
    handleNameChange_uppercase(e){
        state.basicInfo.name = e.target.value.toUpperCase()
        store.sync()
    }

    // 控制文本长度在10个字符以内
    handleNameChange_limitedLength(e){
        if(e.target.value.length > 10) return；
        state.basicInfo.name = e.target.value
        store.sync()
    }
    ...
}
```

## 使用 pastate 双向绑定输入组件

使用过 vue.js 或 angular.js 的人都体验过自动双向数据绑定 (two-ways data binding) 的便捷性，但由于 react state 渲染的单向数据流原则，在 react 中没有默认提供这个功能。而 pastate 的 imState 具有保存节点信息的特殊性，能够很容易实现自动双向数据绑定功能！  

pastate 为你提供了四个已实现双向数据绑定的高阶表单组件 (Higher Order Component, 通常简称 HOC) ，这些组件都是基于 PureComponent 实现的，具有良好的渲染性能，你可以尽情享用他们！这四个表单组件如下：

- `Input` : 文本框
- `Checkbox` : 复选框
- `RadioGroup` : 单选框选项组
- `Select` : 下拉选择框

### Input 文本框
Input 组件可以用于显示单行输入框和多行输入框，只需要传入要绑定的值，当你通过界面更改输入框的值时，Input 组件会自动为你更新所绑定的 state ：
```javascript
import { ..., Input } from 'pastate'
...
render(){
    let state = this.props.state
    return(
        ...
        <Input value={state.text1} /> {/** 单行输入框，内部使用 <input type="text" /> 实现 */}
        <Input value={state.text2} type="textarea" /> {/** 多行输入框，内部使用 <textarea /> 实现 */}
        ...
    )
}
...
```
Input 组件的属性及其说明如下：  

属性 | 值 | 说明 
- | - | - 
value | string \| number , 必填 | 绑定的值，需要使用 this.props.state 中的节点 , 即 imState
type | "text" \| "textarea" \| "password" \| "number" | 输入框类型，默认为 "text"
beforeChange | (newValue: string \|  number, oldValue: string \|  number) => string \|  number | 在绑定值更新前会被调用，可用于实现自定义字符串更新逻辑, 如控制大小写或限制字符串长度等；返回值为最终要更新的值 
afterChange | (newValue: string \|  number) => void | 在绑定值更新后会被调用
disabled | boolean | 指定输入框是否处于禁止状态，默认为 false
className | string | 传递给输入框的 class 名 ( 用于指定 css 样式等 )
id | string | 传递给输入框的 id 名 ( 用于指定 css 样式等 )
useComposedValue | boolean |  [ **实验特性** ]  指定是否在输入法完成拼音过程时才更新 state 的模式，开启后，在输入拼音的过程中绑定的值不会更新，默认为 false

Input 组件实现了自动绑定数据双向绑定功能，如果你需要自定义字符串更新逻辑，或者在字符值更新后做一些操作，可以通过指定可选的 beforeChange 函数和 afterChange 函数来实现。    

```javascript
...

handleTextBeforeChange(newValue, oldValue) {
    // 把输入的字符转化为大写
    return newValue.toUpperCase()
}

render(){
    let state = this.props.state
    return(
        ...
        <Input value={state.text1} beforeChange={this.handleTextBeforeChange} /> 
        ...
    )
}
...
```  

Input 组件的属性 useComposedValue 开启后，可以实现在输入法输入拼音的过程中绑定的值不会更新的功能，等拼音输入完成后 state 值才更新，减少拼音输入过程中不必要的 state 更新和视图渲染动作：

![useComposedValue 效果](http://upload-images.jianshu.io/upload_images/1234637-8b51fdc0448b04f3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

useComposedValue 属性目前为实验特性，如果发现在某个浏览器或某种输入法中有问题，欢迎提交 [issue](https://github.com/BirdLeeSCUT/pastate/issues) 。

### Checkbox 复选框
每个 Checkbox 是一个勾选框组件，只需传递要绑定的布尔值 state 节点，即可完成绑定：
```javascript
import { ..., Checkbox } from 'pastate'
...
render(){
    let state = this.props.state
    return(
        ...
        I am a boy: <Checkbox value={state.isBoy} />
        ...
    )
}
...
```  

![复选框](http://upload-images.jianshu.io/upload_images/1234637-56a894cea34b4408.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)  

Checkbox 组件的属性及其说明如下:   

属性 | 值 | 说明 
- | - | - |
value | boolean，必填 | 绑定的数据值，可直接传入 this.props.state 中的节点值，**无需**做 `state.xxx == true` 转化。 
afterChange | (newValue: boolean) => void | 在绑定值更新后会被调用
disabled | boolean | 指定禁止点击状态，默认为 false
className | string | 传递 class 名 ( 用于指定 css 样式等 )
id | string | 传递 id 名 ( 用于指定 css 样式等 )

### RadioGroup 单选框选项组
RadioGroup 是一个单选框**选项组**，只要传入选项数组 options 常数和要绑定的选项值 value, 即可完成绑定:  

```javascript
import { ..., RadioGroup } from 'pastate'
const goodNames = ['Peter', 'Tom', 'Allen']

...
render(){
    let state = this.props.state
    return(
        ...
        Choose a name:  <RadioGroup options={goodNames} value={state.name}/>
        ...
    )
}
...
```  

![单选框选项组](http://upload-images.jianshu.io/upload_images/1234637-309a991ad9ee5453.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

RadioGroup 组件的属性及其说明如下:   

属性 | 值 | 说明 
- | - | - |
options | Array<string  \| number \| boolean> \| Array<{value: string \| number \| boolean, tag: string, disabled?: boolean}>, 必填 | 选项数据数组
value | string \| number \| boolean，必填 | 绑定的选中值
disabled | boolean | 指定禁止选择状态，默认为 false
vertical | boolean | 指定选项为垂直排列状态，默认为 false
afterChange | (newValue: string \| number \| boolean) => void  | 在绑定值更新后会被调用
id | string | 传递给选项组根元素的 id
className |  string | 传递给选项组根元素的 className
radioClassName | string | 传递给圆形按钮的 className
tagClassName | string | 传递给选项标签的 className
disabledTagClassName | string | 传递给禁用状态的选项标签的**附加**的 className

options 属性接受两种格式的选项数据格式：
```javascript
// 简单格式: Array<string> 
const nameOptions = ['Peter', 'Tom', 'Allen']

// 完备格式: Array<{value: string, tag: string, disabled?: boolean}>
const nameOptionsChinese = [{
    value: 'Peter', // 数据值
    tag: '彼得', // 选项标签值
    disabled: true // 可选地指定某个选项为不可选
},{
    value: 'Tom',
    tag: '汤姆'
},{
    value: 'Allen',
    tag: '艾伦'
}]
```

`RadioGroup` 是基于 PureComponent 实现的，对于 options 属性的值，建议定义一个 **文件级** 的选项数组常量, 这样可以提高渲染效率。如果把 options 值定义在 render 函数里或写成直接赋值的匿名对象（`<RadioGroup options={["a", "b"]} ... >`）, 在每次父组件渲染时，无论绑定的 value 数据有没有更新，RadioGroup 获取到的 options 属性的引用值都不一样，会使 RadioGroup 进行多余的渲染动作。 当你使用其他基于 PureComponent 实现的组件时，在向其传递数组 / 对象类型的常量的时候也可以做这样的性能优化。

### Select 下拉选择框
Select 是一个选择框组件，使用方法与 RadioGroup 类似，只要传入选项数组和要绑定的选择值即可完成绑定：
```javascript
import { ..., Select } from 'pastate'

const nameOptionsChinese = [{
    value: 'DEFAULT',
    tag: '请选择',
    disabled: true
},{
    value: 'Peter',
    tag: '彼得'
}, {
    value: 'Tom',
    tag: '汤姆'
}, {
    value: 'Allen',
    tag: '艾伦'
}]

...
render(){
    let state = this.props.state
    return(
        ...
        Choose a name:  <Select options={nameOptionsChinese} value={state.name}/>
        ...
    )
}
...
```

![下拉选择框](http://upload-images.jianshu.io/upload_images/1234637-de52b3e63bddb83d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![不同操作系统、不同浏览器的下拉样式不一样](http://upload-images.jianshu.io/upload_images/1234637-a6dfd78f6eb5b70f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


Seclect 组件的属性及其说明如下:   

属性 | 值 | 说明 
- | - | - |
options | Array<string \| number \| boolean> \| Array<{value: string  \| number \| boolean, tag: string, disabled?: boolean}>, 必填 | 选项数据数组
value | string  \| number \| boolean，必填 | 绑定的选中值
disabled | boolean | 指定禁止点击状态，默认为 false
afterChange | (newValue: string  \| number \| boolean) => void  | 在绑定值更新后会被调用
id | string | 传递的 id
className |  string | 传递的 className

如有需要显示没有选择的状态，可以多设置一个选项元素，通过元素的 tag 设置其提示文本, 并把元素的 disabled 设为 true 即可，没选中时的 value 值自行定义，不可为 null 或 undefined: 
```javascript
const nameOptionsChinese = [{
    value: 'DEFAULT',
    tag: '请选择',
    disabled: true
}, ... ]
```

Seclect 组件目前仅支持最常用的单选功能，以后将支持多选功能。

## pastate 表单组件的类型定义

Pastate 的高阶组件均是使用 Typescript 进行开发 , 提供完整的类型定义文件，当你在 javascript 项目使用它们时，也可以得到良好的组件属性 intelliSense 类型提示：

![组件的属性类型提示](http://upload-images.jianshu.io/upload_images/1234637-c0b845bffb9df877.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

你也可以右键点击组件名，选择`转到类型定义`， 查看组件的所有属性声明：

![选择转到类型定义](http://upload-images.jianshu.io/upload_images/1234637-934de850267f182b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![组件的类型定义文件](http://upload-images.jianshu.io/upload_images/1234637-32b8cceaa1f60910.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

上图的 type 属性的类型是**枚举字符串**，在输入时，你可以在空双括号中按下 vsCode 的 “触发提示” 快捷键(具体快捷键因不同系统操作系统和不同设置而异，请到编辑器的 “首选项->设置快捷方式” 处查看，该功能很实用)：

![属性值提示](http://upload-images.jianshu.io/upload_images/1234637-b29b71a467582bbe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

## 对现有组件库进行双向数据绑定

很多时候，我们会使用 react 视图组件库来开发应用，如 [ant.design](https://ant.design)、 [material-ui](http://www.material-ui.com) 等，pastate 为这些现有的视图组件提供两个数据双向绑定方法！下面我们以 ant.design 的 [Rate](https://ant.design/components/rate-cn/) 星级评分组件为例进行介绍。

### 使用 Bind 高阶组件
你可以使用 pastate 提供的 Bind 组件去包围一个原始的视图组件，实现双向数据绑定。
我们先简单地安装 ant.design 组件库： `$ npm install antd --save` 或 `$ yarn add antd`，引入 Rate 组件，并使用 Bind 组件进行包装，实现双向数据绑定：
```javascript
import { ..., Bind } from 'pastate';

import Rate from 'antd/lib/rate';
import 'antd/lib/rate/style/css';
// 或经过简单配置后，使用 import { Rate } from 'antd'， 详见 ant.design 文档
...

// 我们使用 state.basicInfo.age  数据进行演示

class BasicInfoView extends PureComponent {
    ...
    render() {
        /** @type {initState['basicInfo']} */
        let state = this.props.state;
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                <div>
                    年龄(Bind): 
                    <Bind value={state.age} > 
                        <Rate />
                    </Bind>
                </div>
                ...
            </div>
        )
    }
}
```
我们使用 Bind 组件对 Rate 组件进行包装，并把本来需要传递给 Rate 组件的 value 值传递通过 Bind 组件进行传递，这样就实现了双向数据绑定！如果我们要传递其他属性值给 Rate, 可以不通过 Bind 直接传递给 Rate，如下：

```javascript
...
<Bind value={state.age} > 
    <Rate count={10} /> {/* 根据 ant design 文档， count 属性指定采用 n 级评分 */}
</Bind>
...
```
这样我们就实现了对 Rate 进行双向数据绑定:

![成功绑定 Rate 组件到 age 数据](http://upload-images.jianshu.io/upload_images/1234637-f472888e2d70814b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


你可以通过点击 Rate 组件的星星或点击 `descrease age` ,`increase age` 按钮对年龄值进行改变，可以发现当通过按钮更新数据时，Rate 组件可以正确响应。而且 Bind 是一个关于 value 的纯组件，当其他无关数据发生改变时，Bind 元素不会发生多余的渲染动作。

Bind 元素还有两个可选的属性：
- **valueProp**: 指定被包装的组件接收数据的属性名，默认是 `value`, 绝大多数组件也是用 `value`，因此无需指定该值。如果被包装的组件使用的是 `checked` 或其他属性名接收数据值，那么请把 Bind 元素的 valueProp 设为对应的 `checked` 或其他属性名。   
- **afterChange**: 当组件绑定的值通过该组件发生改变后，会调用该函数，该函数的签名为 `(newValue) => void`。当绑定的属性值不是通过该组件发生改变时，afterChange 函数不会被调用。  

### 使用 makeBindable 函数

你也可以使用 `makeBindable(RawComponent, valueProp = 'value') ` 函数生成一个可以复用的**可绑定组件**：

```javascript
import { ..., makeBindable } from 'pastate';
...

// 使用 makeBindable 生成对应的可以绑定数据的 Rate 组件
const MyRate = makeBindable(Rate)

class BasicInfoView extends PureComponent {
    ...
    render() {
        /** @type {initState['basicInfo']} */
        let state = this.props.state;
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                <div>
                    年龄(makeBindable): 
                    <MyRate count={10} value={state.age}/> 
                </div>
                ...
            </div>
        )
    }
}
```

你可以通过  makeBindable 函数的第二个参数指定 valueProp 值，如 `const MyCheckbox = makeBindable(Checkbox, 'checked')`, 同样，你可以通过新组件的 afterChange 属性去响应组件值更新后的个性化操作。

## 对于绑定的值为空的情况
无论使用 pastate 的表单输入组件还是包装现有的组件库，对于组件绑定的数据值，不支持被设为 null 值 或 undefind 值。通常情况下，我们不会有这种需求。如果需要实现表单的“未选择”状态， 我们一般通过设置一个默认值且不可选的 default 值来代替。例如有个表单需要选择性别，并且需要一个未选择的状态，这样使用一个 RadioGroup 组件来实现：
```javascript
let initState = {
  sex: '' // 用字符串表示性别，并设置为选择状态是的值为空字符串 '' 或 'default'
}
const sexOptions = ['boy', 'girl'] // 只包含目标值
...
render(){
  let state = this.props.state;
  return <RadioGroup options={sexOptions} value={state.sex} />
}
...
```

如果你的需求场景一定要用到 null 或 undefined 值，欢迎在 [issues](https://github.com/BirdLeeSCUT/pastate/issues) 中分享。 Pastate 能实现绑定的值支持空值，但如果这个需求只有在非常特殊的情况下才用到，我们就暂不把它默认实现在 pastate 库中，因为这会增加计算量，你可以自行实现这个特殊组件的数据绑定逻辑。

[下一章](./5.模块化.md)，我们来看看 pastate 应用如何进行模块化。