Lala Code

Lala 的前端大補帖,歡迎一起鑽研前端技術😊

0%

【DAY 15】React 表單處理,更新資料及儲存表單

cover

[情境任務]

解師傅:我們的餐廳生意越來越好了,為了不讓客人排隊,我想客製一個點餐機~

小當家:啥?這是什麼玩意?

解師傅:直接在點餐機上選擇餐點跟輸入客人的資料,我們既不用自己點餐,客人也不用排隊,根本一舉兩得阿!

小當家:解師傅,你真是個天才!

現在我們已經有餐點了,還需要方便客人填寫資料的表單,一起動手做吧!


表單處理

還記得在 DAY 2 時有提到,React 不做資料綁定,所以在資料有變更時,常常會用 onChange 去做資料的更新

由於一個表單可能會有多個欄位,所以這邊使用 object 來當預設值,方便之後擴充
為了不混淆,會將每個欄位拆開來看,以下分別為各種類型 inputselectradiocheckboxfile 的欄位運用





input 文字輸入

input 類型為 text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState } from "react";

export default function App() {
const [form, setForm] = useState({
name: ""
});

const changeName = (e) => {
setForm((state) => ({
...state,
name: e.target.value
}));
};

return (
<form>
<label htmlFor="name">姓名</label>
<input
id="name"
type="text"
name="name"
value={form.name}
onChange={changeName}
/>
</form>
);
}

input 會接收 valueonChange 事件,如 input 輸入的值變更,setForm 會將 form.name 變更為新的值,以達成 input 雙向綁定





input 類型為其他型別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState } from "react";

export default function App() {
const [form, setForm] = useState({
number: ""
});

const changeNumber = (e) => {
setForm((state) => ({
...state,
number: parseInt(e.target.value, 10)
}));
};

return (
<form>
<label htmlFor="num">此次用餐人數</label>
<input
id="num"
type="number"
name="number"
value={form.number}
onChange={changeNumber}
/>
</form>
);
}

「value 傳入的值一定會是字串」,所以如想要的值為其他型別,要記得轉型,上例的 input 為 Number 型態,需再使用 parseInt 將字串轉型為 number





Select 下拉選單

Select 綁定字串陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { useState } from "react";

export default function App() {
const age = [
"18歲以下",
"18歲~29歲",
"30歲~39歲",
"40歲~49歲",
"50歲~59歲",
"60歲以上"
];

const [form, setForm] = useState({
age: age[0]
});

const changeAge = (e) => {
setForm((state) => ({
...state,
age: e.target.value
}));
};

return (
<form>
<label>請選擇您的年齡區間</label>

<select name="age" value={form.age} onChange={changeAge}>
{age.map((item) => (
<option key={item.value} value={item}>{item}</option>
))}
</select>

<h1>您選擇了: {form.age}</h1>
</form>
);
}

只有字串的陣列很單純,預設值設定第 0 筆,並用 map 渲染出列表,select 接收 valueonChange 事件,setForm 會將 form.age 變更為新的值,達成 select 雙向綁定





Select 綁定物件陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { useState } from "react";

export default function App() {
const age = [
{ label: "18歲以下", value: "0" },
{ label: "18歲~29歲", value: "1" },
{ label: "30歲~39歲", value: "2" },
{ label: "40歲~49歲", value: "3" },
{ label: "50歲~59歲", value: "4" },
{ label: "60歲以上", value: "5" }
];

const [form, setForm] = useState({
age: age[0].value
});

const changeAge = (e) => {
setForm((state) => ({
...state,
age: e.target.value
}));
};

return (
<form>
<label>請選擇您的年齡區間</label>

<select name="age" value={form.age} onChange={changeAge}>
{age.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>

<h1>您選擇了: {age.find((item) => item.value === form.age).label}</h1>
</form>
);
}

有時候 select 的文字,跟要傳入的 value 是不一樣的,這時候可以用物件陣列,做法跟綁定字串陣列差不多,只要綁定物件裡的 value 就可以了

特別注意的是,要顯示選擇的項目,因為 form.age 綁定的是 value 值,我們想顯示 label 需要從 age 陣列去找 value 跟 form.age 相同的的物件,再取得物件的 label





radio 單選

radio 綁定物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useState } from "react";

export default function App() {
const [form, setForm] = useState({
gender: "male",
});

const changeGender = (e) => {
setForm((state) => ({
...state,
gender: e.target.value
}));
};

return (
<form>
<label>性別</label>

<div>
<input
type="radio"
id="male"
name="gender"
value="male"
onChange={changeGender}
checked={form.gender === "male"}
/>
<label htmlFor="male">男性</label>
</div>

<div>
<input
type="radio"
id="female"
name="gender"
value="female"
onChange={changeGender}
checked={form.gender === "female"}
/>
<label htmlFor="female">女性</label>
</div>
</form>
);
}

