Section 3

นักศึกษากำลังสร้างหน้าเว็บ Angular ที่แสดงรายการหนังสือด้วย component และ signal
SECTION 3 · หน้าเว็บแรก

Angular พื้นฐาน — สร้างหน้าเว็บชิ้นแรก


Part 1 — รู้จัก Angular

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • 🟢 Angular (frontend) ← กำลังทำส่วนนี้
  • ✅ NestJS (backend) (ทำไปแล้ว)
  • ✅ PostgreSQL (ฐานข้อมูล) (ทำไปแล้ว)

ต่อไป: ใช้ signal แสดงรายการหนังสือแบบไดนามิก

1.1 ติดตั้ง Angular CLI

powershell
npm install -g @angular/cli@22

💡 ทำไมต้องติดตั้งอีกตัว เครื่องมือสร้างโปรเจกต์ NestJS ที่ใช้ใน Section 1 (@nestjs/cli) กับเครื่องมือสร้างโปรเจกต์ Angular เป็นคนละตัวกัน ต้องมีทั้งคู่

✅ ตรวจสอบ

powershell
ng version

ต้องเห็นเลขเวอร์ชัน Angular CLI: 22.x.x ขึ้นมา

1.2 สร้างโปรเจกต์ Angular

powershell
cd C:\webdev\booklab\apps
ng new web --directory web --style tailwind --routing true --ssr false --skip-git --skip-install --defaults

💡 ความหมายของแต่ละตัวเลือก

  • --directory web = สร้างในโฟลเดอร์ชื่อ web (ให้เข้าชุดกับ apps/api ที่มีอยู่แล้ว)
  • --style tailwind = ใช้ Tailwind CSS ตั้งแต่ตอนสร้างโปรเจกต์เลย Angular จะติดตั้งและตั้งค่าไฟล์ที่จำเป็นให้อัตโนมัติทั้งหมด ไม่ต้องมาตั้งเองทีหลัง
  • --routing true = เตรียมระบบเปลี่ยนหน้า (Router) ไว้ให้ตั้งแต่ต้น จะได้ใช้ใน Section 5
  • --ssr false = ปิดโหมด Server-Side Rendering (วิชานี้ไม่ใช้)
  • --skip-git = ไม่ต้องสร้าง Git repo
  • --skip-install = ยังไม่ต้องติดตั้ง package ตอนนี้ จะติดตั้งรวมจาก root เช่นเดียวกับตอนสร้าง NestJS ใน Section 1
  • --defaults = ตอบคำถามอื่น ๆ ทั้งหมดด้วยค่าเริ่มต้น ไม่ต้องตอบทีละข้อ

✅ ต้องเห็น โฟลเดอร์ apps/web เกิดขึ้น พร้อมไฟล์ apps/web/.postcssrc.json และ apps/web/src/styles.css ที่มีบรรทัด @import 'tailwindcss'; อยู่แล้ว (Angular สร้างให้เองตาม --style tailwind ที่ระบุไว้)

1.3 ติดตั้งทุกอย่างจาก root

powershell
cd C:\webdev\booklab
npm install

ใช้เวลาสักครู่ เพราะ Angular มีเครื่องมือค่อนข้างมาก (รวมถึง Tailwind ที่ติดมาด้วยในคำสั่งก่อนหน้า)

💡 สังเกตว่าตอนนี้โปรเจกต์เราใช้ TypeScript สองเวอร์ชันพร้อมกัน เพราะ Angular 22 ต้องการ TypeScript รุ่นใหม่กว่า NestJS 11 ที่ทำไปใน Section 1 ต้องการ npm ฉลาดพอที่จะจัดการให้แต่ละฝั่งได้เวอร์ชันที่ถูกต้องอัตโนมัติ ไม่ต้องทำอะไรเพิ่ม แค่รู้ไว้เฉย ๆ ว่าทำไมบางทีถึงเห็นโฟลเดอร์ typescript ซ้อนอยู่ทั้งที่ root และใน apps/web/node_modules

