owned this note
owned this note
Published
Linked with GitHub
# React 的 form 表單實作
###### tags: `javascript`、`react`
### 拿到表單中的值並呈現
第一種方式是叫做 Uncontrolled Components:
```jsx=
import React, { Component } from 'react'
class App extends Component {
constructor(props) {
super(props)
this.input = React.createRef() // 加這行 input 的 method,讓 call 它的標籤都會創造一個 reference
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(e) {
alert(this.input.current.value) // 從有 ref 的地方中拿到它們的值
e.preventDefault()
}
render() {
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<div>
// 為這個 input 建立一個 ref
姓名: <input name="name" type="text" ref={this.input} />
</div>
<div>
地址: <input name="address" type="text" />
</div>
<div>
心得: <textarea name="review" />
</div>
<div>
性別:<input id="gender_male" type="radio" value="male" name="gender"></input>
<label for="gender_male">男</label>
<input id="gender_female" type="radio" value="female" name="gender"></input>
<label for="gender_female">女</label>
<input id="gender_other" type="radio" value="other" name="gender"></input>
<label for="gender_other">其他</label>
</div>
<div>
有空的時間:<input id='time_mon' type="checkbox" value="1" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_mon'>星期一</label>
<input id='time_tue' type="checkbox" value="2" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_tue'>星期二</label>
<input id='time_wed' type="checkbox" value="3" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_wed'>星期三</label>
</div>
<div>
<input type="submit" />
</div>
</form>
</div>
)
}
}
export default App
```
因為第一種方式變成說 state 當中沒有值和 UI 對應,也就是說使用者打了什麼會不知道,不太符合 React 的原則。
第二種方式則是 Controlled Components,簡單說就是使用 state 的機制來儲存表單的值,程式碼如下:
```jsx=
import React, { Component } from 'react'
class App extends Component {
constructor(props) {
super(props)
// state 分別加上每個 input 相對應的名稱
this.state = {
name: 123,
address: '',
review: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
}
handleSubmit(e) {
console.log(this.state)
e.preventDefault()
}
// 每當表單有動作就負責改變 state 的值
handleInputChange(e) {
this.setState({
/*
這邊使用 [] 括起來代表說它的值是可變的
根據你的 e.target.name 選到哪個表單 name 而成為什麼
這樣寫的好處就是所有表單只要一個 onChange 事件監聽的 function
就可以改變每個表單的 state 的值了
*/
[e.target.name]: e.target.value
})
}
render() {
const { name, address, review } = this.state
return (
<div className="App">
/*
先替前面三個 input 加入 value 屬性和 onChange 事件監聽
value 屬性的值就是與 state 做連接,每當 state 一改變,value 跟著變
onChange 就是每次表單的值一更動就會觸發的事件監聽
然後都是指向 handleInputChange 這個 function
*/
<form onSubmit={this.handleSubmit}>
<div>
姓名: <input name="name" type="text" value={name} onChange={this.handleInputChange} />
</div>
<div>
地址: <input name="address" type="text" value={address} onChange={this.handleInputChange} />
</div>
<div>
心得: <textarea name="review" value={review} onChange={this.handleInputChange} />
</div>
<div>
性別:<input id="gender_male" type="radio" value="male" name="gender"></input>
<label htmlFor="gender_male">男</label>
<input id="gender_female" type="radio" value="female" name="gender"></input>
<label htmlFor="gender_female">女</label>
<input id="gender_other" type="radio" value="other" name="gender"></input>
<label htmlFor="gender_other">其他</label>
</div>
<div>
有空的時間:<input id='time_mon' type="checkbox" value="1" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_mon'>星期一</label>
<input id='time_tue' type="checkbox" value="2" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_tue'>星期二</label>
<input id='time_wed' type="checkbox" value="3" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_wed'>星期三</label>
</div>
<div>
<input type="submit" />
</div>
</form>
</div>
)
}
}
export default App
```
### 繼續幫其他表單元件加上事件監聽與拿取值
加上 state 之前,表單再新增一個 `<select>` 標籤選擇城市,然後放在表單最下面:
```jsx=
<div>
城市:<select name="city">
<option value="taipei">台北市</option>
<option value="new_taipei">新北市</option>
<option value="other">其他</option>
</select>
</div>
```
接著替 `type=radio` 的 input 加上:
```jsx=
<div>
性別:<input id="gender_male" type="radio" value="male" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_male">男</label>
<input id="gender_female" type="radio" value="female" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_female">女</label>
<input id="gender_other" type="radio" value="other" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_other">其他</label>
</div>
```
可以沿用 `handleInputChange` 這個 function,因為提取表單的值的語法是一樣的。記得在 `state` 加上 `gender: ''` 初始值。
不過如果今天 `gender` 這個 state 有初始值的話,在 `type=radio` 的表單中相對應的「圈圈」不會是被點到的狀態,像這樣都沒有被圈:
![](https://i.imgur.com/6AP9xQc.png)
因此我們還得為這個表單加上 `checked` 的屬性:
```jsx=
<div>
性別:<input id="gender_male" checked={gender === 'male'} type="radio" value="male" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_male">男</label>
<input id="gender_female" checked={gender === 'female'} type="radio" value="female" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_female">女</label>
<input id="gender_other" checked={gender === 'other'} type="radio" value="other" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_other">其他</label>
</div>
```
接著替 `select` 加上:
```jsx=
<div>
城市:<select name="city" value={city} onChange={this.handleInputChange}>
<option value="taipei">台北市</option>
<option value="new_taipei">新北市</option>
<option value="other">其他</option>
</select>
</div>
```
跟上面都是一樣的做法,也是用 `handleInputChange` 這個 function 來變更 state。也要記得在 `state` 加上 `city: ''` 初始值,不過有個小瑕疵是一開始的 `select` 是在 `台北市` 的位置,但是 `state` 上依然是初始值:`''`,我們可以直接把在 `select` 選項當中放在第一個的 `台北市` 的值,也就是 `taipei` 直接設定成 `city` 的初始值,這樣就可以解決了。
接著替 `type=checkbox` 加上:
```jsx=
handleCheckboxChange(e) {
const { time } = this.state
const value = e.target.value
const newTime = time.filter(item => item !== value)
this.setState({
time: time.length !== newTime.length ? newTime : [...time, value]
})
}
```
先在 `state` 裡面新增 `time: []`,因為「有空的時間」這個表單可以是複數值,因此我們使用陣列來儲存複數值。而上面那段的 `newTime` 就是先過濾等於 `value` 的值,剩下的值成為新的 `time` state,而這個 `value` 值要嘛就是被打勾的,要嘛就是被取消打勾的,因此我們接著看到 `setState` 裡面那段判別式,意思就是說原先的 `time` 的陣列長度和被過濾後的 `newTime`的陣列長度如果不一樣,那就把 `time` 更新成被過濾後的 `newTime`,而前後兩個陣列的長度不一樣只有在一個情況會發生,那就是有值被過濾的時候(就是有值被取消打勾了)。
如果都沒被過濾就表示 `newTime` 裡面的值總數就如同原先的 `time` state,因為根本沒動過,所以前後兩個陣列長度會是一樣的,判別式中,如果「陣列長度不一樣」不成立,換句話說陣列長度就是一樣的,而沒被過濾就表示原先的 `time` 陣列並不存在 `value` 值,也表示 `value` 需要被新增到 `time` 當中(就是被打勾的),所以使用 `[...time, value]` 的方式在原先 `time` 陣列當中新增了 `value` 值。
不過我們還要再加上當 `state` 中的 `time` 有初始值的時候,`checkbox` 相對應的「框框」需要被打勾,這時就要設定 `checked` 的屬性,像這樣:
```jsx=
<div>
有空的時間:<input id='time_mon' checked={time.indexOf('1') >= 0} type="checkbox" value="1" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_mon'>星期一</label>
<input id='time_tue' checked={time.indexOf('2') >= 0} type="checkbox" value="2" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_tue'>星期二</label>
<input id='time_wed' checked={time.indexOf('3') >= 0} type="checkbox" value="3" name="time" onChange={this.handleCheckboxChange} />
<label htmlFor='time_wed'>星期三</label>
</div>
```
為每個 `type=checkbox` 的表單加上 `checked={time.indexOf('1') >= 0}`,其中的 `indexOf` 就是要尋找在參數面的那個值到底在 `time` 陣列的哪個位置,如果不在陣列的任何位置的話,就會回傳 `-1`,因此才有那個 `>=0` 的判別式。
### 做優化
來幫上面 `type=checkbox` 的表單做優化,因為太長了,所以我們可以把它獨立出去成一個 Component,如下:
```jsx=
const Checkbox = ({ id, value, htmlFor, label, checked, onChange }) => (
<span>
<input
id={id}
checked={checked}
type="checkbox"
value={value}
name="time"
onChange={onChange} />
<label htmlFor={htmlFor}>{label}</label>
</span>
)
```
要用一個元素包起來全部的內容,這邊是使用 `<span>`。然後也使用了 functional component,而命名函式有省略 `{}` 和 `return`,直接用 `()` 包起來全部要 return 的內容。
另外原先在 Parent component 位置的元素就會變成這樣:
```jsx=
<div>
有空的時間:
<Checkbox id='time_mon' value='1' htmlFor='time_mon' label='星期一' checked={time.indexOf('1') >= 0} onChange={this.handleCheckboxChange} />
<Checkbox id='time_tue' value='2' htmlFor='time_tue' label='星期二' checked={time.indexOf('2') >= 0} onChange={this.handleCheckboxChange} />
<Checkbox id='time_wen' value='3' htmlFor='time_wen' label='星期三' checked={time.indexOf('3') >= 0} onChange={this.handleCheckboxChange} />
</div>
```
而可以依樣畫葫蘆,其他的表單型式也可以獨立出去成為另一個 Children component。
### 所有內容
把前面所改過的內容的程式碼與初始 UI 根據 `state` 的值所呈現的畫面附上:
```jsx=
import React, { Component } from 'react'
const Checkbox = ({ id, value, htmlFor, label, checked, onChange }) => (
<span>
<input
id={id}
checked={checked}
type="checkbox"
value={value}
name="time"
onChange={onChange} />
<label htmlFor={htmlFor}>{label}</label>
</span>
)
class App extends Component {
constructor(props) {
super(props)
this.state = {
name: 123,
address: '444',
review: '5123',
gender: 'male',
city: 'new_taipei',
time: ['1']
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
this.handleCheckboxChange = this.handleCheckboxChange.bind(this)
}
handleSubmit(e) {
console.log(this.state)
e.preventDefault()
}
handleInputChange(e) {
this.setState({
[e.target.name]: e.target.value
})
}
handleCheckboxChange(e) {
const { time } = this.state
const value = e.target.value
const newTime = time.filter(item => item !== value)
this.setState({
time: time.length !== newTime.length ? newTime : [...time, value]
})
}
render() {
const { name, address, review, gender, city, time } = this.state
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<div>
姓名: <input name="name" type="text" value={name} onChange={this.handleInputChange} />
</div>
<div>
地址: <input name="address" type="text" value={address} onChange={this.handleInputChange} />
</div>
<div>
心得: <textarea name="review" value={review} onChange={this.handleInputChange} />
</div>
<div>
性別:<input id="gender_male" checked={gender === 'male'} type="radio" value="male" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_male">男</label>
<input id="gender_female" checked={gender === 'female'} type="radio" value="female" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_female">女</label>
<input id="gender_other" checked={gender === 'other'} type="radio" value="other" name="gender" onChange={this.handleInputChange}></input>
<label htmlFor="gender_other">其他</label>
</div>
<div>
有空的時間:
<Checkbox id='time_mon' value='1' htmlFor='time_mon' label='星期一' checked={time.indexOf('1') >= 0} onChange={this.handleCheckboxChange} />
<Checkbox id='time_tue' value='2' htmlFor='time_tue' label='星期二' checked={time.indexOf('2') >= 0} onChange={this.handleCheckboxChange} />
<Checkbox id='time_wen' value='3' htmlFor='time_wen' label='星期三' checked={time.indexOf('3') >= 0} onChange={this.handleCheckboxChange} />
</div>
<div>
城市:<select name="city" value={city} onChange={this.handleInputChange}>
<option value="taipei">台北市</option>
<option value="new_taipei">新北市</option>
<option value="other">其他</option>
</select>
</div>
<div>
<input type="submit" />
</div>
</form>
</div>
)
}
}
export default App
```
![](https://i.imgur.com/qz6dmOX.png)
表單初始內容就是 `state` 設定的初始值,而有了 `state` 的值,我們操作其它功能就比較方便,比如表單驗證。