🔁 ทวนก่อนเริ่ม — ทั้งบทใช้คำพวกนี้ ถ้าข้อไหนไม่คุ้นให้กลับไปบทนั้นก่อน
- Turbo Frame = เปลี่ยนแค่ถ้วยใบเดียวบนโต๊ะ · ป้ายเลขถ้วย (
id) ต้องตรงกันสองฝั่ง เราตั้งด้วยdom_id(post, :title)(บท 131) - Turbo Stream = ใบสั่งงานหลายข้อให้พนักงานเสิร์ฟ "เติมคอมเมนต์ต่อท้าย · เปลี่ยนตัวนับ · เปลี่ยนฟอร์มเป็นฟอร์มว่าง" (บท 132)
- broadcast = เสียงตามสายประกาศถึงทุกโต๊ะ ·
turbo_stream_from= โต๊ะที่เปิดลำโพงรอฟัง (บท 133) - Stimulus = อุปกรณ์บนโต๊ะที่ทำงานเองโดยไม่ต้องเรียกครัว ·
connect()= ติดตั้งตอนจัดโต๊ะ ·disconnect()= เก็บกวาดตอนเก็บโต๊ะ (บท 134) - prompts/ = แฟ้มใบสั่งงานให้ AI ·
00-context.md= กฎถาวรของร้านที่แนบไปทุกครั้ง (บท 121)
🍜 เริ่มจากโค้ดสถานะไหน: ต่อจากตอนจบบท 134 ที่ commit แล้ว (git status --short ว่าง) — หน้า /posts/1 แก้หัวข้อในที่เดิมได้ · ส่งคอมเมนต์ได้และเด้งทุกแท็บ · ช่องพิมพ์คอมเมนต์มีตัวนับ "0 / 1000"
· ถ้าตามไม่ทัน กลับไปบท 131–134 ตามลำดับ · บทนี้เพิ่มแค่สองไฟล์ใน prompts/ · จบบทโค้ดของคุณคือ จุดเช็คพอยต์ C (จบ Day 3 ช่วงเช้า)
🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 18 · turbo-rails 2.0.23 · stimulus-rails 1.3.4 · Chrome (สั่งด้วยสคริปต์ ไม่ได้กดเอง) (สไลด์กับ repo ของอาจารย์เขียน Ruby 4.0.5 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน)
ผลรันทุกก้อนในบทนี้รันจริงแล้วก็อปมา · ค่าที่เปลี่ยนทุกครั้ง เช่น เวลา ms · เลข id · เลข commit · วันเวลา · ชื่อไฟล์ migration ของคุณจะต่าง · พฤติกรรมในเบราว์เซอร์ตรวจด้วย Chrome จริงที่สั่งด้วยสคริปต์ · log ตัดรหัสสีออกแล้ว
Lab นี้คือวันตรวจรับ ไม่ใช่วันสร้าง
สไลด์หน้า 52 ให้ Lab นี้เป็น "เขียน prompt ให้ AI ทำทั้งบล็อก แล้วตรวจ" · แต่ในเว็บนี้เราเลือกให้คุณทำเองเป็นก่อน — คุณจึงทำครบทั้ง 3 เป้าหมายไปแล้วในบท 131–134 · สิ่งที่เหลือคือส่วนที่สำคัญที่สุดของการทำงานกับ AI: ตรวจรับให้เป็น
| เป้าหมายในสไลด์หน้า 52 | ทำไปแล้วในบท | ไฟล์หลักที่ได้ |
|---|---|---|
| 1) แก้หัวข้อโพสต์ได้ในหน้าเดิม ไม่ต้องเปลี่ยนหน้า | 131 Turbo Frame | posts/_post.html.erb · posts/edit.html+turbo_frame.erb |
| 2) เพิ่ม Comment ได้ และเด้งขึ้นทุกแท็บที่เปิดอยู่ | 132 Turbo Stream (ตอบฟอร์ม) | comments_controller.rb · comments/create.turbo_stream.erb |
| 133 broadcast | models/comment.rb (broadcasts_to) · turbo_stream_from ใน posts/show.html.erb | |
| 3) ช่องพิมพ์ comment มีตัวนับอักษร | 134 Stimulus | controllers/character_counter_controller.js · comments/_form.html.erb |
Lab นี้มี 5 ขั้น:
ช่างมาติดตั้งของใหม่ในร้าน 4 รอบ (บท 131–134): ถ้วยที่เปลี่ยนได้ทีละใบ · ใบสั่งงานหลายข้อ · เสียงตามสาย · นาฬิกาจับเวลาบนโต๊ะ · วันนี้เจ้าของร้านเดินถือใบตรวจรับ 7 ข้อ ไล่ลองของจริงทีละข้อ ก่อนเซ็นรับงาน · ใบตรวจรับเดียวกันนี้ใช้ได้กับช่างทุกเจ้า ไม่ว่าคนหรือ AI
ที่ไม่เหมือนกัน: ตรวจรับร้านจริงทำครั้งเดียวจบ · ของเราตรวจซ้ำได้ฟรีทุกครั้งที่โค้ดเปลี่ยน และควรตรวจซ้ำ — โค้ดที่ผ่านเมื่อวานพังได้วันนี้ ถ้ามีใคร (หรือ AI) ไปแก้ไฟล์ข้าง ๆ
- acceptance criteria (เกณฑ์ตรวจรับ)
- รายการข้อที่ต้องจริงทุกข้อ งานถึงจะนับว่า "เสร็จ" · ส่วนที่ 4 ของ prompt 4 ส่วน (บท 89) · ถาม senior ได้ว่า "งานนี้มี acceptance criteria อะไรบ้างครับ จะได้ตรวจเองก่อนส่ง review"
- manual test · verify
- manual test = ลองใช้งานเองด้วยมือตามขั้นตอนที่กำหนด · verify = พิสูจน์ว่าจริง ไม่ใช่แค่เชื่อ · พิมพ์บอก AI ได้ว่า "Don't tell me it works. Give me the exact steps to verify each acceptance criterion in the browser."
ขั้น 1 — ใบสั่งงาน prompts/03-hotwire.md: ทุกบรรทัดมาจากบทไหน
ไฟล์นี้คือใบสั่งงานที่จะส่งให้ AI ทำบล็อก C ทั้งหมดในครั้งเดียว · หน้าตาเหมือน 01-views.md (บท 126) กับ 02-styling.md (บท 129):
เป้าหมาย · ข้อกำหนด · ห้าม · เกณฑ์ตรวจรับ — สี่ส่วนของบท 121
ตอนนี้ ls prompts ควรเห็น 3 ไฟล์ (00-context.md · 01-views.md · 02-styling.md) · บท 134 ฝากไฟล์นี้ไว้ให้บทนี้ —
สร้างไฟล์ prompts/03-hotwire.md แล้วพิมพ์ตามนี้ (ทั้งไฟล์ — ก็อปมาจากสไลด์หน้า 52 · แก้บรรทัด broadcasts_to หนึ่งจุด และใส่ backtick รอบ <script>):
## เป้าหมาย
1) แก้หัวข้อโพสต์ได้ในหน้าเดิม ไม่ต้องเปลี่ยนหน้า
2) เพิ่ม Comment ได้ และเด้งขึ้นทุกแท็บที่เปิดอยู่
3) ช่องพิมพ์ comment มีตัวนับอักษร
## ข้อกำหนด
- สร้าง model Comment: body:text author_name:string
post:references (ยังไม่มีระบบล็อกอิน)
- validation + NOT NULL + index ครบคู่
- inline edit ใช้ turbo-frame
ใช้ dom_id(post) ตั้ง id ทั้งสองฝั่ง
- เพิ่ม comment ตอบด้วย create.turbo_stream.erb
append รายการ + update ตัวนับ + ล้างฟอร์ม
- real-time ใช้ broadcasts_to :post
(มันส่งต่อให้ job เองอยู่แล้ว ไม่มี option later:)
- ตัวนับอักษรเป็น Stimulus controller
ต้องมี disconnect ที่เก็บกวาด listener
## ห้าม
- เขียน addEventListener นอก Stimulus
- ใส่ `<script>` ใน view
- hardcode id ของ turbo-frame เป็น string
- ใช้ after_save (ต้องเป็น after_commit)
- ลืม status: :unprocessable_content ตอน render ใหม่
## เกณฑ์ตรวจรับ
- [ ] กดแก้ไขแล้ว URL ไม่เปลี่ยน และส่วนอื่นไม่ขยับ
- [ ] เปิด 2 แท็บ พิมพ์แท็บหนึ่ง อีกแท็บขึ้นเอง
- [ ] ตัวนับจำนวน comment อัปเดตพร้อมกัน
- [ ] ฟอร์มว่างเองหลัง submit
- [ ] Stimulus controller มี disconnect
- [ ] เปิด /posts/1/edit ตรง ๆ ยังได้หน้าเต็ม
- [ ] console ไม่มี error เรื่อง matching turbo-frame
## = หัวข้อของ Markdown · - [ ] = ช่องติ๊กที่ยังไม่ติ๊ก (ติ๊กแล้วเขียน - [x]) · `<script>` ครอบด้วย backtick ให้ AI อ่านว่าเป็น "ชื่อแท็ก" ไม่ใช่แท็กจริง
· ทุกบรรทัดของข้อกำหนดกับข้อห้ามมาจากเรื่องที่คุณเจอมาแล้ว:
| บรรทัดในใบสั่ง | เรียนในบท | ทำไมต้องเขียนบอก AI (ถ้าไม่เขียน AI มักทำแบบไหน) |
|---|---|---|
| model Comment · validation + NOT NULL + index ครบคู่ | 132 (ทวน 110 · 111) | generator ไม่ใส่ null: false ให้ช่อง body กับ author_name — AI ที่สั่ง generate แล้วไม่แก้ต่อจะได้แค่ validation ฝั่ง Rails |
inline edit ใช้ turbo-frame · dom_id ทั้งสองฝั่ง | 131 | ถ้าฝั่งหนึ่งพิมพ์ id เอง อีกฝั่งใช้ helper — สะกดไม่ตรงกันนิดเดียว ถ้วยก็หาไม่เจอ |
create.turbo_stream.erb · append + update + ล้างฟอร์ม | 132 | ระบุ 3 จุดชัด ๆ ไม่งั้น AI มักทำแค่ append แล้วลืมตัวนับกับฟอร์ม |
broadcasts_to :post | 133 | บอกชื่อเครื่องมือตรง ๆ · บรรทัดในวงเล็บคือจุดที่เราแก้จากสไลด์ (ดูกล่องเตือนข้างล่าง) |
| Stimulus controller ต้องมี disconnect | 134 | AI ชอบเขียน JS แบบ <script> + querySelector ในหน้า เพราะเห็นตัวอย่างแบบนั้นบนเน็ตเยอะ |
| ห้าม 5 ข้อ | 130–134 | แต่ละข้อคือ "ที่ AI พลาดประจำ" ของสไลด์หน้า 53 — หัวข้อถัดจากขั้น 3 โชว์ว่าพลาดแล้วอาการเป็นยังไง |
⚠️ จุดที่แก้เนื้อหาจากสไลด์: สไลด์หน้า 52 เขียน broadcasts_to :post พร้อม later: true ·
ใน turbo-rails 2.0.23 broadcasts_to ไม่มีตัวเลือก later: และส่งงานต่อให้ job เองอยู่แล้ว (job = งานที่ server เก็บไว้ทำต่อเบื้องหลังหลังตอบลูกค้าไปแล้ว · บท 133 · ละเอียดใน Day 3 Part 2) ·
ถ้าส่งตามสไลด์ AI จะเขียน later: true ที่ไม่มีผลอะไร แล้วคุณจะเสียเวลาสงสัยว่ามันทำอะไร — เราจึงเขียนบอกไว้ในใบสั่งเลย
ขั้น 2 — ตรวจรับ 7 ข้อของสไลด์หน้า 52 ด้วยของจริง
สไลด์หน้า 52 มีประโยคที่สำคัญที่สุดของ Lab นี้: เกณฑ์ข้อ 2 (สองแท็บ) พิสูจน์ด้วยการรัน ไม่ใช่ด้วยการอ่านโค้ด — ซึ่งเป็นแบบที่ดีที่สุด เพราะ AI โกงไม่ได้ · AI เขียนในคำตอบได้ว่า "ทำครบแล้ว ผ่านทุกข้อ" แต่มันทำให้คอมเมนต์เด้งในหน้าต่างที่สองของคุณด้วยคำพูดไม่ได้
พ่อครัวยื่นสูตรมาบอกว่า "ใส่น้ำปลาแล้ว ใส่มะนาวแล้ว" — อ่านสูตรแล้วดูครบ แต่ยังไม่รู้ว่าอร่อยไหม · เจ้าของร้านที่ดีตักชิมเอง และให้คนสองโต๊ะสั่งพร้อมกัน ดูว่าเสียงตามสายดังถึงทั้งสองโต๊ะจริงไหม · อ่านโค้ด = อ่านสูตร · เปิดเบราว์เซอร์สองหน้าต่างลองเอง = ชิม
ที่ไม่เหมือนกัน: การชิมอาหารบอกได้ทุกอย่างที่ลูกค้าจะเจอ · แต่การลองในเบราว์เซอร์บอกไม่ได้ทุกอย่าง
— บางบั๊กไม่มีอาการให้เห็นด้วยตา (เช่น ลืม disconnect ในขั้น 3) จึงต้องมีตรวจด้วยคำสั่งควบคู่กันเสมอ
เตรียมโต๊ะตรวจ
- แท็บ terminal ที่ 1:
bin/dev(เปิดร้าน + ตัว build CSS · บท 128) - เปิด Chrome สองหน้าต่าง (ไม่ใช่สองแท็บในหน้าต่างเดียว — จะได้เห็นพร้อมกัน) วางซ้าย-ขวา · ทั้งสองเปิด
http://localhost:3000/posts/1 - ในหน้าต่างซ้ายเปิด Console: Mac กด Cmd+Option+J · Windows กด Ctrl+Shift+J · Console คือกระดานแจ้งปัญหาของหน้าเว็บ — JavaScript ที่พังหรือคำเตือนของ Turbo จะขึ้นเป็นตัวแดงที่นี่ (หน้าเว็บเองไม่โชว์อะไรให้เห็นเลย)
⚠️ ระวังสับสน — หน้านี้มี "ตัวนับ" สองตัว: ตัวนับจำนวนคอมเมนต์ ในหัวข้อ "ความคิดเห็น (0)" (มาจาก Turbo Stream + broadcast · บท 132–133) กับตัวนับอักษร "0 / 1000" ใต้ช่องพิมพ์ (มาจาก Stimulus · บท 134) · เกณฑ์ข้อ 3 พูดถึงตัวแรก · เกณฑ์ข้อ 5 พูดถึงตัวหลัง
ตารางตรวจรับ (ผลจริงที่ผมได้ จาก Chrome ที่สั่งด้วยสคริปต์บนโค้ดสถานะเดียวกับของคุณ)
| # | เกณฑ์ | ทำยังไง | ควรเห็น (ผลจริง) |
|---|---|---|---|
| 1 | กดแก้ไขแล้ว URL ไม่เปลี่ยน และส่วนอื่นไม่ขยับ | หน้าต่างซ้าย กด "แก้ไขหัวข้อ" · แก้เป็นข้อความใหม่ · กด "บันทึก" | หัวข้อกลายเป็นช่องพิมพ์ที่เดิม (เคอร์เซอร์อยู่ในช่องเลย) · กดบันทึกแล้วหัวข้อใหม่ขึ้นที่เดิม · URL ยังเป็น /posts/1 ตลอด |
| 2 | เปิด 2 แท็บ พิมพ์แท็บหนึ่ง อีกแท็บขึ้นเอง | หน้าต่างซ้าย พิมพ์ชื่อ + ความคิดเห็น แล้วกด "ส่งความคิดเห็น" · ไม่แตะหน้าต่างขวาเลย | คอมเมนต์โผล่ในหน้าต่างขวาเอง · ของผมใช้ 104 ms นับจากกดส่ง (ของคุณจะต่าง แต่ควรรู้สึกว่า "ทันที") |
| 3 | ตัวนับจำนวน comment อัปเดตพร้อมกัน | ดูหัวข้อ "ความคิดเห็น (…)" ทั้งสองหน้าต่าง | ทั้งซ้ายและขวาเปลี่ยนจาก (0) เป็น (1) |
| 4 | ฟอร์มว่างเองหลัง submit | ดูฟอร์มในหน้าต่างซ้ายหลังกดส่ง | ช่องชื่อกับช่องความคิดเห็นว่าง · ตัวนับอักษรกลับเป็น 0 / 1000 (ตอนพิมพ์ "อร่อยมาก" มันขึ้น 8 / 1000) |
| 5 | Stimulus controller มี disconnect | คำสั่ง grep (ขั้น 3) + ลองใส่ console.log (ข้างล่าง) |
เจอ disconnect() { ในไฟล์ตัวนับ · และ Console ขึ้น disconnect → connect ทุกครั้งที่ฟอร์มถูกแทน |
| 6 | เปิด /posts/1/edit ตรง ๆ ยังได้หน้าเต็ม | พิมพ์ localhost:3000/posts/1/edit ในช่อง URL แล้วกด Enter |
หน้า "แก้ไขบทความ" เต็มหน้า มี navbar · ฟอร์ม 3 ช่อง (หัวข้อ · เนื้อหา · หมวด) ไม่ใช่แค่ช่องหัวข้อ |
| 7 | console ไม่มี error เรื่อง matching turbo-frame | ดู Console หลังทำข้อ 1–6 ครบ | ว่างทั้งสามหน้าต่างที่ผมเปิด (ซ้าย · ขวา · หน้า edit ตรง ๆ) |
ระวังเข้าใจผิดในข้อ 1: หลังบันทึกหัวข้อใหม่ แถบทางกลับด้านบน (breadcrumb จากบท 126) และชื่อบนแท็บเบราว์เซอร์ยังเป็นหัวข้อเดิม
— ผลจริง: กรอบโชว์ ต้มยำกุ้ง (แก้ในหน้าเดิม) แต่ breadcrumb ยังเป็น ต้มยำกุ้งน้ำข้น
นี่ไม่ใช่บั๊ก · Turbo Frame เปลี่ยนแค่ "ถ้วยใบเดียว" ตามที่ตั้งใจ (บท 131) ส่วนอื่นของโต๊ะไม่มีใครแตะ · รีเฟรชหน้าแล้วทุกจุดตรงกัน · เกณฑ์ "ส่วนอื่นไม่ขยับ" หมายถึงสิ่งนี้เลย
ข้อ 7 กับข้อความ 422: ถ้าคุณลองกดส่งฟอร์มคอมเมนต์ทั้งที่ว่าง Console จะขึ้นบรรทัดนี้ (ผลจริง):
Failed to load resource: the server responded with a status of 422 (Unprocessable Content)
บรรทัดนี้ปกติ — มันแค่รายงานว่า server ตอบ 422 ("ข้อมูลไม่ผ่าน" ที่เราตั้งใจส่งกลับเพื่อโชว์ error ในฟอร์ม · บท 132) · ที่เกณฑ์ข้อ 7 ห้ามคือข้อความที่มีคำว่า turbo-frame (หน้าตาของมันอยู่ในขั้น 3)
ข้อ 5 แบบเห็นกับตา: ใส่ console.log ชั่วคราว
grep บอกได้แค่ว่า "มีคำว่า disconnect ในไฟล์" · อยากเห็นว่ามันถูกเรียกจริง ให้เปิด app/javascript/controllers/character_counter_controller.js
แล้วเติม 2 บรรทัดที่มีป้าย // ← เติมชั่วคราว (ที่เหลือคือไฟล์เดิมจากบท 134 ไม่ต้องแตะ):
import { Controller } from "@hotwired/stimulus"
// ตัวนับอักษรของช่องพิมพ์คอมเมนต์ (อยู่ใน comments/_form.html.erb) แสดงเป็น "12 / 1000"
export default class extends Controller {
static targets = ["input", "output"]
// ฟอร์มโผล่ (โหลดหน้า หรือ Turbo Stream ใส่ฟอร์มใหม่): เริ่มฟังการพิมพ์
connect() {
console.log("ตัวนับ: connect") // ← เติมชั่วคราว
this.onInput = this.update.bind(this)
this.inputTarget.addEventListener("input", this.onInput)
this.update()
}
// ฟอร์มหายไป: ถอด listener ที่ connect ผูกไว้
disconnect() {
console.log("ตัวนับ: disconnect") // ← เติมชั่วคราว
this.inputTarget.removeEventListener("input", this.onInput)
}
update() {
this.outputTarget.textContent = `${this.inputTarget.value.length} / ${this.inputTarget.maxLength}`
}
}
console.log(...) = เขียนข้อความลง Console (เหมือน fmt.Println ของ Go แต่ไปขึ้นในเบราว์เซอร์) · ไม่ต้อง restart server — importmap ส่งไฟล์ใหม่ให้เอง (บท 127)
บันทึกไฟล์ รีเฟรชหน้า · ส่งคอมเมนต์หนึ่งครั้ง แล้วกดส่งแบบว่างอีกครั้ง · Console ที่ได้จริง:
// เปิดหน้า
ตัวนับ: connect
// ส่งคอมเมนต์สำเร็จ (Turbo Stream เปลี่ยนฟอร์มเป็นฟอร์มว่าง)
ตัวนับ: disconnect
ตัวนับ: connect
// ส่งแบบว่าง (422 · Turbo Stream เปลี่ยนฟอร์มเป็นฟอร์มที่มี error)
Failed to load resource: the server responded with a status of 422 (Unprocessable Content)
ตัวนับ: disconnect
ตัวนับ: connect
ทุกครั้งที่ Turbo Stream เอาฟอร์มเก่าออก ตัวนับตัวเก่าเก็บกวาด (disconnect) แล้วตัวนับของฟอร์มใหม่ติดตั้ง (connect) เอง — โดยที่เราไม่ได้เขียนโค้ดสั่งเลยสักบรรทัด
· ดูเสร็จแล้วคืนไฟล์ด้วย git restore app/javascript (คืนเฉพาะโฟลเดอร์ app/javascript ให้เหมือน commit ล่าสุด)
ข้อ 6 ดูให้ลึกกว่าตา: server ส่งอะไรมาจริง
ข้อนี้ตรวจว่าเราไม่ได้ทำ "หน้า edit ที่ใช้ได้แค่ในกรอบ" · curl (โปรแกรมที่ขอหน้าเว็บแบบไม่มีเบราว์เซอร์) จำลองการเปิดตรง ๆ ได้ (ผลจริง):
$ curl -s localhost:3000/posts/1/edit | grep -E '<title>|<h1|class="navbar'
<title>แก้ไขบทความ</title>
--><header class="navbar bg-base-100 shadow-sm">
<h1 class="font-bold text-4xl">แก้ไขบทความ</h1>
ครบทั้ง title · navbar · หัวเรื่อง = หน้าเต็ม (--> หน้าบรรทัด navbar คือปลายคอมเมนต์ที่ Rails แปะบอกว่ามาจาก partial ไหน · มีแค่ใน development) ·
ทีนี้ดูใน log/development.log เทียบกับตอนกด "แก้ไขหัวข้อ" ในหน้า show (ผลจริง ตัดมาบรรทัดเดียวต่อครั้ง):
# เปิด /posts/1/edit ตรง ๆ
Rendering posts/edit.html.erb within layouts/application
# กด "แก้ไขหัวข้อ" ในกรอบ (เบราว์เซอร์แนบ header Turbo-Frame: title_post_1 มาด้วย)
Rendering posts/edit.html+turbo_frame.erb within layouts/turbo_rails/frame
header = ป้ายข้อมูลที่แปะบนหัวใบสั่ง (ผู้ใช้มองไม่เห็น) · URL เดียวกัน แต่ server เลือกไฟล์ต่างกันตามว่าคำขอ "มาจากกรอบ" หรือไม่ — นี่คือ variant ที่ตั้งไว้ในบท 131 (set_turbo_frame_variant) ·
คนที่ปิด JavaScript หรือเปิดลิงก์ในแท็บใหม่ ยังแก้บทความได้ครบ — แนวคิดนี้ชื่อ progressive enhancement (ของพื้นฐานใช้ได้เสมอ JS แค่ทำให้ดีขึ้น)
- DevTools · Console
- เครื่องมือนักพัฒนาที่ติดมากับเบราว์เซอร์ · Console = แท็บที่โชว์ข้อความจาก
console.logและ error ของ JavaScript · ถาม senior ได้ว่า "หน้านี้ไม่ขยับครับ แต่ Console มี error ตัวแดงข้อความนี้ — ช่วยดูหน่อย" (แล้วก็อปข้อความไปด้วยทั้งบรรทัด) - end-to-end check · two-browser test
- ตรวจตั้งแต่ต้นทางถึงปลายทางแบบที่ผู้ใช้เจอจริง · ทดสอบ real-time ด้วยสองหน้าต่างพร้อมกัน · พิมพ์บอก AI ได้ว่า "I will verify the broadcast with two browser windows side by side. List what I should see in each window."
- progressive enhancement
- ทำให้หน้าใช้ได้ด้วย HTML ธรรมดาก่อน แล้วค่อยเติม JS ให้ลื่นขึ้น · ถ้า JS ไม่ทำงาน ผู้ใช้ยังทำงานได้ · ถาม senior ได้ว่า "ถ้าผู้ใช้เปิดลิงก์ edit ในแท็บใหม่ หน้านี้ยังทำงานครบไหมครับ"
ขั้น 3 — ตรวจ 4 อย่างด้วยคำสั่ง (สไลด์หน้า 53) และกับดักในคำสั่งของสไลด์
สไลด์หน้า 53 มีกล่อง "ตรวจ 4 อย่างนี้ก่อน":
- Console ของเบราว์เซอร์มี error ไหม — ทำแล้วในขั้น 2
grepหาแท็ก<scriptในโฟลเดอร์ viewgrepหาคำว่าdisconnectในโฟลเดอร์ JavaScript- เปิด 2 แท็บทดสอบของจริง — ทำแล้วในขั้น 2
เหลือข้อ 2 กับ 3 ที่เป็นคำสั่ง · ข้อ 2 (ผลจริง):
$ grep -rn "<script" app/views
$
ทวนจากบท 126: grep = ค้นหาข้อความในไฟล์ · -r ค้นทั้งโฟลเดอร์ · -n บอกเลขบรรทัด ·
ผลจริงคือไม่มีอะไรขึ้นเลย = ไม่เจอ = ผ่าน (ไม่มีแท็ก <script ซ่อนอยู่ใน view ไหน) · ส่วนข้อ 3 สไลด์เขียนคำสั่งแบบนี้:
$ grep -n "disconnect" app/javascript/**
ไฟล์ตัวนับมี disconnect() { อยู่บรรทัด 15 ของ app/javascript/controllers/character_counter_controller.js แน่ ๆ — คำสั่งนี้จะเจอไหม? ขึ้นอะไร?
ไม่เจอ · ผลจริง (ลองทั้ง zsh ที่เป็นค่าตั้งต้นของ terminal บน Mac และ bash ได้เหมือนกัน):
$ grep -n "disconnect" app/javascript/**
grep: app/javascript/controllers: Is a directory
- terminal (ตัวที่อ่านคำสั่งเรียกว่า shell) แปลง
**เป็นรายชื่อไฟล์ก่อนส่งให้ grep ·**ที่ไม่มีอะไรต่อท้าย มันอ่านเหมือน*ธรรมดา = "ของที่อยู่ชั้นแรกของโฟลเดอร์" → ได้แค่app/javascript/application.jsกับโฟลเดอร์controllers grepไม่มี-rจึงไม่เข้าไปในโฟลเดอร์ บ่นว่า "Is a directory" แล้วข้ามไป · ไฟล์ตัวนับอยู่ในโฟลเดอร์นั้น จึงไม่ถูกค้นเลย- อันตรายตรงที่ข้อความนี้ดูเหมือน "ไม่เจอ disconnect" — คุณอาจสรุปผิดว่างานไม่ผ่าน (หรือแย่กว่า: ใช้คำสั่งนี้ตรวจงานที่ไม่มี disconnect จริง แล้วไม่ทันสังเกตว่ามันไม่ได้ค้นอะไรเลย)
คำสั่งที่ใช้ได้จริง — เติม -r แล้วให้ชื่อโฟลเดอร์ตรง ๆ (ผลจริง):
$ grep -rn "disconnect" app/javascript
app/javascript/controllers/character_counter_controller.js:15: disconnect() {
อ่านผล: ชื่อไฟล์:เลขบรรทัด:เนื้อบรรทัด · theme_controller.js กับ hello_controller.js ไม่มี disconnect และไม่ต้องมี
เพราะไม่ได้สร้าง listener หรือตัวจับเวลาใน connect (กฎข้อ 10 พูดถึงเฉพาะ controller "ที่สร้างของใน connect")
ใส่บั๊กกลับ ก่อนเชื่อว่า "ไม่เจอ = ผ่าน"
ตัวตรวจที่ไม่เคยจับอะไรได้เลย อาจแปลว่างานดี — หรือแปลว่าตัวตรวจพัง (เหมือนคำสั่ง ** ข้างบน) · ทางเดียวที่จะรู้คือใส่บั๊กให้มันจับดูก่อน:
① เติม 3 บรรทัดนี้ท้าย app/views/posts/show.html.erb ชั่วคราว (แบบที่ AI ชอบเขียน):
<script>
document.querySelector("#comment_body").addEventListener("input", (e) => console.log("script นับได้", e.target.value.length))
</script>
<script>…</script>= แท็กที่ใส่ JavaScript ลงในหน้า HTML ตรง ๆdocument.querySelector("#comment_body")= หา element ตัวแรกในหน้าที่มี idcomment_body— ช่องพิมพ์ความคิดเห็น (form_withตั้ง id นี้ให้เองจากชื่อ model + ชื่อช่อง · บท 124).addEventListener("input", …)= "ทุกครั้งที่มีการพิมพ์ ให้ทำ…" (ผูก listener · บท 134)(e) => console.log(…)= ฟังก์ชันสั้นแบบลูกศร (arrow function · บท 65) ·e= ข้อมูลของการพิมพ์ครั้งนั้น ·e.target.value.length= จำนวนตัวอักษรในช่อง
ไม่ต้องจำโค้ดนี้ — แค่จำหน้าตา "แบบที่ AI ชอบเขียน" ไว้ · บันทึกไฟล์แล้วรัน grep เดิม (ผลจริง):
$ grep -rn "<script" app/views
app/views/posts/show.html.erb:38:<script>
จับได้ ✔ · เลข 38 = บรรทัดที่เพิ่งเติม (ของคุณอาจต่างถ้าไฟล์ยาวไม่เท่า) ·
② ลบ 3 บรรทัดนั้นออก แล้วลบเมธอด disconnect() ทั้งก้อนออกจากไฟล์ตัวนับชั่วคราว:
$ grep -rn "disconnect" app/javascript
$
ไม่มีผล = จับได้ว่าหายไป ✔ · คืนของทั้งหมดด้วย git restore app แล้ว git status --short ต้องไม่เห็นไฟล์ใต้ app/ เลย
⚠️ ติดนิสัยคืนเฉพาะโฟลเดอร์ที่ทดลอง (git restore app) ไม่ใช่ git restore . — คำสั่งหลังคืนทุกไฟล์ที่แก้แต่ยังไม่ commit
รวมถึงงานจริงที่ยังไม่ได้เก็บ เช่น prompts/00-context.md ที่คุณจะเติมในขั้น 4 · และมันทิ้งโดยไม่ถามสักคำ
เครื่องตรวจควันที่เงียบมาทั้งปี อาจเพราะครัวปลอดภัย หรือเพราะถ่านหมด · วิธีเดียวที่จะรู้คือลองให้มีควัน (กดปุ่มทดสอบ) แล้วฟังว่ามันร้องไหม ·
ใส่ <script> ปลอม ๆ หรือลบ disconnect ชั่วคราว = ปล่อยควันให้ตัวตรวจได้ลองร้อง
ที่ไม่เหมือนกัน: เครื่องตรวจควันจับควันได้ทุกแบบ · grep จับได้แค่ตัวอักษรที่สั่งให้หาเป๊ะ ๆ
— AI ที่เขียน <SCRIPT> ตัวใหญ่ (เติม -i ให้ grep ไม่สนตัวเล็กตัวใหญ่ได้) หรือผูก listener ผ่าน library อื่น จะรอด grep ไปได้ · grep จึงเป็น "ด่านแรกที่เร็ว" ไม่ใช่ด่านสุดท้าย
- glob ·
*·** - glob = รูปแบบชื่อไฟล์ที่ shell ขยายให้ก่อนรันคำสั่ง ·
*= ชื่ออะไรก็ได้ในชั้นเดียว ·**/*.js= ทุกไฟล์ .js ทุกชั้น (ต้องมี/*.jsต่อท้าย) · พิมพ์บอก AI ได้ว่า "Use grep -rn on the folder instead of a ** glob — the glob didn't recurse in my shell" - false negative · sanity check
- false negative = ตัวตรวจบอกว่า "ไม่มีปัญหา" ทั้งที่มี · sanity check = ลองให้ตัวตรวจจับของที่รู้คำตอบอยู่แล้ว ก่อนเชื่อผลของมัน · ถาม senior ได้ว่า "check ตัวนี้เคยจับบั๊กได้จริงไหมครับ หรือผ่านมาตลอด"
ที่ AI พลาดประจำในบล็อกนี้ — อาการ · ตรวจยังไง · แก้ยังไง
สไลด์หน้า 53 รวมไว้ 6 ข้อ · ผมใส่บั๊กแต่ละแบบเข้าไปในโค้ดที่ทำงานถูกแล้วทีละข้อ แล้วดูอาการจริงในเบราว์เซอร์ · สังเกตว่าหลายข้อหน้าเว็บไม่โชว์ error อะไรเลย:
| AI ทำแบบนี้ | อาการที่เห็นจริง | ตรวจยังไง | แก้ยังไง |
|---|---|---|---|
frame id ไม่ตรงกันสองฝั่ง (ฝั่งหนึ่งพิมพ์ "post_title" เอง อีกฝั่งใช้ dom_id) |
กด "แก้ไขหัวข้อ" แล้วหัวข้อหายกลายเป็นคำว่า Content missing · Console มี error ตัวแดง (ข้อความอยู่ใต้ตาราง) | grep -rn "turbo_frame_tag" app/views → ทุกบรรทัดต้องเป็น dom_id(…) |
ใช้ dom_id(post, :title) ทั้งสองฝั่ง (บท 131) |
ลืม status: :unprocessable_content ตอน render ฟอร์มใหม่ |
กดบันทึกฟอร์มที่ข้อมูลผิด แล้วหน้าไม่เปลี่ยนเลย ไม่มีข้อความ error · log บอก Completed 200 OK · Console: Error: Form responses must redirect to another location |
grep -rn "render :new\|render :edit" app/controllers → ทุกบรรทัดต้องมี status: |
เติม status: :unprocessable_content (ทวนบท 132) |
เขียน JS แบบ querySelector ใน <script> แทน Stimulus |
ตอนเปิดหน้าใช้ได้ · แต่หลังส่งคอมเมนต์ครั้งแรก มันเงียบ (ฟอร์มใหม่ที่ Turbo Stream ใส่มาไม่มีใครผูก listener ให้) — ส่วนตัวนับ Stimulus ยังนับต่อ | grep -rn "<script" app/views |
ย้ายไปเป็น Stimulus controller (บท 134) |
| Stimulus controller ไม่มี disconnect | ส่วนใหญ่ไม่เห็นอาการทันที · listener ซ้อนกันเมื่อ element เดิมถูกถอดแล้วใส่ใหม่ หรือรั่วสะสมถ้าผูกกับ window / ตัวจับเวลา (บท 134) |
grep -rn "disconnect" app/javascript ต้องเจอในทุกไฟล์ที่มี addEventListener หรือ setInterval (ตัวจับเวลาที่ทำซ้ำทุก ๆ กี่วินาที) ใน connect |
เขียน disconnect() ที่ถอดของที่ connect() สร้าง |
ใช้ after_save แทน after_commit |
ส่วนใหญ่ใช้ได้ตอนลองคนเดียว · ปัญหาโผล่เมื่อ job ทำงานก่อนข้อมูลลงตู้จริง แล้วไปหยิบแฟ้มที่ยังไม่มี (บท 133 · ไม่ได้รันกรณีนี้ให้ดู) | grep -rn "after_" app/models → ทุกตัวที่ broadcast ต้องลงท้าย _commit |
ใช้ broadcasts_to (ข้างในเป็น after_commit อยู่แล้ว) หรือ after_create_commit |
ลืม turbo_stream_from ในหน้า show |
หน้าต่างที่ส่งเองเห็นคอมเมนต์ (จาก Turbo Stream ตอบฟอร์ม) · อีกหน้าต่างนิ่ง ตัวนับยัง 0 · Console ว่าง · log ยังเขียนว่า Broadcasting to … เหมือนปกติ |
ทดสอบสองหน้าต่าง · grep -rn "turbo_stream_from" app/views |
เติม <%= turbo_stream_from @post %> ในหน้า show (บท 133) |
อ่านคำสั่งในคอลัมน์ "ตรวจยังไง": \| ใน grep แปลว่า "หรือ" — "render :new\|render :edit" = หาบรรทัดที่มี render :new หรือ render :edit
ข้อความ error ของแถวแรก (ผลจริง · เลข title_post_2 คือบทความที่ผมลอง · ใน Console จะมีคำนำหน้าสั้น ๆ และบรรทัดชื่อไฟล์ turbo.min-….js ตามมาอีกหลายบรรทัด — อ่านแค่ประโยคนี้พอ):
The response (200) did not contain the expected <turbo-frame id="title_post_2"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.
แปลภาษาคน: "ครัวส่งจานกลับมา แต่ในจานไม่มีถ้วยที่ติดป้าย title_post_2 — พนักงานเลยไม่รู้จะเอาอะไรไปวาง" ·
นี่คือ "error เรื่อง matching turbo-frame" ที่เกณฑ์ข้อ 7 พูดถึง
และ log ของแถวที่สอง (ผลจริง · ตัดมาสองบรรทัด · ::1 = เครื่องตัวเอง แบบเดียวกับ localhost) — server ตอบ "200 สำเร็จ" ทั้งที่บันทึกไม่ได้:
Started POST "/posts" for ::1 at 2026-11-07 11:00:28 +0700
Completed 200 OK in 12ms (Views: 6.0ms | ActiveRecord: 2.2ms (1 query, 0 cached) | GC: 0.2ms)
ทำไมหน้าไม่เปลี่ยน: Turbo ถือกฎว่า "ส่งฟอร์มสำเร็จ (200) ต้องตามด้วยการพาไปหน้าอื่น (redirect)" · พอได้ 200 พร้อมหน้าฟอร์มเดิมกลับมา มันเลยไม่วาด แล้วบ่นใน Console ·
พอใส่ 422 ("ข้อมูลไม่ผ่าน") Turbo รู้ว่า "อ๋อ ให้วาดฟอร์มที่มี error" → ผู้ใช้เห็นข้อความ บันทึกไม่ได้ มี 3 จุดที่ต้องแก้ ตามปกติ (ผลจริงตอนมี status)
⚠️ แถวสุดท้ายน่ากลัวที่สุด — ไม่มีใครฟ้องเลย ทั้ง Console ทั้ง log · ถ้าตรวจด้วยหน้าต่างเดียวจะ "ผ่าน" ทุกข้อ · นี่คือเหตุผลที่สไลด์ยกเกณฑ์สองหน้าต่างเป็นเกณฑ์ที่ดีที่สุด
💬 อาการไม่ตรงกับตารางนี้? ส่งสามอย่างไปถาม senior หรือ AI: ข้อความใน Console ทั้งบรรทัด ·
log ของ request นั้น (จาก Started ถึง Completed) · และไฟล์ที่เกี่ยวกับเกณฑ์ข้อนั้น (คอลัมน์ "ไฟล์หลัก" ในตารางต้นบท)
บอกด้วยว่าเปิดกี่หน้าต่าง ทำอะไรในหน้าต่างไหน — ปัญหา real-time มักซ่อนอยู่ในคำตอบข้อนี้
ขั้น 4 — เติมกฎ 9–12 ลง 00-context.md: คู่มือโตจาก 8 ข้อเป็น 12 ข้อ
ทวนบท 121: prompts/00-context.md คือกฎถาวรของร้าน · และ AGENTS.md (ซึ่ง CLAUDE.md ชี้ไปหา) มีบรรทัด @prompts/00-context.md
→ Claude Code อ่านกฎทุกข้อเองทุกครั้งที่เปิดในโปรเจกต์นี้ · สไลด์หน้า 53 ให้เติม 4 ข้อจากบล็อก Hotwire
เปิด prompts/00-context.md · หาบรรทัด 8. strong parameters ไม่ใช่ authorization — แยกให้ออก ·
ใต้บรรทัดว่างที่ต่อจากข้อ 8 พิมพ์ก้อนนี้ แล้วเว้นบรรทัดว่างอีกหนึ่งบรรทัดก่อนหัวข้อ ## รูปแบบการตอบ:
## กฎเพิ่มจากบล็อค Hotwire
9. JS ทั้งหมดอยู่ใน Stimulus controller
ห้ามมี `<script>` ใน view
10. ทุก controller ที่สร้างของใน connect
ต้องเก็บกวาดใน disconnect
11. turbo-frame id ใช้ dom_id() ทั้งสองฝั่ง
12. render ใหม่หลัง validation ไม่ผ่าน
ต้องมี status: :unprocessable_content
บันทึกแล้วตรวจด้วย git diff prompts/00-context.md — ควรได้แบบนี้ (ผลจริง ตัดหัว 4 บรรทัดแรกออก) ·
บรรทัด @@ … @@ = git บอกว่าแก้ตรงบรรทัดที่เท่าไร · บรรทัดที่ขึ้นต้นด้วย + คือที่คุณเติม · บรรทัดที่ขึ้นต้นด้วยช่องว่างคือของเดิมรอบ ๆ:
@@ -28,6 +28,16 @@
7. migration ต้องไม่ล็อกตารางที่มีข้อมูลเยอะ — ใช้ expand-contract
8. strong parameters ไม่ใช่ authorization — แยกให้ออก
+## กฎเพิ่มจากบล็อค Hotwire
+
+9. JS ทั้งหมดอยู่ใน Stimulus controller
+ ห้ามมี `<script>` ใน view
+10. ทุก controller ที่สร้างของใน connect
+ ต้องเก็บกวาดใน disconnect
+11. turbo-frame id ใช้ dom_id() ทั้งสองฝั่ง
+12. render ใหม่หลัง validation ไม่ผ่าน
+ ต้องมี status: :unprocessable_content
+
## รูปแบบการตอบ
ถ้าคุณทำภารกิจท้ายบท 126 ไว้ (หัวข้อ "กฎที่ทีมเราเจอเอง" ท้ายไฟล์) ก็ไม่ชนกัน — อันนั้นอยู่ท้ายไฟล์ อันนี้อยู่ต่อจากข้อ 8 ·
ถ้อยคำตรงกับสไลด์ทุกคำ ต่างแค่ใส่ backtick รอบ <script> ให้เหมือนบรรทัด "ห้ามมี <script> ใน view" ที่มีอยู่แล้วในไฟล์
| กฎ | มาจากที่ AI พลาดข้อไหน | ตรวจด้วยคำสั่ง |
|---|---|---|
9 · JS อยู่ใน Stimulus · ห้าม <script> | querySelector ใน <script> | grep -rn "<script" app/views ต้องว่าง |
| 10 · สร้างของใน connect ต้องเก็บใน disconnect | ไม่มี disconnect | grep -rn "disconnect" app/javascript |
11 · turbo-frame id ใช้ dom_id() ทั้งสองฝั่ง | frame id ไม่ตรงกัน | grep -rn "turbo_frame_tag" app/views |
| 12 · render หลัง validation ไม่ผ่านต้องมี status | ลืม status | grep -rn "render :new\|render :edit" app/controllers |
นับดู: ไฟล์นี้มีกฎ 8 ข้อมาตั้งแต่เช้า (บล็อก A กับ B ของเราไม่ได้เพิ่มกฎลงไฟล์นี้ — บท 126 ให้เติมเป็นภารกิจเสริมแยกหัวข้อ) · ตอนนี้มี 12 ข้อ — ตรงกับที่สไลด์หน้า 54 เขียนว่า "00-context.md ที่โตจาก 8 กฎเป็น 12 กฎ" · ทุกข้อเกิดจากความผิดพลาดที่เคยเกิดจริง ไม่ได้คิดขึ้นลอย ๆ
ทวนภาพจากบท 121: CLAUDE.md คือป้ายหน้าห้องพนักงาน · 00-context.md คือแฟ้มกฎถาวรที่แขวนข้างป้าย · ร้านเปิดใหม่แฟ้มนี้มีกฎไม่กี่ข้อ · วันหนึ่งพนักงานใหม่ลืมปิดแก๊ส → เติมข้อ "ปิดแก๊สก่อนกลับ" · อีกวันเสิร์ฟผิดโต๊ะ → เติมข้อ "เช็คเลขโต๊ะสองครั้ง" · แฟ้มที่หนาขึ้นคือบันทึกบทเรียนของร้าน · พนักงานใหม่ (AI) ทุกกะอ่านป้ายและแฟ้มข้างป้ายก่อนเข้างาน จึงไม่ต้องพลาดเรื่องเดิมซ้ำ
ที่ไม่เหมือนกัน: พนักงานคนจริงทำผิดกฎแล้วโดนเตือน · AI อ่านกฎแล้วยังพลาดได้ โดยไม่มีใครจับได้ทันที — กฎใน context เป็นคำแนะนำ ไม่ใช่การบังคับ (บท 121) · คอลัมน์ "ตรวจด้วยคำสั่ง" ในตารางข้างบนจึงสำคัญเท่ากับตัวกฎ
- project memory · rules file
- ไฟล์ที่เครื่องมือ AI โหลดเข้า context ให้เองทุกครั้ง เช่น
CLAUDE.md/AGENTS.mdและไฟล์ที่มัน@ไปถึง · พิมพ์บอก AI ได้ว่า "Follow the rules in prompts/00-context.md, especially 9–12. Point out any rule you had to break and why." - guardrail
- กฎหรือการตรวจที่กันไม่ให้ผิดซ้ำ · กฎที่ดีบอกได้ว่า "ทำอะไรแทน" และตรวจได้ด้วยคำสั่ง · ถาม senior ได้ว่า "กฎข้อนี้ทีมมี check อัตโนมัติไหมครับ หรือต้องตรวจตอน review เอง"
ขั้น 5 — เก็บเข้าโปรเจกต์
โค้ดแอปของบล็อก C คุณ commit ไปแล้วทีละบท (บท 131–134) · ของใหม่ของบทนี้มีแค่สองไฟล์ใน prompts/ ·
ดูก่อนว่าจะเก็บอะไร (ผลจริง — ต้องมีแค่สองบรรทัดนี้):
$ git status --short
M prompts/00-context.md
?? prompts/03-hotwire.md
M = แก้ไฟล์เดิม (เติมกฎ 9–12) · ?? = ไฟล์ใหม่ที่ git ยังไม่รู้จัก ·
ถ้ามีไฟล์ใต้ app/ โผล่มาด้วย แปลว่ายังมีของทดลองค้างจากขั้น 2–3 (เช่น console.log ที่ลืมลบ) → git diff app ดูก่อน แล้ว git restore app
$ git add .
$ git commit -m "prompts: ใบสั่งงาน Hotwire + กฎ 9–12"
[my-day3 cac01ee] prompts: ใบสั่งงาน Hotwire + กฎ 9–12
2 files changed, 47 insertions(+)
create mode 100644 prompts/03-hotwire.md
create mode 100644 = git บอกว่าเป็นไฟล์ใหม่ (เลข 100644 คือ "ไฟล์ธรรมดา" ไม่ต้องสนใจ) ·
my-day3 = branch ที่สร้างไว้ในบท 120 · เลข cac01ee ของคุณจะต่าง · 47 insertions = 10 บรรทัดใน 00-context + 37 บรรทัดของไฟล์ใหม่ ·
สไลด์หน้า 53 ให้ commit ทั้งบล็อกเป็นก้อนเดียวด้วยข้อความ "hotwire: inline edit + live comments" — ของเราแบ่งเป็น 5 ก้อนตามบท จึงย้อนดูทีละขั้นได้ (ผลจริง):
$ git log --oneline -6
cac01ee prompts: ใบสั่งงาน Hotwire + กฎ 9–12
ddb4521 Stimulus ตัวนับอักษรช่องคอมเมนต์
184f5c7 hotwire: broadcast คอมเมนต์ทุกแท็บ
16cad7a Comment + ฟอร์มคอมเมนต์ตอบด้วย Turbo Stream
58001e9 แก้หัวข้อบทความในหน้าเดิมด้วย Turbo Frame
faebd6f styling: daisyUI + navbar + ปุ่มสลับธีม
อ่านจากล่างขึ้นบน = ลำดับที่ทำ · บรรทัดล่างสุดคือจบ Lab Vibe 2 (บท 129) · ข้อความ commit ของคุณต้องตรงกับที่แต่ละบทให้พิมพ์ (ถ้าเคย commit ภารกิจเสริมเพิ่ม จะมีบรรทัดแทรกอยู่ — ไม่เป็นไร)
✅ จบ Lab: โค้ดของคุณตอนนี้คือจุดเช็คพอยต์ C — จบ Day 3 ช่วงเช้า ·
สไลด์หน้า 53 บอกว่าตามไม่ทันให้ git checkout day3-c-done (branch สำเร็จรูปของอาจารย์) — ใช้ได้ แต่ไฟล์ข้างในต่างจากบทเรียนนี้มาก
(ใช้ระบบแปลภาษา t(".title") + gem rails-i18n · ชื่อ id ฟอร์มต่างกัน · กฎใน 00-context เลขข้อต่างกัน) · แนะนำให้ไล่บท 131–134 มากกว่า
ทางเลือก: ลอง vibe จริง — ให้ AI ทำบล็อก C ใหม่ทั้งหมด แล้วตรวจด้วย checklist ชุดนี้
ทำหลัง commit ข้างบนแล้ว · ไม่บังคับ · นี่คือสิ่งที่สไลด์ตั้งใจให้ทำใน Lab: ยิงใบสั่งงานให้ AI แล้วเราเป็นคนตรวจ · บทนี้ไม่โชว์ว่า AI ตอบอะไร (ของแต่ละคนจะต่างกันทุกครั้ง) — ให้คุณดูเองแล้วตรวจด้วยตาราง "AI พลาดประจำ"
git log --oneline -6→ หาเลข commit ของ Lab Vibe 2 (styling: daisyUI + navbar + ปุ่มสลับธีม— บรรทัดใต้แก้หัวข้อบทความในหน้าเดิมด้วย Turbo Frame) · ในตัวอย่างข้างบนคือfaebd6f(ของคุณจะเป็นเลขอื่น)git switch -c ai-vibe3 <เลข commit นั้น>— สร้าง branch ใหม่จากจุดจบบท 129 แล้วย้ายไปอยู่ (โค้ดใน editor จะไม่มีคอมเมนต์ ไม่มีตัวนับ)git checkout @{-1} -- prompts/— ก็อปโฟลเดอร์prompts/จาก branch ที่เพิ่งจากมา (@{-1}= "branch ก่อนหน้า") · AI จะได้ทั้งใบสั่ง03-hotwire.mdและกฎ 9–12- ปิด
bin/dev(Ctrl+C) แล้วbin/rails db:resetแล้วเปิดbin/devใหม่ —db:resetล้างตู้แล้วสร้างใหม่จากdb/schema.rbของ branch นี้ + seeds (คอมเมนต์ทดลองหายหมด) · ข้ามไม่ได้ เหตุผลในกล่องเตือนข้างล่าง - เปิด Claude Code (บท 86) แล้วสั่งประมาณ: "ทำตาม @prompts/03-hotwire.md ทีละเป้าหมาย · เสร็จแต่ละเป้าหมายให้บอกว่าแก้ไฟล์ไหน และบอกวิธีตรวจเกณฑ์ที่เกี่ยวข้อง"
(
00-context.mdถูกโหลดให้เองผ่านCLAUDE.mdไม่ต้อง@ซ้ำ) - ตรวจเองครบทั้ง 7 ข้อ + grep ทุกตัว — อย่าเชื่อที่ AI บอกว่าผ่าน · จดว่า AI พลาดข้อไหน
- กลับบ้าน:
git add . && git commit -m "ลอง AI บล็อก C"→git switch my-day3→ ปิดbin/devแล้วbin/rails db:resetอีกรอบ (ให้ตู้กลับมีตาราง comments แบบของเรา) แล้วเปิดbin/devใหม่
⚠️ ถ้าข้ามขั้น 4 (db:reset) ตู้ของคุณยังมีตาราง comments จากบท 132 อยู่ · พอ AI สร้าง migration ของมันแล้วสั่ง migrate จะพังแบบนี้ (ผลจริง ตัดมาบางบรรทัด):
bin/rails aborted!
StandardError: An error has occurred, this and all later migrations canceled: (StandardError)
PG::DuplicateTable: ERROR: relation "comments" already exists
"ตาราง comments มีอยู่แล้ว" — สลับ branch เปลี่ยนแค่ไฟล์ ไม่ได้เปลี่ยนของในตู้ (บท 120) · ถ้า AI เสนอให้ลบ migration หรือแก้ schema.rb เพื่อเลี่ยง error นี้ — ไม่ต้องตาม ตัวปัญหาคือตู้ ไม่ใช่โค้ด · ใช้ db:reset แทน
สรุป Day 3 ช่วงเช้า — เช้านี้ได้อะไรไปบ้าง
สไลด์หน้า 54 สรุปไว้ประโยคเดียว: ตอนเช้าเรามีหน้า scaffold เปล่า ๆ ตอนนี้เรามีเว็บที่ใช้งานได้จริงแล้ว
| บล็อก | หน้าเว็บได้อะไร | เข้าใจอะไร | ใบสั่งงาน |
|---|---|---|---|
| A · View layer (บท 120–126) | หน้า index + show ที่อ่านรู้เรื่อง · แบ่งหน้า · ทางกลับ | ERB · partial + locals ที่ใช้ซ้ำได้ · helper · XSS · N+1 ใน view · seed 30 บทความ 5 หมวดที่รันซ้ำได้ | prompts/01-views.md |
| B · CSS + assets (บท 127–129) | navbar + card ด้วย daisyUI · สลับธีมสว่าง/มืด | fingerprinting · Propshaft · importmap · Tailwind v4 · กับดักชื่อคลาสที่ต่อ string | prompts/02-styling.md |
| C · Hotwire (บท 130–135) | แก้หัวข้อในหน้าเดิม · คอมเมนต์เด้งทุกแท็บ · ตัวนับอักษร | Turbo Drive · Frame · Stream · broadcast · Stimulus connect/disconnect | prompts/03-hotwire.md |
ตอนนี้ prompts/ มี 4 ไฟล์ (ผลจริง · จำนวนบรรทัดของคุณอาจต่างเล็กน้อยถ้าเติมกฎเองไว้):
$ wc -l prompts/*
45 prompts/00-context.md
33 prompts/01-views.md
30 prompts/02-styling.md
37 prompts/03-hotwire.md
145 total
wc -l = นับจำนวนบรรทัดของแต่ละไฟล์ · 00-context.md = กฎถาวร 12 ข้อ · อีกสามไฟล์ = ใบสั่งงานของสามบล็อก ·
สไลด์บอกว่าช่วงบ่ายจะได้อีก 3 ไฟล์ และวันถัดไปอีก 2 ไฟล์ ก่อนเอามารวมกัน
และประโยคหลัก 3 ข้อของวัน (สไลด์หน้า 8) — เช้านี้เจอไปแล้วในรูปไหน:
- ① เว็บ interactive ไม่จำเป็นต้องมี state สองชุด — ทั้งบล็อก A และ C: server เป็นเจ้าของข้อมูลชุดเดียว ส่ง HTML มาให้ (บท 122 · 130) · ตัวนับคอมเมนต์ที่ตรงกันทุกแท็บในขั้น 2 คือหลักฐาน — ไม่มีสมุดจดเล่มที่สองบนโต๊ะให้คลาดกัน
- ② exactly-once ไม่มีอยู่จริง ออกแบบให้ทำซ้ำได้แทน — เจอแค่รูปเล็ก: seeds ที่ใช้
find_or_create_by!รันกี่รอบก็ได้ 30 บทความ (บท 120) · เรื่องจริงจังอยู่บล็อก E - ③ อย่า invalidate cache ให้ key เปลี่ยนแทน — เจอในรูป fingerprinting: แก้ CSS แล้วชื่อไฟล์เปลี่ยน เบราว์เซอร์จึงหยิบเล่มใหม่เอง (บท 127) · เรื่องจริงจังอยู่บล็อก F
ควรเข้าใจอะไรแล้วบ้าง — เช็คตัวเองทีละบท
ลองตอบในใจทีละข้อ · ข้อไหนตอบไม่ได้ กลับไปอ่านบทนั้นก่อนเข้าช่วงบ่าย (ไม่ต้องจำโค้ด แค่อธิบายเป็นภาษาคนได้ก็พอ)
| บท | ถ้าตอบข้อนี้ได้ = เข้าใจแล้ว |
|---|---|
| 120 | ทำไม seeds ต้องรันซ้ำได้ และทำไมต้องมีข้อมูลถึง 30 แถว |
| 121 | prompt 4 ส่วนมีอะไร · CLAUDE.md กับ 00-context.md ต่างกันยังไง · ทำไมกฎใน context "ไม่ใช่การบังคับ" |
| 122 | ทำไม Rails ไม่ต้องมีชั้น API · <%= %> กับ <% %> ต่างกันยังไง และลืม = แล้วเกิดอะไร |
| 123 | partial ที่ใช้ locals ต่างจาก partial ที่แตะ @ตัวแปร ยังไง · content_for ส่งของไปไหน |
| 124 | form_with model: รู้ได้ยังไงว่าต้อง POST หรือ PATCH · ของฟรีที่มันให้มีอะไรบ้าง |
| 125 | ทำไม raw / html_safe อันตราย · นับ query ของหน้าหนึ่งจาก log ยังไง |
| 126 | ตรวจรับงานด้วย checklist ยังไง · ทำไมต้องใส่บั๊กกลับก่อนเชื่อตัวตรวจ |
| 127 | fingerprinting แก้ปัญหา cache ยังไง · importmap ทำให้ไม่ต้องมี node_modules ได้ยังไง |
| 128 | ทำไมชื่อคลาส Tailwind ที่ต่อ string ตอนรันถึงไม่มีสี · semantic color ของ daisyUI ดีกว่าชื่อสีตรง ๆ ตรงไหน |
| 129 | ธีมที่เลือกถูกจำไว้ที่ไหน · ทำไมต้องลอง assets:precompile ก่อนส่งงาน |
| 130 | Turbo Drive ทำอะไรตอนกดลิงก์ · ทำไม JS ที่รอ DOMContentLoaded ทำงานแค่ครั้งแรก |
| 131 | ทำไม id ของกรอบต้องตรงกันสองฝั่ง · เปิด /posts/1/edit ตรง ๆ แล้วทำไมยังได้หน้าเต็ม |
| 132 | Frame กับ Stream ต่างกันยังไง · ทำไมตอนข้อมูลไม่ผ่านต้องตอบ 422 |
| 133 | broadcast ไปถึงอีกแท็บได้ยังไง · ทำไมต้อง after_commit · Turbo ไม่เหมาะกับงานแบบไหน |
| 134 | ชื่อไฟล์ controller ผูกกับ data-controller ยังไง · connect / disconnect ถูกเรียกตอนไหน |
ต่อจากนี้ — ทำให้มันเป็นผลิตภัณฑ์
ช่วงบ่ายของ Day 3 (สไลด์ชุดถัดไป · เว็บนี้จัดไว้เป็น Rails Day 4 · Part 1) มีอีกสามบล็อก:
- D · rich text + รูปภาพ จาก MinIO — บทความที่มีตัวหนา รูปแทรก · รูปไปเก็บใน MinIO (กล่องเก็บไฟล์ที่
docker compose up -dเปิดไว้ให้ตั้งแต่บท 120 แต่ยังไม่ได้ใช้) → บท 136–141 - E · ตั้งเวลาโพสต์ด้วย background job — "ให้บทความนี้ขึ้นพรุ่งนี้ 8 โมง" · งานที่ทำทีหลังโดยไม่ให้ผู้ใช้รอ (ประโยค ② ของวันจะจริงจังที่นี่) → บท 142–145
- F · ทำให้มันเร็วด้วย cache — จำหน้าที่วาดแล้วไว้ใช้ซ้ำ (ประโยค ③ ของวัน) · ยังไม่ได้ทำเป็นบทเรียน
เรื่องระบบล็อกอินกับการเขียน test อยู่ Day 4 · เช้านี้ช่องชื่อผู้เขียนคอมเมนต์จึงยังพิมพ์เองได้ (ข้อกำหนดเขียนไว้ว่า "ยังไม่มีระบบล็อกอิน")
ภารกิจ: รัน checklist ทั้งหมดกับโปรเจกต์ของคุณเอง
🎯 ภารกิจ Lab Vibe 3 (~10 นาที)
git status --shortต้องว่าง (commit ขั้น 5 แล้ว) · แท็บ terminal ที่ 1 รันbin/dev- เปิด Chrome สองหน้าต่างวางข้างกัน ที่
http://localhost:3000/posts/2(ลองบทความอื่นนอกจาก 1) · เปิด Console ทั้งสองหน้าต่าง - ทำเกณฑ์ข้อ 1–4 ในตารางขั้น 2: แก้หัวข้อ → ส่งคอมเมนต์จากหน้าต่างซ้าย → ดูหน้าต่างขวา
- เปิดแท็บที่สาม พิมพ์
localhost:3000/posts/2/editตรง ๆ (ข้อ 6) - รันใน terminal แท็บที่ 2 ทีละบรรทัด:
grep -rn "<script" app/views grep -rn "disconnect" app/javascript grep -rn "turbo_frame_tag" app/views grep -rn "render :new\|render :edit" app/controllers grep -rn "after_" app/models - ติ๊กเกณฑ์ทั้ง 7 ข้อใน
prompts/03-hotwire.mdของคุณเอง (เปลี่ยน[ ]เป็น[x]) ถ้าผ่านจริง — แล้วgit restore prompts/03-hotwire.mdให้ใบสั่งกลับเป็นช่องว่างไว้ใช้ครั้งหน้า
ควรเห็น: คอมเมนต์โผล่ในหน้าต่างขวาเองภายในเสี้ยววินาที · ตัวนับ "ความคิดเห็น (…)" เพิ่มพร้อมกันทั้งสองฝั่ง · ฟอร์มซ้ายว่างและตัวนับอักษรเป็น 0 / 1000 ·
Console ไม่มีตัวแดง · grep บรรทัดแรกไม่มีผล · บรรทัดที่สองเจอ 1 บรรทัด · บรรทัดที่สามเจอ 2 บรรทัดที่เป็น dom_id ทั้งคู่
ถ้าเจออาการนี้ แปลว่า…
- หน้าต่างขวาไม่ขยับ แต่รีเฟรชแล้วเห็นคอมเมนต์ → ลำโพงไม่ได้เปิด: ไม่มี
<%= turbo_stream_from @post %>ในหน้า show · หรือคุณเปิดหน้าต่างขวาไว้ที่บทความอื่น (เสียงตามสายประกาศถึงเฉพาะโต๊ะที่สั่งเมนูเดียวกัน) - คอมเมนต์ขึ้นในหน้าต่างซ้ายสองครั้ง → partial
_commentไม่มีid="<%= dom_id comment %>"· Turbo ใช้ id นี้รู้ว่าคอมเมนต์ที่มาทางเสียงตามสายคือตัวเดียวกับที่ใส่ไปแล้ว (บท 133) · ผลจริงตอนลบ id ออก: หน้าต่างที่ส่งมีคอมเมนต์ 2 อัน แต่ตัวนับบอก(1) - กรอบหัวข้อขึ้นคำว่า Content missing → id สองฝั่งไม่ตรง หรือชื่อไฟล์
edit.html+turbo_frame.erbพิมพ์ผิด — ผลจริงตอนตั้งชื่อเป็น+turboframe: Rails ไม่ฟ้อง แต่ไปใช้edit.html.erbที่ไม่มีกรอบแทน → Content missing + error ใน Console แบบเดียวกับตาราง "AI พลาดประจำ" - ตัวนับอักษรไม่ขึ้นเลย (ไม่มีแม้แต่
0 / 1000) → ชื่อไม่ตรงกันระหว่างไฟล์character_counter_controller.jsกับdata-controller="character-counter"— Stimulus ไม่ฟ้องอะไรถ้าหา controller ไม่เจอ · ผลจริงตอนพิมพ์เป็นcharacter_counter(ขีดล่าง): ช่องตัวนับว่าง Console ก็ว่าง (บท 134)
ผลจริงของคำสั่ง grep ทั้ง 5 บรรทัดบนโค้ดจุดเช็คพอยต์ C:
$ grep -rn "<script" app/views
$ grep -rn "disconnect" app/javascript
app/javascript/controllers/character_counter_controller.js:15: disconnect() {
$ grep -rn "turbo_frame_tag" app/views
app/views/posts/edit.html+turbo_frame.erb:2:<%= turbo_frame_tag dom_id(@post, :title) do %>
app/views/posts/_post.html.erb:11: <%= turbo_frame_tag dom_id(post, :title) do %>
$ grep -rn "render :new\|render :edit" app/controllers
app/controllers/categories_controller.rb:31: format.html { render :new, status: :unprocessable_content }
app/controllers/categories_controller.rb:44: format.html { render :edit, status: :unprocessable_content }
app/controllers/posts_controller.rb:34: format.html { render :new, status: :unprocessable_content }
app/controllers/posts_controller.rb:47: format.html { render :edit, status: :unprocessable_content }
$ grep -rn "after_" app/models
app/models/comment.rb:7: after_create_commit -> { broadcast_update_later_to post, target: "comment_count", html: post.comments.count }
- บรรทัดแรกไม่มีผล = ไม่มี
<script>ใน view (กฎ 9) · บรรทัดที่สอง = ตัวนับมี disconnect (กฎ 10) - สองฝั่งของกรอบใช้
dom_id(…, :title)เหมือนกัน (กฎ 11) · ฝั่งหนึ่งเป็น@post(ในหน้า edit) อีกฝั่งเป็นpost(local ใน partial) แต่ได้ id เดียวกันtitle_post_2 - ทุก
render :new/render :editมี status (กฎ 12) · ของ categories มาจาก scaffold ตั้งแต่ต้น · ฟอร์มคอมเมนต์ (บท 132) ใช้render turbo_stream: …แยกบรรทัด จึงไม่ติด grep นี้ — เปิดcomments_controller.rbดูเองว่ามีstatus: :unprocessable_contentทั้งสองที่ - บรรทัดสุดท้ายคือบรรทัดของบท 133 ที่ส่งตัวนับใหม่ไปทุกแท็บ · ชื่อลงท้าย
_commit·broadcasts_to :postไม่ขึ้นใน grep นี้เพราะไม่มีคำว่าafter_แต่ข้างในใช้ after_commit อยู่แล้ว (บท 133) - ใน Chrome ของผม: คอมเมนต์ถึงหน้าต่างขวาใน 104 ms · ทั้งสองฝั่งได้
(1)· ฟอร์มซ้ายว่าง · Console ว่างทุกหน้าต่าง · หน้า edit ตรง ๆ มี navbar + ฟอร์ม 3 ช่อง
Quiz
ข้อ 1 · (จากสไลด์หน้า 51) สถานการณ์ A: "เราเพิ่มคอมเมนต์ แล้วหน้าเราต้องอัปเดต 3 จุด" · สถานการณ์ B: "คนอื่นเพิ่มคอมเมนต์ แล้วหน้าเราขึ้นเอง" · ทั้งคู่ใช้ Turbo Stream — ต่างกันตรงไหน?
ตัวเลือกที่ 3 ถูก · A = Turbo Stream ตอบกลับฟอร์ม (create.turbo_stream.erb · บท 132) ส่งกลับไปแค่หน้าต่างที่กดส่ง · B = Turbo Stream broadcast (broadcasts_to + turbo_stream_from · บท 133) ส่งถึงทุกหน้าต่างที่เปิดลำโพงไว้
ตัวเลือกที่ 1 ผิด — lazy frame คือ "โหลดกรอบทีหลัง" ไม่ได้อัปเดตหลายจุด · ตัวเลือกที่ 2 กลับด้านกัน ·
ตัวเลือกที่ 4 ผิด — หน้าต่างของคนอื่นไม่ได้ส่งฟอร์ม จึงไม่มีใครได้คำตอบจาก create.turbo_stream.erb · ผลจริงในตาราง "AI พลาดประจำ": ลบ turbo_stream_from แล้วหน้าที่ส่งเองยังอัปเดต แต่อีกหน้าต่างนิ่ง — แปลว่าเป็นสองทางแยกกัน
จุดที่ต้องจำ: ถามตัวเองว่า "ใครเป็นคนขอ" — เราขอเอง = ตอบฟอร์ม · เราไม่ได้ขอแต่ต้องรู้ = broadcast
ข้อ 2 · AI ส่งงานมาบอก "ทำ disconnect แล้ว" · คุณรัน grep -n "disconnect" app/javascript/** ได้แค่ grep: app/javascript/controllers: Is a directory · ควรสรุปยังไง?
ตัวเลือกที่ 2 ถูก · ผลจริงในขั้น 3: คำสั่งแบบ ** ไม่เข้าไปในโฟลเดอร์ · grep -rn "disconnect" app/javascript เจอ character_counter_controller.js:15 ทันที
ตัวเลือกที่ 1 สรุปเกินหลักฐาน — ตัวตรวจพังเอง ไม่ใช่งานพัง · ตัวเลือกที่ 3 กับ 4 เชื่อผลของตัวตรวจที่ไม่ได้ทำงานจริง · ข้อความ "Is a directory" คือ grep บ่นว่าข้าม ไม่ใช่ "เจอ"
จุดที่ต้องจำ: ก่อนเชื่อผลของตัวตรวจ ลองให้มันจับของที่รู้คำตอบอยู่แล้วก่อน
ข้อ 3 · กรอกฟอร์มบทความใหม่แบบเว้นว่างแล้วกดบันทึก · หน้าไม่เปลี่ยน ไม่มีข้อความ error · log เขียน Completed 200 OK · Console มี Form responses must redirect to another location · น่าจะผิดตรงไหน?
ตัวเลือกที่ 1 ถูก · ผลจริงในตาราง "AI พลาดประจำ": ลบ status ออกบรรทัดเดียว ได้อาการนี้ครบทุกข้อ · ใส่กลับแล้วเห็น บันทึกไม่ได้ มี 3 จุดที่ต้องแก้
ตัวเลือกที่ 2 ผิด — ถ้าบันทึกผ่านจริง controller จะ redirect ไปหน้าบทความ ไม่ใช่ 200 ค้างที่เดิม ·
ตัวเลือกที่ 3 ผิด — ฟอร์มบทความใหม่ไม่ได้อยู่ในกรอบ และอาการกรอบไม่ตรงคือ Content missing ·
ตัวเลือกที่ 4 ผิด — turbo_stream_from เกี่ยวกับเสียงตามสายจากคนอื่น ไม่เกี่ยวกับคำตอบของฟอร์มที่เราส่ง
จุดที่ต้องจำ: ข้อมูลไม่ผ่านต้องตอบ 422 · "200 แต่หน้าไม่ขยับ" = กลิ่นของ status ที่หายไป
ข้อ 4 · ทำไมสไลด์ถึงบอกว่าเกณฑ์ "เปิด 2 แท็บ พิมพ์แท็บหนึ่ง อีกแท็บขึ้นเอง" เป็นเกณฑ์แบบที่ดีที่สุด?
ตัวเลือกที่ 4 ถูก · คำว่า "ทำครบแล้ว" ในคำตอบของ AI ทำให้คอมเมนต์เด้งในหน้าต่างของคุณไม่ได้ · ต้องมีโค้ดที่ทำงานจริงเท่านั้น (สไลด์หน้า 52)
ตัวเลือกที่ 1 ผิด — ข้อนี้ต้องเปิดเบราว์เซอร์ grep ตรวจไม่ได้ · ตัวเลือกที่ 2 ผิด — ผ่านข้อนี้แต่ลืม status (ข้อ 3 ของ quiz) ก็ได้ · ตัวเลือกที่ 3 กลับด้าน — ถ้าให้ AI ตรวจเองแล้วเราแค่อ่านรายงาน ก็กลับไปเป็น "อ่านสูตร" ไม่ใช่ "ชิม"
จุดที่ต้องจำ: เกณฑ์ที่ดีคือเกณฑ์ที่เรารันเองแล้วเห็นผลกับตา — เขียนแบบนี้ไว้ในทุกใบสั่งงาน
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 53 | grep -n "disconnect" app/javascript/** | ได้แค่ grep: app/javascript/controllers: Is a directory ไม่ค้นในโฟลเดอร์ · ใช้ grep -rn "disconnect" app/javascript | สนใจ — ขั้น 3 |
| 52 | broadcasts_to :post พร้อม later: true | turbo-rails 2.0.23 ไม่มีตัวเลือก later: · ส่งต่อให้ job เองอยู่แล้ว · ใบสั่งของเราแก้บรรทัดนี้ | สนใจ — ขั้น 1 |
| 53 | "ลืม status: :unprocessable_entity" (กฎ 12 ในหน้าเดียวกันเขียน _content) | scaffold ของ Rails 8.1 ใช้ :unprocessable_content · เราใช้ตัวนี้ทั้งโปรเจกต์ | แค่รู้ไว้ |
| 52 | "ใช้ dom_id(post) ตั้ง id ทั้งสองฝั่ง" | ในโปรเจกต์เรา dom_id(post) ถูก <article> ใช้ไปแล้ว จึงใช้ dom_id(post, :title) (บท 131) · หลักการเดียวกัน | แค่รู้ไว้ |
| 53 | ตามไม่ทัน git checkout day3-c-done | branch ของอาจารย์ใช้ i18n + rails-i18n · id ฟอร์มและเลขกฎใน 00-context ต่างกัน ไฟล์ไม่ตรงกับบทเรียน | แค่รู้ไว้ |
| 54 | "บ่ายจะได้อีก 3 ไฟล์ แล้ววันอังคารอีก 2 ไฟล์" | อ้างตามสไลด์ · ยังไม่มีสไลด์ชุดนั้นในเว็บนี้ | แค่รู้ไว้ |
อ้างอิง
- Turbo Handbook — Redirecting After a Form Submission — อ่านสองย่อหน้าแรก: Turbo คาดว่าส่งฟอร์มแล้วต้อง redirect · ยกเว้นตอบ 4xx/5xx เช่น 422 เพื่อวาดฟอร์มที่มี error (ที่มาของกฎข้อ 12)
- Turbo Handbook — Decompose with Turbo Frames — อ่านสองย่อหน้าแรกของหน้า — Turbo ใช้ id ของกรอบจับคู่ว่าจะเอาส่วนไหนของคำตอบมาแทน (ที่มาของ error "did not contain the expected <turbo-frame>")
- Stimulus Reference — Lifecycle Callbacks: Disconnection —
รายการเหตุการณ์ที่ทำให้
disconnect()ถูกเรียก (เช่น element ถูกเอาออกจากหน้า) ตรงกับที่เห็นใน Console ตอนฟอร์มถูกแทน - Rails Guides — Active Record Callbacks: Transaction Callbacks — อ่านย่อหน้าต้นหัวข้อ ว่าทำไมงานที่ต้องคุยกับระบบภายนอก (เช่น ส่ง broadcast) ควรรอหลัง commit
- Claude Code Docs — Memory: Import additional files —
วิธีที่บรรทัด
@prompts/00-context.mdในAGENTS.mdดึงกฎทั้ง 12 ข้อเข้ามาให้ Claude Code อ่านทุกครั้ง - repo ของอาจารย์ — wongnok-blog-all-in-one —
เลือก branch
day3-c-done(ปุ่มเลือก branch มุมซ้ายบน) ถ้าอยากเทียบกับเฉลยของอาจารย์ · จำไว้ว่าไฟล์ต่างจากบทเรียนนี้