✅ ต้องเห็น บรรทัดท้ายบอกจำนวน package ที่ติดตั้งเพิ่ม

1.4 กัน VS Code ขีดเส้นแดงหลอก

โปรเจกต์เรามี TypeScript สองเวอร์ชัน (อธิบายไว้ในขั้นที่ 1.3) บางครั้ง VS Code อาจเลือกใช้เวอร์ชันผิดกับไฟล์ Angular แล้วขีดเส้นแดงทั้งที่โค้ดไม่ได้ผิด ป้องกันไว้ก่อนเลยด้วยการสร้างโฟลเดอร์ .vscode ที่ root ของโปรเจกต์ (C:\webdev\booklab\.vscode) แล้วสร้างไฟล์ settings.json ข้างใน

json
{
  "typescript.tsdk": "apps/web/node_modules/typescript/lib"
}

บันทึกแล้วกด Ctrl + Shift + P พิมพ์ TypeScript: Select TypeScript Version เลือก Use Workspace Version

💡 ไฟล์นี้บอก VS Code ว่า "ให้ใช้ TypeScript เวอร์ชันที่อยู่ใน apps/web เป็นตัวหลักในการตรวจโค้ด" ซึ่งตรงกับเวอร์ชันที่ Angular ต้องการพอดี

1.5 รัน frontend ครั้งแรก

เปิด Terminal ใหม่ (ปล่อยตัวที่รัน API ทิ้งไว้)

powershell
cd C:\webdev\booklab
npm run dev:web

รอสักครู่แล้วเปิด http://localhost:4200

✅ ต้องเห็นหน้าเว็บต้อนรับของ Angular (โลโก้และลิงก์ต่าง ๆ ที่ Angular ใส่มาให้เป็นค่าเริ่มต้น) — Section นี้เราจะลบออกแล้วเขียนของตัวเอง

💡 ตอนนี้เรามีโปรแกรมสองตัวรันพร้อมกัน

  • port 3000 = backend NestJS
  • port 4200 = frontend Angular

เป็นคนละโปรแกรม คนละ Terminal หากปิดตัวใด ตัวนั้นจะหยุดทำงานทันที

แผนภาพการทำงานในเครื่อง: Browser บน port 4200 ใช้ Angular component และ signal ส่งคำขอผ่าน proxy ไปยัง NestJS API บน port 3000 ซึ่งอ่านข้อมูลจาก PostgreSQL แล้วส่ง JSON response กลับ

⚠️ ถ้าจะรันทั้งสองด้วยคำสั่งเดียว ใช้ npm run dev แต่ตอนเรียนแนะนำให้แยก Terminal จะดูข้อความ error ง่ายกว่า

1.6 สำรวจไฟล์

text
apps/web/src/app/
├── app.ts          ← โค้ดของหน้าหลัก
├── app.html        ← หน้าตาของหน้าหลัก (ตอนนี้ยังเป็นหน้าต้อนรับของ Angular)
├── app.css         ← สไตล์เฉพาะของหน้าหลัก
├── app.config.ts   ← ตั้งค่ารวมของทั้งแอป
└── app.routes.ts   ← ตาราง route ของ URL

เปิด app.ts ดู

typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}

💡 Component คืออะไร — แนวคิดหลักของ Angular คือตัวต่อเลโก้ ชิ้นหนึ่ง ที่รวม 3 อย่างไว้ด้วยกัน

  • .html = หน้าตา
  • .css = สไตล์
  • .ts = สมองที่ควบคุมพฤติกรรม

การ์ดหนังสือหนึ่งใบคือหนึ่งชิ้น ทำแม่พิมพ์ไว้ครั้งเดียว แล้วปั๊มออกมา 20 ใบใส่ข้อมูลคนละเล่มได้ หน้าเว็บทั้งหน้าคือเลโก้หลายชิ้นประกอบกัน

💡 @Component({...}) = ป้ายที่บอกว่าคลาสนี้เป็น component พร้อมบอกว่าไฟล์หน้าตาอยู่ไหน 💡 selector: 'app-root' = ชื่อแท็กที่เอาไปวางใน HTML ได้ เหมือน <div> แต่เป็นของเราเอง

