Mock CRUD and Database Prep
Day 3 - ชั่วโมงที่ 4: เปลี่ยนสถานะในข้อมูลชั่วคราว
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงนี้ นักศึกษาจะสามารถ:
- อธิบาย Create, Read และ Update ในโปรเจกต์ปัจจุบันได้
- เปลี่ยน Status ของ Issue ใน State ได้
- เปลี่ยนข้อมูลใน Array และ Object โดยสร้างชุดใหม่แทนการแก้ชุดเดิม
- ส่งฟังก์ชันจาก Component ที่ครอบอยู่ไปให้ Component ลูกเรียกใช้
- ทำให้
IssueListใช้ได้ทั้งแบบอ่านอย่างเดียวและแบบแก้สถานะได้ - อธิบายข้อจำกัดของข้อมูลชั่วคราว ก่อนเชื่อมฐานข้อมูลได้
ไฟล์ที่แก้
components/IssueBoard.tsx
components/IssueList.tsxSlide 1: เพิ่มการเปลี่ยนสถานะต่อจาก Hour 3
หน้า Home สร้าง Issue ใหม่ได้แล้ว โดย IssueBoard เก็บ items ใน State หรือข้อมูลที่ Component จำไว้ แล้วส่งให้ IssueList
ชั่วโมงนี้จะให้ List ส่ง id และสถานะที่เลือกกลับไปให้ Board เปลี่ยน State
- Create (เพิ่ม): เพิ่มรายการจากฟอร์มได้แล้ว
- Read (อ่าน): แสดงรายการจาก State ได้แล้ว
- Update (แก้ไข): กำลังจะเพิ่มการเปลี่ยนสถานะ
- Delete (ลบ): ไม่เพิ่มการลบใน Project นี้ ใช้
DONEเพื่อระบุว่างานเสร็จแล้ว
ทั้งสี่คำเรียกรวมกันว่า CRUD ส่วนข้อมูลยังอยู่ชั่วคราวใน Browser
Slide 2: เปลี่ยน State ด้วยข้อมูลชุดใหม่
ตัวอย่างอธิบายที่ไม่ควรใช้ ไม่ต้องนำไปแก้ Project:
issue.status = "DONE";
setItems(items);การส่ง Array เดิมกลับเข้า State อาจทำให้ React ข้ามการคำนวณหน้าจอใหม่ เพราะยังอ้างถึงข้อมูลก้อนเดิม แม้ค่าภายในจะเปลี่ยนแล้ว
วิธีที่เราจะใช้:
.map()สร้าง Array ใหม่จากรายการเดิม- ถ้า
idตรง ให้สร้าง Object ใหม่ด้วย{ ...issue, status } - ถ้า
idไม่ตรง ให้คืน Object เดิม
เรียกว่า Immutable Update คือสร้างข้อมูลชุดใหม่โดยไม่แก้ต้นฉบับ
Slide 3: เพิ่มฟังก์ชันเปลี่ยนสถานะใน Board
import type { Issue, IssueStatus } from "@/types/issue";
เพิ่ม Function นี้ภายใน IssueBoard หลัง handleCreateIssue และก่อน return:
function handleUpdateStatus(id: string, status: IssueStatus) { setItems((currentItems) => currentItems.map((issue) => issue.id === id ? { ...issue, status } : issue ) );}
id ระบุรายการที่จะเปลี่ยน ส่วน status คือค่าใหม่; เก็บ State และ Handler สร้างรายการเดิมไว้
Slide 4: อ่านการอัปเดตทีละขั้น
ตัวอย่างอธิบายจาก Handler ที่เพิ่งเพิ่ม:
issue.id === id ? { ...issue, status } : issueเงื่อนไข ? ค่าถ้าจริง : ค่าถ้าเท็จเลือกว่าจะคืนค่าใด...issueคัดลอกข้อมูลทุกช่องของ Object เดิม เช่นหัวข้อและผู้แจ้งstatusที่เขียนตามหลังแทนเฉพาะสถานะด้วยค่าใหม่
setItems ส่งรายการล่าสุดให้ currentItems และ .map() คืน Array ใหม่ โดยจำนวนรายการเท่าเดิม
เมื่อกดเปลี่ยนสถานะ จึงควรเปลี่ยนเพียงป้ายสถานะของรายการที่เลือก ไม่สร้างแถวเพิ่ม
Slide 5: List เดียวรองรับสองหน้า
IssueList ถูกใช้สองจุด และต้องรองรับทั้งคู่:
| จุดที่เรียกใช้ | พฤติกรรมที่ต้องการ |
|---|---|
Home ผ่าน IssueBoard | แสดงปุ่มและเปลี่ยน State ได้ |
/issues ผ่าน app/issues/page.tsx | แสดงข้อมูลอย่างเดียว |
เราจะเพิ่ม Callback ชื่อ onUpdateStatus ให้ List เรียกเมื่อกดปุ่ม โดยเป็น Optional Prop หรือค่าที่จะส่งมาก็ได้ ไม่ส่งก็ได้ ใช้ ? เหมือนฟอร์มใน Hour 3
ส่ง Callback มา → แสดงคอลัมน์จัดการ; ไม่ส่งมา → ซ่อนทั้งหัวคอลัมน์และช่องปุ่ม
หน้า /issues จึงยังใช้ <IssueList issues={issues} /> เดิมได้
Slide 6: ประกาศ Callback ของ IssueList
"use client";
แก้ Type Import และ Props เดิม โดยคง Import อื่นไว้:
import type { Issue, IssueStatus } from "@/types/issue"; type IssueListProps = { issues: Issue[]; onUpdateStatus?: (id: string, status: IssueStatus) => void;};
List กำลังจะรับการคลิกผ่าน onClick และหน้า /issues ที่ทำงานฝั่ง Server ยังนำไปใช้โดยตรง จึงใส่ "use client" ที่ไฟล์นี้ด้วย
Slide 7: รับ Callback และเตรียมสถานะ
const statuses: IssueStatus[] = ["OPEN", "IN_PROGRESS", "DONE"];
แก้เฉพาะบรรทัดประกาศ Function เดิมเป็น:
export function IssueList({ issues, onUpdateStatus }: IssueListProps) {
เก็บเงื่อนไข Empty State และ return ของตารางภายใน Function ไว้ทั้งหมด
Array statuses จะใช้สร้างปุ่มสามปุ่ม โดยแต่ละปุ่มส่งสถานะของตัวเองให้ Callback
Slide 8: เพิ่มหัวคอลัมน์เมื่อมี Callback
<th className="px-3 py-3 font-semibold">รายละเอียด</th>{onUpdateStatus && ( <th className="px-3 py-3 font-semibold">จัดการ</th>)}
บรรทัด “รายละเอียด” เป็นจุดอ้างอิงเดิม ไม่เพิ่มซ้ำ
onUpdateStatus && (...) แสดงหัวคอลัมน์เมื่อมี Callback เท่านั้น ตอนนี้ยังไม่เห็นคอลัมน์ เพราะ Board ยังไม่ได้ส่ง Callback
Slide 9: เตรียมช่องปุ่มในแต่ละแถว
{onUpdateStatus && ( <td className="px-3 py-4"> <div className="flex flex-wrap gap-2"> {/* เพิ่มปุ่มสถานะตรงนี้ในขั้นถัดไป */} </div> </td>)}
เมื่อมี Callback จึงสร้างช่องจัดการให้ตรงกับหัวคอลัมน์; เก็บช่องรายละเอียดและลิงก์เดิมไว้
Comment เป็นจุดวางปุ่มในขั้นถัดไป ส่วน flex-wrap จะช่วยให้ปุ่มขึ้นบรรทัดใหม่ได้
Slide 10: สร้างปุ่มจากรายการสถานะ
{statuses.map((status) => ( <button key={status} type="button" disabled={issue.status === status} onClick={() => onUpdateStatus(issue.id, status)} > {status} </button>))}
ตัวอย่างเริ่มที่ .map() เมื่อวางจริงให้เยื้องเข้าไปภายใน <div> ของช่องปุ่ม
onClick เรียก Callback ตอนคลิก; disabled ปิดปุ่มสถานะปัจจุบัน และ type="button" ระบุว่าไม่ใช่ปุ่มส่ง Form
Slide 11: ตกแต่งปุ่มสถานะ
<button key={status} type="button" disabled={issue.status === status} onClick={() => onUpdateStatus(issue.id, status)} className="rounded-md border border-slate-300 px-2 py-1 text-xs font-semibold text-slate-700 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-700 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400">
แสดงเฉพาะ Tag เปิด ให้เก็บ {status}, </button> และโครง .map() เดิมไว้
disabled:* ทำให้ปุ่มที่กดไม่ได้ดูจางลง ส่วน focus-visible:* ช่วยเห็นตำแหน่งเมื่อใช้แป้นพิมพ์
Slide 12: เพิ่มพื้นที่ให้คอลัมน์จัดการ
<table className="w-full min-w-[920px] border-collapse text-sm">
เปลี่ยนจาก min-w-[720px] เป็น min-w-[920px] โดยเก็บเนื้อหาและ Tag ปิดเดิม
กล่องที่ครอบตารางต้องยังมี overflow-x-auto เพื่อให้เลื่อนเฉพาะตารางบนจอเล็ก
ค่าขั้นต่ำนี้ใช้ทั้ง Home และ /issues เพราะใช้ตารางเดียวกัน แม้ /issues ไม่มีคอลัมน์จัดการ
Slide 13: ต่อ Callback จาก Board
<IssueList issues={items} onUpdateStatus={handleUpdateStatus}/>
เก็บ <IssueForm onCreateIssue={handleCreateIssue} /> เดิมไว้
หน้า Home ควรเห็นคอลัมน์จัดการแล้ว: คลิกปุ่ม → List ส่ง id และ status → Board เปลี่ยน items → List แสดงป้ายสถานะใหม่
app/issues/page.tsx ไม่ต้องแก้ เพราะยังแสดงข้อมูลอย่างเดียว
Slide 14: ลองเปลี่ยนสถานะที่ Home
- เลือกรายการที่เป็น
OPENแล้วกดIN_PROGRESS: ป้ายสถานะต้องเป็นสีเหลือง - กด
DONE: ป้ายสถานะต้องเป็นสีเขียว และปุ่มDONEกดไม่ได้ - ตรวจว่าจำนวนแถวและข้อมูลของรายการอื่นไม่เปลี่ยน
- เพิ่มรายการใหม่จาก Form แล้วลองเปลี่ยนสถานะของรายการใหม่นั้น
- กด Tab ไปยังปุ่มสถานะ ต้องเห็นเส้นรอบปุ่มที่ถูกเลือก และใช้ Enter กดได้
ตัวอย่างนี้เลือกกลับไปสถานะอื่นได้ ยังไม่ได้จำกัดลำดับการเปลี่ยนสถานะ
Slide 15: ตรวจหน้าอ่านอย่างเดียวและจอเล็ก
/issuesต้องมีห้าคอลัมน์เดิม ไม่มีหัวข้อ “จัดการ” และไม่มีปุ่มสถานะ- Link รายละเอียดของ Mock Data ต้องยังเปิดได้
- Empty State เดิมยังอยู่ก่อน
returnหลักและไม่ขึ้นกับ Callback - ที่ 375px และ 1024px ตารางที่กว้างเกินพื้นที่ต้องเลื่อนภายในกล่อง ไม่ดันทั้งหน้า
- Console และ Terminal ต้องไม่มี Error
สถานะที่เปลี่ยนใน Home ยังไม่แสดงใน /issues เพราะหน้านี้อ่าน Mock Data เดิม
Slide 16: จาก State ชั่วคราวไปสู่ Database
ตอนนี้ Home เพิ่มรายการและเปลี่ยนสถานะได้ แต่ข้อมูลยังอยู่ใน State ของ IssueBoard
| ตอนนี้ | หลังเชื่อม Database ใน Day 4 |
|---|---|
| Refresh แล้วการเพิ่มและแก้หาย | บันทึกแล้วอ่านกลับได้ |
| หน้าอื่นอ่านข้อมูลตัวอย่างเดิม | ทุกหน้าอ่านแหล่งข้อมูลเดียวกัน |
| เปลี่ยนเฉพาะใน Browser นี้ | Browser อื่นอ่านค่าที่บันทึกแล้วได้ |
ขั้นต่อไปคือสร้างโปรเจกต์บน Supabase และตารางในฐานข้อมูล ก่อนย้ายการอ่านและบันทึกไปฝั่ง Server
Server ยังต้องตรวจข้อมูลและตรวจว่าใครมีสิทธิ์แก้ไข การซ่อนปุ่มในหน้าเว็บอย่างเดียวไม่ได้ป้องกันการแก้ข้อมูล