利用 valueonChange 達成雙向綁定,radio 還有 checked 屬性,依據 form.gender 去判斷是否 checked





radio 綁定陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { useState } from "react";

export default function App() {
const gender = [
{ label: "男性", value: "male" },
{ label: "女性", value: "female" }
];

const [form, setForm] = useState({
gender: "male",
});

const changeGender = (e) => {
setForm((state) => ({
...state,
gender: e.target.value
}));
};

return (
<form>
<label>性別</label>

{gender.map((item) => (
<div key={item.value}>
<input
type="radio"
id={item.value}
name="gender"
value={item.value}
onChange={changeGender}
checked={form.gender === item.value}
/>

<label htmlFor={item.value}>
{item.label}
</label>
</div>
))}
</form>
);
}

將項目整理成陣列,用 map 渲染列表,並綁定 valuechecked 的值





checkbox 多選

checkbox 綁定物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { useState } from "react";

export default function App() {
const purpose = [
{ label: "約會聚餐", value: "date" },
{ label: "朋友聚會", value: "friend" },
{ label: "商務用餐", value: "business" },
{ label: "慶祝生日", value: "birthday" },
{ label: "其他", value: "others" }
];

const [form, setForm] = useState({
purpose: {
date: false,
friend: false,
business: false,
birthday: false,
others: false
}
});

const changePurpose = (e) => {
const key = e.target.value;

setForm((state) => ({
...state,
purpose: {
...state.purpose,
[key]: !state.purpose[key]
}
}));
};

return (
<form>
<label>此次用餐目的</label>

{purpose.map((item) => (
<div key={item.value}>
<input
type="checkbox"
name="purpose"
value={item.value}
id={item.value}
checked={form.purpose[item.value]}
onChange={changePurpose}
/>

<label htmlFor={item.value}>
{item.label}
</label>
</div>
))}
</form>
);
}

綁定物件的 boolean 值去控制是否 checked,並在 setForm 做開關的動作





checkbox 綁定陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { useState } from "react";

export default function App() {
const purpose = [
{ label: "約會聚餐", value: "date" },
{ label: "朋友聚會", value: "friend" },
{ label: "商務用餐", value: "business" },
{ label: "慶祝生日", value: "birthday" },
{ label: "其他", value: "others" }
];

const [form, setForm] = useState({
purpose: []
});

const changePurpose = (e) => {
const value = e.target.value;

setForm((state) => {
if (state.purpose.includes(value)) {
return {
...state,
purpose: state.purpose.filter((item) => item !== value)
};
} else {
return {
...state,
purpose: [...state.purpose, value]
};
}
});
};

return (
<form>
<label>此次用餐目的</label>

{purpose.map((item, idx) => (
<div key={item.value}>
<input
type="checkbox"
value={item.value}
name="purpose"
id={item.value}
checked={form.purpose.includes(item.value)}
onChange={changePurpose}
/>

<label htmlFor={item.value}>
{item.label}
</label>
</div>
))}
</form>
);
}

陣列會傳入有 checked 的 value,如點擊已 checked 的項目,則會用 filter 過濾掉此 value





file 檔案上傳與圖片預覽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useState } from "react";

export default function App() {
const [form, setForm] = useState({
file: ""
});

const changeFile = (e) => {
// 取得第0筆檔案
const file = e.target.files[0];
// FileReader 讀取瀏覽器選中的檔案
const fileReader = new FileReader();
// 讀取完改變 img
fileReader.addEventListener("load", fileLoad);
// 將圖片繪出,轉換成 Base64 編碼
fileReader.readAsDataURL(file);
};

const fileLoad = (e) => {
// 此處的 e 為 fileReader
setForm((state) => ({
...state,
file: e.target.result
}));
};

return (
<form>
<label>相關圖片</label>
<div>
<input
type="file"
id="upload"
name="file"
onChange={changeFile}
/>
<img src={form.file} width="100%" alt="" />
</div>
</form>
);
}

type 為 file 時,沒辦法用 value 指定,透過 fileReader 讀取檔案,再轉換給 form.file





統一 function

因為 changeName、changeAge、changeGender 的 function 邏輯都是一樣的,所以可以在 onChange 時統一讀取同一個 function,如下讀取 changeValue,取得欄位的 name 屬性,並賦予新值

1
2
3
4
5
6
7
8
const changeValue = (e) => {
const name = e.target.name;

setForm((state) => ({
...state,
[name]: e.target.value
}));
};

完整 form 表單如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import { useState } from "react";