1.7 Tailwind CSS เบื้องต้น

ตอนสร้างโปรเจกต์ใน 1.2 เราใส่ --style tailwind ไว้ Angular เลยติดตั้ง Tailwind CSS มาให้เรียบร้อยแล้ว หัวข้อถัดไปกำลังจะได้ใช้จริง ก่อนจะพิมพ์ตามควรรู้ก่อนว่าที่พิมพ์ไปคืออะไร

เขียน CSS แบบเดิม เทียบกับ Tailwind

แบบเดิม ต้องคิดชื่อคลาสเอง แล้วไปเขียนกฎในไฟล์ CSS แยกอีกที

html
<h1 class="page-title">Booklab</h1>

<!-- แล้วต้องไปเขียนอีกไฟล์หนึ่ง -->
.page-title {
  font-size: 1.875rem;
  font-weight: 700;
  color: #2563eb;
  padding: 2rem;
}

แบบ Tailwind ใช้คลาสสำเร็จรูปที่มีอยู่แล้ว ใส่ในแท็กได้เลย ไม่ต้องเปิดไฟล์ CSS

html
<h1 class="text-3xl font-bold text-blue-600 p-8">Booklab</h1>

💡 หนึ่งคลาส = หนึ่งหน้าที่ แนวคิดนี้เรียกว่า utility-first แทนที่จะสร้างคลาสใหญ่หนึ่งตัวที่ทำหลายอย่าง Tailwind ให้คลาสเล็ก ๆ ที่ทำอย่างเดียวมาต่อกัน อยากได้หน้าตาแบบไหนก็หยิบมาประกอบ

ถอดรหัสทีละตัว

บรรทัดข้างบนคือบรรทัดเดียวกับที่จะพิมพ์ในหัวข้อถัดไป แกะดูทีละคลาส

คลาส แปลว่า
text-3xl ขนาดตัวอักษรใหญ่ระดับ 3xl
font-bold ตัวหนา
text-blue-600 สีตัวอักษรเป็นน้ำเงินระดับ 600
p-8 ระยะห่างด้านในรอบ ๆ ขนาด 8 (= 32px)

💡 หลักการอ่านชื่อคลาส เกือบทุกตัวอ่านได้เป็น {สิ่งที่จะปรับ}-{ขนาด} เช่น p คือ padding m คือ margin text คือตัวอักษร bg คือสีพื้นหลัง พอจับหลักได้จะเดาคลาสใหม่ ๆ ได้เองโดยไม่ต้องเปิดคู่มือ

ตารางคลาสที่จะเจอบ่อยใน Section 3 ถึง 6

กลุ่ม คลาสที่จะเจอจริง
ระยะห่างด้านใน (padding) p-4 p-6 p-8 px-3 py-2
ระยะห่างด้านนอก (margin) mt-2 mb-4 mx-auto
ตัวอักษร text-xs text-sm text-lg text-3xl font-bold
สี text-gray-600 text-white bg-blue-600 bg-gray-100
กรอบและมุม border rounded rounded-lg shadow-md
การจัดวาง flex grid gap-3 items-center w-full max-w-3xl

ไม่ต้องท่องจำ เปิดกลับมาดูตารางนี้ได้ตลอด เมื่อใช้บ่อยขึ้นจะจำได้เอง

💡 ระบบตัวเลขของขนาด เลข 4 เท่ากับ 1rem หรือ 16px ดังนั้น p-2 = 8px p-4 = 16px p-8 = 32px เลขยิ่งมากยิ่งห่าง

💡 ระบบตัวเลขของสี ไล่ตั้งแต่ 50 ถึง 900 เลขยิ่งมากยิ่งเข้ม bg-blue-100 คือฟ้าอ่อน ส่วน text-blue-600 คือน้ำเงินเข้มพอที่จะอ่านบนพื้นขาวได้สบายตา

