[情境任務]
解師傅:我們的餐廳生意越來越好了,為了不讓客人排隊,我想客製一個點餐機~
小當家:啥?這是什麼玩意?
解師傅:直接在點餐機上選擇餐點跟輸入客人的資料,我們既不用自己點餐,客人也不用排隊,根本一舉兩得阿!
小當家:解師傅,你真是個天才!
現在我們已經有餐點了,還需要方便客人填寫資料的表單,一起動手做吧!
還記得在 DAY 2 時有提到,React 不做資料綁定,所以在資料有變更時,常常會用 onChange
去做資料的更新
由於一個表單可能會有多個欄位,所以這邊使用 object 來當預設值,方便之後擴充
為了不混淆,會將每個欄位拆開來看,以下分別為各種類型 input
、select
、radio
、checkbox
、file
的欄位運用
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 雙向綁定
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
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 雙向綁定
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
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
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
的值
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 做開關的動作
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
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
因為 changeName、changeAge、changeGender 的 function 邏輯都是一樣的,所以可以在 onChange
時統一讀取同一個 function,如下讀取 changeValue,取得欄位的 name
屬性,並賦予新值
const changeValue = (e) => {
const name = e.target.name;
setForm((state) => ({
...state,
[name]: e.target.value
}));
};
完整 form 表單如下
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>
);
}
打開 codesandbox 程式碼範例 一起試看看吧!
[任務解題]
依照上面的範例,加上了 className,已完成訂購單囉!你真是幫了餐廳一個大忙!
表單的處理在 React 也是一門學問,React 不像其他框架有做雙向綁定的模版,所以利用 onChange 可以幫助我們綁定新的值,就達到雙向綁定的效果囉!
本文將同步更新至我的部落格
Lala 的前端大補帖