export default function App() {
const age = [
{ label: "18歲以下", value: "0" },
{ label: "18歲~29歲", value: "1" },
{ label: "30歲~39歲", value: "2" },
{ label: "40歲~49歲", value: "3" },
{ label: "50歲~59歲", value: "4" },
{ label: "60歲以上", value: "5" }
];

const gender = [
{ label: "男性", value: "male" },
{ label: "女性", value: "female" }
];

const purpose = [
{ label: "約會聚餐", value: "date" },
{ label: "朋友聚會", value: "friend" },
{ label: "商務用餐", value: "business" },
{ label: "慶祝生日", value: "birthday" },
{ label: "其他", value: "others" }
];

const [form, setForm] = useState({
name: "",
number: "",
gender: "male",
age: age[0].value,
purpose: [],
file: ""
});

const changeNumber = (e) => {
setForm((state) => ({
...state,
number: parseInt(e.target.value, 10)
}));
};

const changeValue = (e) => {
const name = e.target.name;

setForm((state) => ({
...state,
[name]: e.target.value
}));
};

const changePurpose = (e) => {
const value = e.target.value;

setForm((state) => {
if (state.purpose.includes(value)) {
return {
...state,
purpose: state.purpose.filter((item) => item !== value)
};
} else {
return {
...state,
purpose: [...state.purpose, value]
};
}
});
};

const changeFile = (e) => {
// 取得第0筆檔案
const file = e.target.files[0];
// FileReader 讀取瀏覽器選中的檔案
const fileReader = new FileReader();
// 讀取完改變 img
fileReader.addEventListener("load", fileLoad);
// 將圖片繪出,轉換成 Base64 編碼
fileReader.readAsDataURL(file);
};

const fileLoad = (e) => {
// 此處的 e 為 fileReader
setForm((state) => ({
...state,
file: e.target.result
}));
};

return (
<div>
<h1>React 熱炒店訂購單</h1>
<form>
<div>
<label htmlFor="name">
姓名
</label>
<input
id="name"
type="text"
name="name"
value={form.name}
onChange={changeValue}
/>
</div>

<div>
<label htmlFor="num">
此次用餐人數
</label>
<input
id="num"
type="number"
value={form.number}
onChange={changeNumber}
/>
</div>

<div>
<label>性別</label>
<div>
{gender.map((item) => (
<div key={item.value}>
<input
type="radio"
name="gender"
id={item.value}
value={item.value}
onChange={changeValue}
checked={form.gender === item.value}
/>
<label htmlFor={item.value}>
{item.label}
</label>
</div>
))}
</div>
</div>

<div>
<label>請選擇您的年齡區間</label>

<select
name="age"
value={form.age}
onChange={changeValue}
>
{age.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>

<h6>
您選擇了: {age.find((item) => item.value === form.age).label}
</h6>
</div>

<div>
<label>此次用餐目的</label>
<div>
{purpose.map((item, idx) => (
<div key={item.value}>
<input
type="checkbox"
value={item.value}
id={item.value}
checked={form.purpose.includes(item.value)}
onChange={changePurpose}
/>
<label htmlFor={item.value}>
{item.label}
</label>
</div>
))}
</div>
</div>

<div>
<label>相關圖片</label>
<div>
<input
type="file"
id="upload"
onChange={changeFile}
/>
<button
type="button"
id="upload"
>
上傳
</button>

<img src={form.file} width="100%" />
</div>
</div>
</form>
</div>
);
}

[任務解題]

依照上面的範例,加上了 className,已完成訂購單囉!你真是幫了餐廳一個大忙!



結語

表單的處理在 React 也是一門學問,React 不像其他框架有做雙向綁定的模版,所以利用 onChange 可以幫助我們綁定新的值,就達到雙向綁定的效果囉!


本文為 IT 鐵人賽系列文 你 React 了嗎? 30 天解鎖 React 技能




Hey!想學習更多前端知識嗎?

最近 Lala 開了前端課程 👉【實地掌握RWD - 12小時新手實戰班】👈
無論您是 0 基礎新手,又或是想學 RWD 的初學者,
我們將帶你從零開始,深入了解並掌握 RWD 響應式網頁設計的核心技術,快來一起看看吧 😊



🚀線上課程分享

線上課程可以加速學習的時間,省去了不少看文件的時間XD,以下是我推薦的一些課程
想學習更多關於前後端的線上課程,可以參考看看。

Hahow

Hahow 有各式各樣類型的課程,而且是無限次數觀看,對學生或上班族而言,不用擔心被時間綁住



六角學院

如果你是初學者,非常推薦六角學院哦!
剛開始轉職也是上了六角的課,非常的淺顯易懂,最重要的是,隨時還有線上的助教幫你解決問題!


Udemy

Udemy 裡的課程非常的多,品質普遍不錯,且價格都滿實惠的,CP值很高!
也是很多工程師推薦的線上課程網站。
❤️