⚠️ พิมพ์ชื่อคลาสผิดจะเงียบสนิท ไม่มี error ขึ้นที่ไหนเลย หน้าเว็บก็ยังรันได้ปกติ เพียงแต่สไตล์จะไม่แสดงผล หากใส่คลาสแล้วหน้าตาไม่เปลี่ยน ให้สงสัยเรื่องสะกดผิดก่อนเสมอ เช่น text-grey-600 ต้องเป็น text-gray-600 (สะกดแบบอเมริกัน)

ลองเดาดู

ถ้าเห็นแท็กนี้ คิดว่าผลลัพธ์จะออกมาหน้าตาแบบไหน

html
<p class="p-2 text-sm text-red-600">ไม่พบข้อมูล</p>

เฉลย: ข้อความ "ไม่พบข้อมูล" ตัวเล็กกว่าปกติ สีแดง และมีระยะห่างด้านในรอบ ๆ 8px — คลาสชุดนี้จะได้ใช้จริงตอนทำสถานะ "ไม่พบข้อมูล" ใน Section 4

1.8 แทนที่หน้าเริ่มต้นด้วยของเราเอง

หน้าที่เห็นตอนนี้เป็นหน้าต้อนรับที่ Angular ใส่มาให้ ลบทิ้งแล้วใส่ของเราเอง แทนที่ทั้งไฟล์ app.html — คลาสในโค้ดข้างล่างคือชุดที่เพิ่งแกะกันไปใน 1.7

html
<h1 class="text-3xl font-bold text-blue-600 p-8">Booklab</h1>
<router-outlet />

กด Ctrl + S

✅ หน้าเว็บต้องเปลี่ยนเองทันทีโดยไม่ต้องกด refresh เหลือแค่คำว่า "Booklab" ตัวใหญ่สีน้ำเงิน

💡 เรียกว่า hot reload — Angular คอยจับตาไฟล์อยู่ พอบันทึกก็อัปเดตหน้าจอให้เลย

💡 <router-outlet /> = ช่องว่างที่บอกว่า "เอาหน้าที่ตรงกับ URL ปัจจุบันมาแสดงตรงนี้" จำเป็นต้องมีไว้เสมอ และจะได้ใช้จริงใน Section 5 ตอนทำหลายหน้า


Part 2 — Signal และการแสดงข้อมูล

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • 🟢 Angular (frontend) ← กำลังทำส่วนนี้
  • ✅ NestJS (backend) (ทำไปแล้ว)
  • ✅ PostgreSQL (ฐานข้อมูล) (ทำไปแล้ว)

ต่อไป: แตกโค้ดออกเป็น component ย่อยที่ใช้ซ้ำได้

2.1 สร้าง component รายการหนังสือ

powershell
cd apps/web
npm exec -- ng generate component books/book-list --skip-tests

✅ ต้องเกิด 3 ไฟล์ ใน src/app/books/book-list/

⚠️ สังเกตว่าชื่อคลาสคือ BookList ไม่มีคำว่า Component ต่อท้าย และชื่อไฟล์ไม่มี .component — เป็นรูปแบบใหม่ของ Angular เวอร์ชันนี้ ถ้าไปเจอบทเรียนเก่าที่เขียนว่า book-list.component.ts ก็คือของเดิม ใช้แทนกันได้

2.2 สร้าง Book Interface

สร้างไฟล์ apps/web/src/app/books/book.ts

typescript
export interface Book {
  id: number;
  title: string;
  author: string;
  year: number;
  category?: { id: number; name: string };
}

💡 เหมือน interface ที่เขียนฝั่ง API เป๊ะ ๆ เพราะข้อมูลชุดเดียวกันวิ่งข้ามมา 💡 เครื่องหมาย ? ที่ category? แปลว่า "มีหรือไม่มีก็ได้"

2.3 ใส่ข้อมูลจำลองและ signal

แทนที่ apps/web/src/app/books/book-list/book-list.ts

typescript
import { Component, signal } from '@angular/core';
import { Book } from '../book';

