[情境任務]
解師傅:我們的餐廳生意越來越好了,為了不讓客人排隊,我想客製一個點餐機~
小當家:啥?這是什麼玩意?
解師傅:直接在點餐機上選擇餐點跟輸入客人的資料,我們既不用自己點餐,客人也不用排隊,根本一舉兩得阿!
小當家:解師傅,你真是個天才!
現在我們已經有餐點了,還需要方便客人填寫資料的表單,一起動手做吧!
表單處理
還記得在 DAY 2 時有提到,React 不做資料綁定,所以在資料有變更時,常常會用 onChange
去做資料的更新
由於一個表單可能會有多個欄位,所以這邊使用 object 來當預設值,方便之後擴充
為了不混淆,會將每個欄位拆開來看,以下分別為各種類型 input
、select
、radio
、checkbox
、file
的欄位運用
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 會接收 value
和 onChange
事件,如 input 輸入的值變更,setForm 會將 form.name
變更為新的值,以達成 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 接收 value
和 onChange
事件,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> ); }
|
利用 value
和 onChange
達成雙向綁定,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 渲染列表,並綁定 value
、checked
的值
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) => { const file = e.target.files[0]; const fileReader = new FileReader(); fileReader.addEventListener("load", fileLoad); fileReader.readAsDataURL(file); };
const fileLoad = (e) => { 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) => { const file = e.target.files[0]; const fileReader = new FileReader(); fileReader.addEventListener("load", fileLoad); fileReader.readAsDataURL(file); };
const fileLoad = (e) => { 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 技能
🚀實體工作坊分享
最近時賦學苑開了實體體驗課,即使你對程式碼沒有概念也能上手!Lala 會帶你一起做出一個個人品牌形象網站,帶你快速了解前端的開發流程,快跟我們一起玩轉 Web 吧!
🚀線上課程分享
線上課程可以加速學習的時間,省去了不少看文件的時間XD,以下是我推薦的一些課程
想學習更多關於前後端的線上課程,可以參考看看。
Hahow 有各式各樣類型的課程,而且是無限次數觀看,對學生或上班族而言,不用擔心被時間綁住
如果你是初學者,非常推薦六角學院哦!
剛開始轉職也是上了六角的課,非常的淺顯易懂,最重要的是,隨時還有線上的助教幫你解決問題!
Udemy 裡的課程非常的多,品質普遍不錯,且價格都滿實惠的,CP值很高!
也是很多工程師推薦的線上課程網站。