@Component({
  selector: 'app-book-list',
  imports: [],
  templateUrl: './book-list.html',
  styleUrl: './book-list.css',
})
export class BookList {
  books = signal<Book[]>([
    { id: 1, title: 'Clean Code', author: 'Robert C. Martin', year: 2008 },
    { id: 2, title: 'Refactoring', author: 'Martin Fowler', year: 1999 },
    { id: 3, title: "Don't Make Me Think", author: 'Steve Krug', year: 2000 },
  ]);
}

💡 signal คืออะไร — และทำไมสำคัญ คือกล่องที่ส่งเสียงได้ เมื่อค่าข้างในเปลี่ยน signal จะแจ้งไปยังหน้าจอเอง แล้วหน้าจออัปเดตตามทันที โดยที่เราไม่ต้องสั่งให้วาดใหม่เอง

  • signal<Book[]>([...]) = สร้างกล่อง ใส่รายการหนังสือไว้
  • books() มีวงเล็บ = เปิดกล่องดูค่า
  • books.set(ของใหม่) = เปลี่ยนค่าในกล่อง

⚠️ ต้องมีวงเล็บทุกครั้งที่อ่านค่า ลืมใส่แล้วจะได้ตัวกล่องแทนที่จะได้ของข้างใน เป็นจุดที่ผิดพลาดบ่อยที่สุด

2.4 แสดงผลบนหน้าจอ

แทนที่ book-list.html

html
<div class="p-6">
  <h1 class="text-2xl font-bold mb-4">รายการหนังสือ</h1>

  <ul class="grid gap-3">
    @for (b of books(); track b.id) {
      <li class="border rounded-lg p-4">
        <p class="font-bold">{{ b.title }}</p>
        <p class="text-sm text-gray-600">{{ b.author }} · {{ b.year }}</p>
      </li>
    }
  </ul>
</div>

💡 @for (...) { } = คำสั่งวนซ้ำที่เขียนได้ใน HTML โดยตรง แปลว่า "หยิบหนังสือทีละเล่มจาก books() มาสร้าง <li> หนึ่งอัน"

💡 track b.id = บอก Angular ว่าใช้ id เป็นตัวแยกแยะว่าอันไหนเป็นอันไหน เพื่อให้อัปเดตหน้าจอได้เร็ว จำเป็นต้องใส่ ถ้าไม่ใส่จะ error

💡 {{ b.title }} = เอาค่าจากโค้ดมาแสดงบนหน้าจอ เรียกว่า binding หรือเส้นเชื่อมระหว่างข้อมูลกับสิ่งที่เห็น

💡 class="p-6 text-2xl font-bold" = คลาสของ Tailwind ที่ติดตั้งไว้แล้ว แต่ละตัวทำหน้าที่เดียว เช่น p-6 คือ padding, font-bold คือตัวหนา — ไม่ต้องเขียน CSS เองเลย

2.5 เอา component มาแสดงจริง

แก้ apps/web/src/app/app.routes.ts

typescript
import { Routes } from '@angular/router';
import { BookList } from './books/book-list/book-list';

export const routes: Routes = [
  { path: '', component: BookList },
];

แก้ apps/web/src/app/app.html ให้เหลือแค่

html
<router-outlet />

💡 <router-outlet /> = ช่องว่างที่บอกว่า "เอาหน้าที่ตรงกับ URL ปัจจุบันมาแสดงตรงนี้" 💡 path: '' = หน้าแรกสุด คือ localhost:4200 โดยไม่มีเส้นทางต่อท้าย

✅ เปิด localhost:4200 ต้องเห็นรายการหนังสือ 3 เล่มในกรอบสวย ๆ


Part 3 — แตก component ย่อย

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • 🟢 Angular (frontend) ← กำลังทำส่วนนี้
  • ✅ NestJS (backend) (ทำไปแล้ว)
  • ✅ PostgreSQL (ฐานข้อมูล) (ทำไปแล้ว)

ต่อไป: เพิ่มปุ่มกดโต้ตอบกับผู้ใช้

ตอนนี้โค้ดหน้าตาของการ์ดปนอยู่ในรายการ ถ้าอยากใช้การ์ดที่อื่นด้วยจะคัดลอกไปมา ลองแยกออกมาเป็นชิ้นเลโก้ของตัวเอง

3.1 สร้าง component การ์ด

powershell
npm exec -- ng generate component books/book-card --skip-tests

3.2 ให้การ์ดรับข้อมูลเข้ามาได้

แทนที่ book-card.ts

typescript
import { Component, input } from '@angular/core';
import { Book } from '../book';

@Component({
  selector: 'app-book-card',
  imports: [],
  templateUrl: './book-card.html',
  styleUrl: './book-card.css',
})
export class BookCard {
  book = input.required<Book>();
}

💡 input() คือช่องเสียบข้อมูลของตัวต่อ แม่พิมพ์การ์ดมีช่องให้เสียบว่าจะแสดงหนังสือเล่มไหน ชิ้นเดียวกันจึงแสดงข้อมูลต่างกันได้

input.required<Book>() = ช่องนี้ต้องเสียบ ถ้าลืมจะเตือนตั้งแต่ตอนพิมพ์ อ่านค่าด้วย book() มีวงเล็บ เหมือน signal เพราะเป็น signal ชนิดหนึ่ง

3.3 หน้าตาของการ์ด

แทนที่ book-card.html

html
<div class="border rounded-lg p-4 hover:shadow-md transition">
  <p class="font-bold text-lg">{{ book().title }}</p>
  <p class="text-sm text-gray-600">{{ book().author }}</p>
  <div class="mt-2 flex items-center gap-2">
    <span class="text-xs bg-gray-100 px-2 py-1 rounded">{{ book().year }}</span>
    @if (book().category) {
      <span class="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">
        {{ book().category?.name }}
      </span>
    }
  </div>
</div>

💡 @if (...) { } = แสดงเฉพาะเมื่อเงื่อนไขเป็นจริง ตรงนี้คือ "ถ้ามีหมวดหมู่ค่อยแสดงป้ายสีฟ้า" 💡 ?. ใน category?.name = "ถ้ามี category ค่อยหยิบ name" ป้องกัน error ตอนไม่มีข้อมูล

3.4 เอาการ์ดไปใช้ในรายการ

แก้ book-list.ts เพิ่ม import

typescript
import { Component, signal } from '@angular/core';
import { Book } from '../book';
import { BookCard } from '../book-card/book-card';

@Component({
  selector: 'app-book-list',
  imports: [BookCard],
  templateUrl: './book-list.html',
  styleUrl: './book-list.css',
})
export class BookList {
  books = signal<Book[]>([
    { id: 1, title: 'Clean Code', author: 'Robert C. Martin', year: 2008, category: { id: 1, name: 'Programming' } },
    { id: 2, title: 'Refactoring', author: 'Martin Fowler', year: 1999, category: { id: 1, name: 'Programming' } },
    { id: 3, title: "Don't Make Me Think", author: 'Steve Krug', year: 2000, category: { id: 2, name: 'Design' } },
  ]);
}

⚠️ ต้องใส่ BookCard ใน imports: [] มิฉะนั้น Angular จะไม่รู้จักแท็ก <app-book-card> — เป็นจุดที่มักลืมกันบ่อย

แก้ book-list.html

html
<div class="p-6 max-w-3xl mx-auto">
  <h1 class="text-2xl font-bold mb-4">รายการหนังสือ</h1>

  <div class="grid gap-3">
    @for (b of books(); track b.id) {
      <app-book-card [book]="b" />
    }
  </div>
</div>

💡 [book]="b" = เสียบข้อมูลเข้าช่อง book ของการ์ด วงเล็บเหลี่ยมแปลว่า "ค่านี้มาจากโค้ด" ถ้าไม่ใส่วงเล็บ Angular จะคิดว่าเราส่งข้อความคำว่า "b" ไปจริง ๆ

✅ หน้าเว็บต้องเหมือนเดิม แต่ตอนนี้โค้ดแยกเป็นชิ้นแล้ว


Part 4 — เพิ่มการโต้ตอบ

🗺️ ตำแหน่งตอนนี้ในระบบ

  • ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
  • 🟢 Angular (frontend) ← กำลังทำส่วนนี้
  • ✅ NestJS (backend) (ทำไปแล้ว)
  • ✅ PostgreSQL (ฐานข้อมูล) (ทำไปแล้ว)

ต่อไป: Section หน้า: เชื่อม frontend นี้เข้ากับ API จริง

4.1 ทำปุ่มกรองตามหมวดหมู่

แก้ book-list.ts เพิ่มสองอย่าง

typescript
export class BookList {
  books = signal<Book[]>([ /* ...ของเดิม... */ ]);

  selected = signal<string>('ทั้งหมด');

  filtered() {
    const all = this.books();
    if (this.selected() === 'ทั้งหมด') return all;
    return all.filter((b) => b.category?.name === this.selected());
  }
}

แก้ book-list.html

html
<div class="p-6 max-w-3xl mx-auto">
  <h1 class="text-2xl font-bold mb-4">รายการหนังสือ</h1>

  <div class="flex gap-2 mb-4">
    @for (c of ['ทั้งหมด', 'Programming', 'Design']; track c) {
      <button
        (click)="selected.set(c)"
        class="px-3 py-1 rounded border"
        [class.bg-blue-600]="selected() === c"
        [class.text-white]="selected() === c">
        {{ c }}
      </button>
    }
  </div>

  <div class="grid gap-3">
    @for (b of filtered(); track b.id) {
      <app-book-card [book]="b" />
    }
  </div>
</div>

💡 (click)="..." = วงเล็บกลมคือเหตุการณ์ แปลว่า "เมื่อคลิกให้ทำสิ่งนี้" จำง่าย ๆ: วงเล็บเหลี่ยม [ ] = ข้อมูลไหลเข้าหน้าจอ / วงเล็บกลม ( ) = เหตุการณ์ไหลออกจากหน้าจอ

💡 selected.set(c) = เปลี่ยนค่าในกล่อง signal พอเปลี่ยนปุ๊บ หน้าจอวาดใหม่เองทันที นี่คือพลังของ signal

💡 [class.bg-blue-600]="เงื่อนไข" = ใส่คลาสนี้เฉพาะเมื่อเงื่อนไขเป็นจริง ใช้ทำปุ่มที่ถูกเลือกให้เป็นสีน้ำเงิน

✅ กดปุ่มแล้วรายการต้องเปลี่ยนตาม และปุ่มที่เลือกต้องเป็นสีน้ำเงิน

🚀 ถ้าเสร็จก่อนเพื่อน

  1. เพิ่มตัวเลขบอกจำนวนเล่มที่กำลังแสดงอยู่
  2. เพิ่มปุ่มเรียงตามปี จากใหม่ไปเก่า
  3. ทำให้การ์ดเปลี่ยนสีขอบเมื่อหนังสือเก่ากว่าปี 2000

❓ ปัญหาที่พบบ่อย

อาการ สาเหตุ วิธีแก้
'app-book-card' is not a known element ลืมใส่ใน imports: [] เพิ่ม BookCard เข้าไป
แสดงคำว่า [object Object] หรือฟังก์ชัน ลืมวงเล็บตอนอ่าน signal เปลี่ยน books เป็น books()
@for ขึ้น error ลืม track เพิ่ม track b.id
หน้าขาวเปล่า มี error ใน console กด F12 เปิด Console อ่านข้อความสีแดง
Tailwind ไม่ทำงาน พิมพ์ชื่อคลาสผิด ตรวจสะกด เช่น text-gray-600 ไม่ใช่ text-grey-600

📌 What You Learned

Syntax Meaning
signal(value) A reactive container that holds a value.
name() Read the current value.
name.set(newValue) Update the stored value.
input.required<T>() A required input received from a parent component.
@for (x of list; track x.id) Render each item in a list.
@if (condition) Render content when a condition is true.
[prop]="value" Pass data into a property.
(click)="handler()" Handle an event.

Next section: Remove the mock data and load real data from the API created in Section 2.