บทเรียน 131 · Rails · Day 3

Turbo Frames — แก้หัวข้อบทความในหน้าเดิม ไม่ต้องเปลี่ยนหน้า

~20 นาทีอ่าน + ลงมือทำตามอีก ~20 นาที · ควรผ่าน บท 130 (Hotwire · Turbo Drive) · บท 123 (partial) · บท 124 (form_with) มาก่อน

ตอนนี้ถ้าอยากแก้หัวข้อบทความคำเดียว ต้องกด "แก้ไข" แล้วย้ายไปอีกหน้าที่มีฟอร์มสามช่อง · บทนี้จะทำให้กดปุ่มข้างหัวข้อ แล้วหัวข้อกลายเป็นช่องพิมพ์ตรงนั้นเลย บันทึกแล้วกลับเป็นหัวข้อใหม่ — หน้าไม่เปลี่ยน URL ไม่เปลี่ยน และไม่ต้องเขียน JavaScript สักบรรทัด
อ่านจบแล้วคุณจะทำ "กรอบที่เปลี่ยนตัวเองได้" (Turbo Frame) เป็น · อ่านอาการเวลามันพังออก · และจำกฎข้อเดียวของมันได้: id ของกรอบต้องตรงกันสองฝั่ง

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

🍜 เริ่มจากโค้ดสถานะไหน: โปรเจกต์ wongnok-blog-all-in-one ตอนจบ Lab บท 129 (มี daisyUI · navbar · ปุ่มสลับธีมแล้ว) · ของที่ทดลองในบท 130 ต้อง git restore ไปแล้ว · บทนี้แก้ 2 ไฟล์ + สร้าง 1 ไฟล์ แล้ว commit — บท 132 จะต่อจากตรงนี้ (ตามไม่ทัน: ดูวิธีเปิดโปรเจกต์ในบท 120 แล้วทำ Lab บท 126 · 129 ตามลำดับ)

🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 18 · turbo-rails 2.0.23 · ทดสอบในเบราว์เซอร์ด้วย Chrome จริง (สไลด์กับ repo ของอาจารย์เขียน Ruby 4.0.5 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน)

ผลรันทุกก้อนรันจริงแล้วก็อปมา · ตัวอย่างใช้บทความเลขที่ 30 ("นมเย็น" — ใบแรกบนหน้ารวม) · เลข id · เวลา ms · วันเวลาใน log · รหัสกัน CSRF ยาว ๆ ของคุณจะต่าง · path ยาว ๆ ของ gem ย่อเป็น …

ตอนนี้: แก้หัวข้อคำเดียว ต้องย้ายไปอีกหน้า

เปิด http://localhost:3000/posts/30 · ใต้บทความมีปุ่ม "แก้ไข" · กดแล้วเบราว์เซอร์ไป /posts/30/edit ซึ่งเป็นอีกหน้าหนึ่ง มีฟอร์มสามช่อง (หัวข้อ · เนื้อหา · หมวด) · แก้เสร็จกดบันทึก ถูกส่งกลับมาหน้าเดิม

ใช้ได้ แต่ถ้าแค่อยากแก้คำผิดในหัวข้อ มันอ้อมเกินไป · ที่เราอยากได้คือแก้ตรงที่มันอยู่ — กดปุ่มข้างหัวข้อ → หัวข้อกลายเป็นช่องพิมพ์ → กดบันทึก → กลับเป็นหัวข้อใหม่ · ส่วนอื่นของหน้าอยู่เฉย ๆ

ของที่ทำแบบนี้ได้ชื่อ Turbo Frame — ชิ้นที่สองของ Hotwire (ชิ้นแรกคือ Turbo Drive บท 130) · เราเอากล่องพิเศษชื่อ <turbo-frame> ไปครอบส่วนที่อยากให้เปลี่ยนได้ · ลิงก์หรือฟอร์มที่อยู่ข้างในกล่อง เมื่อกดแล้ว Turbo จะเปลี่ยนแค่ข้างในกล่องนั้น ไม่แตะส่วนอื่นของหน้าเลย

🥣 เทียบให้เห็นภาพ — เปลี่ยนแค่ถ้วยใบเดียวบนโต๊ะ

Turbo Drive (บท 130) เหมือนพนักงานเปลี่ยนจานทั้งชุดบนโต๊ะเดิม · Turbo Frame ละเอียดกว่านั้น: ลูกค้าขอเปลี่ยนน้ำจิ้มถ้วยเดียว · พนักงานเดินไปครัว รับถาดที่ครัวจัดมา แล้วหยิบเฉพาะถ้วยที่ป้ายเลขตรงกับถ้วยบนโต๊ะมาวางแทน · ข้าว กับข้าว แก้วน้ำ อยู่ที่เดิมหมด

ที่ไม่เหมือนกัน: ร้านจริงคงไม่มีใครจัดอาหารทั้งถาดเพื่อเปลี่ยนน้ำจิ้มถ้วยเดียว · แต่ Turbo ขอหน้าเว็บมาทั้งหน้าจริง ๆ แล้วทิ้งส่วนที่เหลือ — เดี๋ยวจะเห็นว่า Rails ช่วยให้ "ถาด" นั้นบางลงได้

เทียบกับ React (บท 73): ถ้าทำใน React คุณจะเขียน const [editing, setEditing] = useState(false) สลับการ์ดเป็นช่องพิมพ์ · ยิง fetch (คำสั่งของ JavaScript ที่ส่งคำขอไปหา server) ส่ง JSON ไป · แล้วเก็บหัวข้อใหม่ไว้ใน state ของเบราว์เซอร์อีกชุด · Turbo Frame ไม่มี state ชุดที่สองแบบนั้น — server ส่ง HTML ของกล่องมาให้ทั้งก้อน เบราว์เซอร์แค่เอามาวาง

🗣 ศัพท์ที่เพิ่งเรียนไป
Turbo Frame · <turbo-frame>
กล่อง HTML ที่ Turbo ดูแล · ลิงก์และฟอร์มข้างในกล่องจะโหลดผลมาใส่แค่ในกล่องนั้น · พิมพ์บอก AI ได้ว่า “wrap the post title in a turbo frame so the edit link loads inside it”
inline edit
แก้ของ "ในที่ของมัน" แทนที่จะพาไปหน้าแก้ไขแยก · เป็นงานที่ Turbo Frame เหมาะที่สุด · ถาม senior ได้ว่า “ตรงนี้ทำ inline edit ด้วย Turbo Frame ได้ไหม หรือต้องเปลี่ยนหลายจุดจนต้องใช้ Stream”

Turbo Frame ทำงานยังไง — 5 ขั้น

ก่อนพิมพ์โค้ด ดูภาพรวมก่อน (สไลด์หน้า 42 วาดไว้แบบเดียวกัน) · สมมติหน้า show มีกล่อง id title_post_30 ครอบหัวข้อกับปุ่ม "แก้ไขหัวข้อ":

หน้าที่ผู้ใช้เปิดอยู่ (/posts/30) navbar ┌─ turbo-frame id="title_post_30" ───────── │ นมเย็น [แก้ไขหัวข้อ] └────────────────────────────────────────── เนื้อหา · ปุ่ม "แก้ไข" "ลบบทความ" ข้างล่าง ① กด "แก้ไขหัวข้อ" — ลิงก์นี้อยู่ในกล่อง ② Turbo ไปขอ GET /posts/30/edit ③ คำตอบกลับมา → Turbo หากล่องที่ id = "title_post_30" ในคำตอบ ④ เอาเฉพาะข้างในกล่องนั้นมาวางแทนของเดิม → [ช่องพิมพ์] [บันทึก] [ยกเลิก] ⑤ navbar · เนื้อหา · ปุ่มข้างล่าง ไม่ขยับ · URL ยังเป็น /posts/30

ขั้น ③ คือหัวใจ (id = ชื่อประจำตัวของแท็ก HTML หนึ่งตัว ห้ามซ้ำในหน้าเดียวกัน) · Turbo ไม่ได้เดาว่าจะเอาส่วนไหนของคำตอบ · มันหากล่องที่ id เหมือนกับกล่องที่ลิงก์อยู่ · เจอ = เอาข้างในมาวาง · ไม่เจอ = พัง · นี่คือกฎข้อเดียวของ Frames: id ต้องตรงกันสองฝั่ง (เดี๋ยวเราจะทำให้พังจริงให้ดู)

งานของเราจึงมี 2 ฝั่ง:

ขั้นที่ 1 — ครอบหัวข้อด้วยกล่อง (_post.html.erb)

เปิด bin/dev ทิ้งไว้ก่อน (บท 128 — มันคอยสร้าง CSS ใหม่ทุกครั้งที่เราใช้คลาสใหม่ · บทนี้มีคลาสที่แอปยังไม่เคยใช้ เช่น input-lg)

เปิด app/views/posts/_post.html.erb · ในก้อน <% if full %> (ส่วนที่ใช้ในหน้า show) มีหัวข้ออยู่บรรทัดเดียว:

    <% if full %>
      <h1 class="card-title text-3xl"><%= post.title %></h1>

เปลี่ยนบรรทัด <h1> นั้นเป็นแบบนี้ (บรรทัด if full กับบรรทัด <p> ข้างล่างไม่ต้องแตะ):

    <% if full %>
      <%# แก้หัวข้อในหน้าเดิม: id ต้องตรงกับ posts/edit.html+turbo_frame.erb %>
      <%= turbo_frame_tag dom_id(post, :title) do %>
        <div class="flex items-start justify-between gap-2">
          <h1 class="card-title text-3xl"><%= post.title %></h1>
          <%= link_to "แก้ไขหัวข้อ", edit_post_path(post), class: "btn btn-ghost btn-sm shrink-0" %>
        </div>
      <% end %>
      <p class="leading-relaxed whitespace-pre-line"><%= post.description %></p>

ลองดูว่า dom_id ให้อะไร · เปิด console ด้วย bin/rails console แล้วพิมพ์ตาม (helper. ข้างหน้า = ขอยืม helper ของ view มาใช้ใน console · ผลจริง):

wongnok-blog-all-in-one(dev):001> helper.dom_id(Post.find(30))
  Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 30 LIMIT 1 /*application='WongnokBlogAllInOne'*/
=> "post_30"
wongnok-blog-all-in-one(dev):002> helper.dom_id(Post.find(30), :title)
  Post Load (1.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 30 LIMIT 1 /*application='WongnokBlogAllInOne'*/
=> "title_post_30"
wongnok-blog-all-in-one(dev):003> helper.dom_id(Post.find(29), :title)
  Post Load (2.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 29 LIMIT 1 /*application='WongnokBlogAllInOne'*/
=> "title_post_29"

ทำไมต้องเติม :title? เพราะ <article> ที่ครอบบทความทั้งชิ้นใช้ dom_id post = post_30 ไปแล้วตั้งแต่ Lab บท 126 · id ในหน้าเว็บหนึ่งหน้าห้ามซ้ำกัน (เหมือนเลขที่บ้าน) · กล่องหัวข้อจึงต้องมีชื่อของตัวเอง

ในภาพร้านอาหาร dom_id คือเครื่องพิมพ์ป้ายเลขถ้วย · ใส่บทความเดียวกัน + คำเติมเดียวกัน ได้ป้ายเดียวกันทุกครั้ง · เดี๋ยวขั้นที่ 4 เราจะใช้เครื่องพิมพ์ตัวเดียวกันนี้พิมพ์ป้ายให้ถ้วยฝั่งครัว ป้ายสองฝั่งจึงตรงกันเอง

เปิดหน้า /posts/30 ด้วย curl ดู HTML ที่เบราว์เซอร์ได้รับจริง (ตัดมาเฉพาะช่วงบทความ):

$ curl -s localhost:3000/posts/30
…
--><article id="post_30" class="card bg-base-100 shadow-sm">
…
      <turbo-frame id="title_post_30">
        <div class="flex items-start justify-between gap-2">
          <h1 class="card-title text-3xl">นมเย็น</h1>
          <a class="btn btn-ghost btn-sm shrink-0" href="/posts/30/edit">แก้ไขหัวข้อ</a>
        </div>
</turbo-frame>      <p class="leading-relaxed whitespace-pre-line">นมสีชมพูจากน้ำแดง หวานหอม เด็ก ๆ ชอบที่สุด เติมไข่มุกได้</p>
  </div>
</article>
🗣 ศัพท์ที่เพิ่งเรียนไป
turbo_frame_tag
helper ของ turbo-rails ที่เขียน <turbo-frame id="…">…</turbo-frame> ให้ · พิมพ์บอก AI ได้ว่า “use turbo_frame_tag with dom_id on both sides, don't hardcode the frame id”
dom_id
helper สร้าง id ของ HTML จาก record เช่น post_30 · เติมคำได้ (title_post_30) · ใช้ตั้งป้ายเลขถ้วยให้ตรงกันทุกไฟล์ · ถาม senior ได้ว่า “ในหน้านี้มี dom_id(post) ซ้ำกันสองที่ไหม ควรเติม prefix อะไร”

ลองกดตอนนี้เลย — กล่องยังมีแค่ฝั่งเดียว

ยังไม่ต้องทำอีกฝั่ง · รีเฟรชหน้า /posts/30 จะเห็นปุ่ม "แก้ไขหัวข้อ" ทางขวาของหัวข้อ

ตอนนี้หน้า /posts/30/edit ยังเป็นหน้าเต็มแบบเดิม ไม่มีกล่อง title_post_30 อยู่ในนั้นเลย · ถ้ากด "แก้ไขหัวข้อ" ตอนนี้ คิดว่าจะเกิดอะไร: (ก) ไปหน้าแก้ไขเต็มตามปกติ · (ข) ไม่เกิดอะไรเลย · (ค) อย่างอื่น?

ผลจริงใน Chrome — (ค): URL ยังเป็น /posts/30 · แต่ตรงหัวข้อกลายเป็นคำว่า Content missing ตัวหนา · ข้อความเนื้อหา · navbar · ปุ่มข้างล่าง อยู่เหมือนเดิม · HTML ของกล่องหลังกด:

<turbo-frame id="title_post_30" src="http://localhost:3000/posts/30/edit" complete=""><strong class="turbo-frame-error">Content missing</strong></turbo-frame>

src กับ complete คือของที่ Turbo เติมเองหลังกด (ไปโหลดจากไหนมา · โหลดเสร็จแล้ว) · ข้างในเหลือแค่ป้ายเตือน Content missing

และในแท็บ Console ของ DevTools (บท 130 · Mac: ⌘+⌥+J) มี error สีแดง · บรรทัดแรกที่ผมดักได้จาก Chrome:

The response (200) did not contain the expected <turbo-frame id="title_post_30"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.

แปล: "คำตอบ (200 = สำเร็จ) ไม่มีกล่อง title_post_30 ที่รออยู่ จึงไม่เอาอะไรมาวาง" · server ตอบปกติดี แต่ Turbo หาถ้วยที่ป้ายเลขตรงกันไม่เจอ — มันเลยไม่ยอมเปลี่ยนทั้งหน้า และเขียนป้ายเตือนไว้ในกล่องแทน

ท่อนท้าย "To perform a full page visit… turbo-visit-control" = คำแนะนำวิธีบังคับให้โหลดทั้งหน้าแทน — บทนี้ไม่ใช้ ข้ามได้ · ข้างหน้าข้อความอาจมีชื่อ error แปลก ๆ (ผมเห็นเป็น Nt: เพราะไฟล์ Turbo ถูกบีบให้เล็ก) ไม่ต้องสนใจเช่นกัน

นี่คือกฎข้อเดียวของ Frames ที่เห็นด้วยตาแล้ว · รีเฟรชหน้าเพื่อให้หัวข้อกลับมา · ทีนี้ไปทำอีกฝั่งให้ตรงกัน

ขั้นที่ 2 — ทำไมไม่ครอบหน้า edit เดิมเฉย ๆ

ทางที่ตรงที่สุด (และเป็นทางที่สไลด์หน้า 41 สอน) คือเปิด edit.html.erb แล้วครอบฟอร์มด้วยกล่อง id เดียวกัน · สไลด์บอกว่า "controller ไม่ต้องแก้อะไรเลย" — ซึ่งจริงถ้าหน้า edit มีไว้ใช้ในกล่องอย่างเดียว · แต่แอปเรายังต้องใช้หน้า edit เต็ม (ปุ่ม "แก้ไข" ข้างล่าง) · ผมลองทำตามสไลด์ตรง ๆ แล้ว ได้ปัญหาสองอย่าง:

ข้อสองเกิดเพราะฟอร์มที่อยู่ในกล่อง จะส่งแบบ "เปลี่ยนแค่กล่อง" เสมอ ไม่ว่าหน้านั้นจะเปิดมายังไง — บันทึกเสร็จ Turbo หยิบแค่กล่องจากหน้า show มาวางแทนฟอร์ม ส่วนอื่นของหน้า edit ค้างอยู่อย่างนั้น · ทางแก้: แยกเป็นสองไฟล์ — หน้า edit เต็มเหมือนเดิมทุกอย่าง · และไฟล์ฟอร์มเล็กที่ใช้เฉพาะตอนคำขอมาจากกล่อง

แล้ว server จะรู้ได้ยังไงว่าคำขอมาจากกล่อง? Turbo บอกให้เอง · ตอนกดลิงก์ในกล่อง มันแนบป้าย (header — ข้อมูลเสริมที่วิ่งไปกับคำขอ ผู้ใช้มองไม่เห็น) ชื่อ Turbo-Frame ไปด้วย · ผมดักคำขอจริงจาก Chrome ตอนกด "แก้ไขหัวข้อ" ได้แบบนี้ (ในเครื่องคุณดูได้ที่ DevTools แท็บ Network → คลิกคำขอ edit → Request Headers):

GET http://localhost:3000/posts/30/edit
  accept: text/html, application/xhtml+xml
  turbo-frame: title_post_30
  x-turbo-request-id: c2cdefa6-4159-4881-8e47-6bb1787cfdaa   ← เปลี่ยนทุกครั้ง

ปุ่ม "แก้ไข" ข้างล่าง (อยู่นอกกล่อง) ไม่มีป้าย turbo-frame · URL เดียวกัน แต่ป้ายต่างกัน — server จึงตอบต่างกันได้

🏷 เทียบให้เห็นภาพ — ใบสั่งที่ติดป้าย "สำหรับถ้วยหัวข้อ"

พนักงานเสิร์ฟ (Turbo) เดินมาสั่งที่ครัวพร้อมแนบป้ายว่า "สำหรับถ้วย title_post_30" · ครัวเห็นป้ายก็ใช้สูตรจานเล็ก (ฟอร์มช่องเดียว) · ลูกค้าที่เดินมาสั่งเองที่เคาน์เตอร์ (เปิด URL ตรง ๆ ไม่มีป้าย) ได้จานเต็มเหมือนเดิม · ชื่อเมนูเหมือนกัน (/posts/30/edit) แต่ได้คนละจาน

ที่ไม่เหมือนกัน: ในครัวจริง พ่อครัวเห็นป้ายก็รู้เอง · แต่ใน Rails ป้ายแค่วิ่งมาถึง ไม่มีใครเปลี่ยนสูตรให้ · เราต้องเขียนกฎเองใน controller ว่า "เห็นป้ายนี้ → ใช้ไฟล์ไหน" (3 บรรทัดข้างล่าง)

ขั้นที่ 3 — บอก controller ว่า "เห็นป้ายแล้วใช้สูตรจานเล็ก"

เปิด app/controllers/posts_controller.rb · เพิ่มบรรทัดที่ 3 ใต้ before_action :set_post ที่มีอยู่แล้ว:

class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]
  before_action :set_turbo_frame_variant, only: %i[ edit update ]

แล้วเลื่อนลงไปส่วน private · วาง method ใหม่ใต้ set_post (เหนือ post_params):

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params.expect(:id))
    end

    # คำขอที่มาจาก turbo-frame (กด "แก้ไขหัวข้อ" ในหน้า show) → ใช้ edit.html+turbo_frame.erb
    # เปิด /posts/1/edit ตรง ๆ ไม่มี header Turbo-Frame → ได้ edit.html.erb หน้าเต็มเหมือนเดิม
    def set_turbo_frame_variant
      request.variant = :turbo_frame if turbo_frame_request?
    end

สังเกตว่า action edit กับ update ของ scaffold ไม่ต้องแก้สักตัวอักษร · ป้ายกับ before_action ทำงานแทนหมด

🗣 ศัพท์ที่เพิ่งเรียนไป
header Turbo-Frame
ป้ายที่ Turbo แนบไปกับคำขอที่เกิดในกล่อง · ค่าคือ id ของกล่อง · server ใช้แยกว่า "ขอทั้งหน้า" หรือ "ขอแค่กล่อง" · ถาม senior ได้ว่า “action นี้ควรตอบต่างกันไหมถ้ามี Turbo-Frame header”
request variant · turbo_frame_request?
variant = "รุ่น" ของคำขอ · Rails จะเลือกไฟล์ view ที่ชื่อมี +รุ่น ให้เอง (เช่น edit.html+turbo_frame.erb) · พิมพ์บอก AI ได้ว่า “set request.variant = :turbo_frame when turbo_frame_request? and add edit.html+turbo_frame.erb, keep the full edit page unchanged”

ขั้นที่ 4 — ไฟล์ฟอร์มเล็กในกล่อง (edit.html+turbo_frame.erb)

สร้างไฟล์ใหม่ app/views/posts/edit.html+turbo_frame.erb — ระวังชื่อ: +turbo_frame อยู่ระหว่าง .html กับ .erb และต้องสะกดตรงกับ :turbo_frame ใน controller · edit.html.erb ตัวเดิมไม่ต้องแตะ

<%# ฟอร์มแก้เฉพาะหัวข้อ ใช้ตอนกด "แก้ไขหัวข้อ" ในหน้า show (id ต้องตรงกับ posts/_post.html.erb) %>
<%= turbo_frame_tag dom_id(@post, :title) do %>
  <%= form_with model: @post, class: "flex flex-col gap-2" do |form| %>
    <%= form.text_field :title, autofocus: true, aria: { label: "หัวข้อ" },
          class: ["input input-lg w-full", { "input-error": @post.errors[:title].any? }] %>

    <% @post.errors.full_messages_for(:title).each do |message| %>
      <p class="text-sm text-error"><%= message %></p>
    <% end %>

    <div class="flex gap-2">
      <%= form.submit "บันทึก", class: "btn btn-primary btn-sm" %>
      <%= link_to "ยกเลิก", @post, class: "btn btn-ghost btn-sm" %>
    </div>
  <% end %>
<% end %>

ตรวจในเบราว์เซอร์จริง — 4 อย่างที่ต้องเห็น

รีเฟรช /posts/30 แล้วลองตามนี้ (ผมตรวจทุกข้อด้วย Chrome จริง · ผลข้างล่างเป็นของจริง):

① กด "แก้ไขหัวข้อ" — หัวข้อกลายเป็นช่องพิมพ์ที่มีคำว่า "นมเย็น" · เคอร์เซอร์กะพริบอยู่ในช่องเลย · ปุ่ม "บันทึก" กับ "ยกเลิก" อยู่ใต้ช่อง · URL ยังเป็น /posts/30 · ผมเช็คใน Chrome แล้ว ของนอกกล่องทั้งหมด (navbar · breadcrumb · เนื้อหา · ปุ่มข้างล่าง) เป็นชิ้นเดิม ไม่ได้ถูกวาดใหม่

log ของ server ตอนกด (ผลจริง · ตัดบรรทัด ↳):

Started GET "/posts/30/edit" for ::1 at 2026-11-07 10:58:40 +0700
Processing by PostsController#edit as HTML
  Parameters: {"id" => "30"}
  Post Load (1.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 30 LIMIT 1 /*action='edit',application='WongnokBlogAllInOne',controller='posts'*/
  Rendering layout …/turbo-rails-2.0.23/app/views/layouts/turbo_rails/frame.html.erb
  Rendering posts/edit.html+turbo_frame.erb within layouts/turbo_rails/frame
  Rendered posts/edit.html+turbo_frame.erb within layouts/turbo_rails/frame (Duration: 3.3ms | GC: 0.0ms)
  Rendered layout …/turbo-rails-2.0.23/app/views/layouts/turbo_rails/frame.html.erb (Duration: 5.9ms | GC: 0.0ms)
Completed 200 OK in 9ms (Views: 6.6ms | ActiveRecord: 0.8ms (1 query, 0 cached) | GC: 0.0ms)

อยากเห็นของที่ server ส่งกลับมาด้วยตาตัวเอง ใช้ curl แนบป้ายเอง (-H = ใส่ header) · ผลจริง (ตัดรหัสกัน CSRF ยาว ๆ และ script ของ rack-mini-profiler — ตัวจับเวลาที่บท 120 เจอ):

$ curl -s -H "Turbo-Frame: title_post_30" localhost:3000/posts/30/edit
<!-- BEGIN …/turbo-rails-2.0.23/app/views/layouts/turbo_rails/frame.html.erb
--><html>
  <head>
    <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="…" />
    
  </head>
  <body>
    <!-- BEGIN app/views/posts/edit.html+turbo_frame.erb
--><turbo-frame id="title_post_30">
  <form class="flex flex-col gap-2" action="/posts/30" accept-charset="UTF-8" method="post"><input type="hidden" name="_method" value="patch" /><input type="hidden" name="authenticity_token" value="…" />
    <input autofocus="autofocus" aria-label="หัวข้อ" class="input input-lg w-full" type="text" value="นมเย็น" name="post[title]" id="post_title" />


    <div class="flex gap-2">
      <input type="submit" name="commit" value="บันทึก" class="btn btn-primary btn-sm" data-disable-with="บันทึก" />
      <a class="btn btn-ghost btn-sm" href="/posts/30">ยกเลิก</a>
    </div>
</form></turbo-frame><!-- END app/views/posts/edit.html+turbo_frame.erb -->
  …
</body>
</html>
<!-- END …/turbo-rails-2.0.23/app/views/layouts/turbo_rails/frame.html.erb -->

② ลบหัวข้อจนว่าง แล้วกด "บันทึก" — ช่องมีขอบแดง · ใต้ช่องขึ้น Title can't be blank สีแดง · ทุกอย่างเกิดในกล่อง URL ยังเป็น /posts/30 · log:

Started PATCH "/posts/30" for ::1 at 2026-11-07 10:58:41 +0700
Processing by PostsController#update as TURBO_STREAM
  Parameters: {"authenticity_token" => "[FILTERED]", "post" => {"title" => ""}, "commit" => "บันทึก", "id" => "30"}
  …
  Rendering posts/edit.html+turbo_frame.erb within layouts/turbo_rails/frame
  …
Completed 422 Unprocessable Content in 4ms (Views: 1.0ms | ActiveRecord: 0.9ms (1 query, 0 cached) | GC: 0.0ms)

③ พิมพ์ "นมเย็นสูตรโบราณ" แล้วกด "บันทึก" — กล่องกลับเป็นหัวข้อตัวใหญ่ "นมเย็นสูตรโบราณ" + ปุ่ม "แก้ไขหัวข้อ" · URL ยังเป็น /posts/30 · log ย่อ:

Started PATCH "/posts/30" for ::1 at 2026-11-07 10:58:41 +0700
…
  Post Update (2.4ms)  UPDATE "posts" SET "title" = 'นมเย็นสูตรโบราณ', "updated_at" = '2026-11-07 03:58:41.734534' WHERE "posts"."id" = 30 …
…
Redirected to http://localhost:3000/posts/30
Completed 303 See Other in 8ms (ActiveRecord: 3.9ms (2 queries, 0 cached) | GC: 0.0ms)


Started GET "/posts/30" for ::1 at 2026-11-07 10:58:41 +0700
…
  Rendering posts/show.html.erb within layouts/turbo_rails/frame
…
Completed 200 OK in 5ms (Views: 2.1ms | ActiveRecord: 1.1ms (2 queries, 0 cached) | GC: 0.0ms)

บันทึกสำเร็จ → update สั่ง redirect_to @post เหมือนเดิม (303 See Other = "ไปดูหน้านี้แทน" · บท 118) → Turbo ตามไปขอหน้า show (ยังติดป้าย Turbo-Frame) → เอาแค่กล่อง title_post_30 มาวาง · (เวลาใน UPDATE ช้ากว่าใน log 7 ชั่วโมงเพราะฐานข้อมูลเก็บเป็นเวลาสากล UTC — ไม่ใช่บั๊ก)

แต่สังเกตให้ดี — ในหน้าจอเดียวกันนั้น:

ไม่ใช่บั๊ก · ทั้งสามอย่างอยู่นอกกล่อง และ Turbo Frame เปลี่ยนได้แค่ในกล่องเดียว ตามที่ออกแบบไว้ · รีเฟรชหน้าแล้วทุกอย่างตรง (ข้อมูลใน DB ถูกแล้ว) · ถ้าอยากให้หลายจุดเปลี่ยนพร้อมกัน ต้องใช้อีกเครื่องมือหนึ่ง — Turbo Streams ในบท 132

④ กด "แก้ไขหัวข้อ" อีกครั้ง แล้วกด "ยกเลิก" — กล่องกลับเป็นหัวข้อ "นมเย็นสูตรโบราณ" + ปุ่มเหมือนเดิม · URL ไม่เปลี่ยน

⚠️ ช่องพิมพ์ไม่ใหญ่ (สูงเท่าช่องธรรมดา)? แปลว่าไม่ได้เปิด bin/dev — คลาสใหม่ input-lg ยังไม่ถูกสร้างใน CSS (ผมลองแล้ว: ช่องสูง 40px แทน 48px · ส่วนข้อความ error ยังแดงอยู่ เพราะ CSS เดิมมี text-error อยู่แล้ว) · เปิด bin/dev หรือสั่ง bin/rails tailwindcss:build แล้วรีเฟรช (บท 128)

ของแถม: เปิด /posts/30/edit ตรง ๆ ยังได้หน้าเต็ม

ลองสองทาง: กดปุ่ม "แก้ไข" ข้างล่างบทความ (อยู่นอกกล่อง) · หรือพิมพ์ localhost:3000/posts/30/edit ที่ช่อง URL · ทั้งสองทางได้หน้าแก้ไขเต็มแบบเดิมทุกอย่าง: URL เปลี่ยนเป็น /posts/30/edit · มี navbar · ฟอร์มสามช่อง · ในหน้าไม่มี <turbo-frame> เลย · log บอกว่า:

Processing by PostsController#edit as HTML
…
  Rendering layout layouts/application.html.erb
  Rendering posts/edit.html.erb within layouts/application
…
Completed 200 OK in 8ms (Views: 3.8ms | ActiveRecord: 1.8ms (2 queries, 0 cached) | GC: 0.0ms)

คำขอไม่มีป้าย → before_action ไม่ตั้ง variant → ได้ edit.html.erb กับ layout application ของเราตามปกติ · ความสามารถแบบนี้มีชื่อเรียกว่า progressive enhancement — หน้าเว็บทำงานได้ครบด้วยลิงก์และฟอร์มธรรมดาก่อน · Turbo เป็น "ของแถม" ที่ทำให้ลื่นขึ้น · ถ้า JavaScript โหลดไม่ขึ้น หรือมีคนเปิด URL ตรง ๆ ทุกอย่างยังใช้ได้

🧑‍🍳 เทียบให้เห็นภาพ — ร้านที่มีพนักงานเสิร์ฟถึงโต๊ะ แต่เคาน์เตอร์ยังเปิดอยู่

ร้านนี้มีบริการเปลี่ยนถ้วยถึงโต๊ะ (Turbo Frame) ซึ่งสะดวก · แต่ถ้าวันไหนพนักงานไม่อยู่ (JavaScript ไม่ทำงาน) หรือลูกค้าเดินไปที่เคาน์เตอร์เอง (เปิด URL ตรง ๆ) ก็ยังสั่งอาหารได้ครบจาน · บริการถึงโต๊ะเป็นชั้นที่เพิ่มบนร้านที่ใช้งานได้อยู่แล้ว

ที่ไม่เหมือนกัน: ร้านจริงเคาน์เตอร์กับพนักงานเป็นคนละช่องทาง · ในเว็บใช้ "เมนู" เดียวกันเป๊ะ (/posts/30/edit) · ที่ได้คนละจานเพราะป้าย Turbo-Frame ล้วน ๆ — ถ้าเราลืมทำหน้าเต็มไว้ (แบบที่สไลด์สอน) "เคาน์เตอร์" จะพังโดยไม่มีใครเห็นจนกว่าจะมีคนเปิด URL ตรง ๆ

🗣 ศัพท์ที่เพิ่งเรียนไป
progressive enhancement
สร้างหน้าให้ใช้ได้ด้วย HTML ธรรมดาก่อน แล้วค่อยเสริมความลื่นด้วย JavaScript · ถ้าชั้นเสริมพัง ชั้นล่างยังทำงาน · ถาม senior ได้ว่า “หน้านี้ยัง progressively enhanced อยู่ไหม — เปิด URL ตรง ๆ แล้วใช้ได้ครบหรือเปล่า”

★ กฎข้อเดียว: id ต้องตรงสองฝั่ง — ลองทำให้พัง

สไลด์หน้า 42 บอกว่านี่คือบั๊กอันดับ 1 ของคนเริ่มใช้ Frames · สาเหตุที่เจอบ่อย: ฝั่งหนึ่งใช้ dom_id อีกฝั่งพิมพ์ id เอง · ลองดู — สมมติคุณเดาว่า id น่าจะเป็น "post_30_title" แล้วพิมพ์เองในไฟล์ฟอร์มเล็ก (บรรทัด 2):

<%= turbo_frame_tag "post_#{@post.id}_title" do %>

"post_#{@post.id}_title" = ต่อ string เอง (#{…} = เอาค่ามาแทรก · Ruby บท 91) ได้ post_30_title · ผลจริงเมื่อกด "แก้ไขหัวข้อ":

$ curl -s -H "Turbo-Frame: title_post_30" localhost:3000/posts/30/edit | grep turbo-frame
--><turbo-frame id="post_30_title">
</form></turbo-frame><!-- END app/views/posts/edit.html+turbo_frame.erb -->
🔢 เทียบให้เห็นภาพ — ป้ายเลขถ้วยที่ไม่ตรงกัน

ถ้วยบนโต๊ะติดป้าย "title_post_30" · ครัวจัดถ้วยใหม่มาแต่ติดป้าย "post_30_title" · พนักงานถือถาดมาถึงโต๊ะ หาถ้วยป้ายตรงกันไม่เจอ · เลยไม่วางอะไรเลย แปะโน้ต "Content missing" ไว้ในถ้วยเดิม แล้วจดเรื่องนี้ลงสมุดปัญหา (Console)

ที่ไม่เหมือนกัน: พนักงานจริงเห็นว่าป้ายคล้าย ๆ กัน คงเดาได้ว่าถ้วยนี้แหละ · Turbo ไม่เดาเลย — ต่างกันตัวเดียวก็ถือว่าไม่เจอ · และครัวก็ไม่รู้ตัวด้วย (log ฝั่ง server ขึ้น 200 OK ปกติ) อาการจึงไปโผล่ที่เบราว์เซอร์อย่างเดียว

ทางกันที่ดีที่สุด: ใช้ dom_id(…, :title) ทั้งสองฝั่ง ห้ามพิมพ์ id เอง · คนพิมพ์เองเดาลำดับผิดได้ (อย่างที่เพิ่งเห็น — Rails เอาคำเติมไว้ข้างหน้า) · helper ตัวเดียวกัน ได้คำตอบเดียวกันเสมอ · และคอมเมนต์บรรทัดแรกของทั้งสองไฟล์ช่วยเตือนว่ากล่องนี้มีคู่ — แก้ฝั่งหนึ่งต้องไปดูอีกฝั่ง

อย่าลืมแก้บรรทัด 2 กลับเป็น dom_id(@post, :title) ก่อนไปต่อ · ลองกดอีกที หัวข้อต้องกลายเป็นช่องพิมพ์ได้ตามปกติ

หนีออกจากกล่อง และของอื่นที่ Frames ทำได้

บางทีลิงก์อยู่ในกล่อง แต่เราอยากให้มันเปลี่ยนทั้งหน้า · ตัวอย่าง: เพิ่มลิงก์ "เปิดฟอร์มเต็ม" ไว้ข้างปุ่มยกเลิก เผื่อคนอยากแก้เนื้อหาด้วย · ถ้าเขียนแบบธรรมดา (ลองแล้วค่อย git restore ทิ้งตอนท้าย):

      <%= link_to "เปิดฟอร์มเต็ม", edit_post_path(@post), class: "btn btn-link btn-sm" %>

ผลจริง: กดแล้วไม่ไปไหน — URL ยังเป็น /posts/30 และกล่องยังเป็นฟอร์มช่องเดียวเหมือนเดิม · เพราะลิงก์อยู่ในกล่อง → ติดป้าย Turbo-Frame → controller ให้ฟอร์มเล็กอีกรอบ · วนอยู่ในกล่อง

ต้องบอก Turbo ว่า "ลิงก์นี้ให้เปลี่ยนทั้งหน้า" ด้วย data: { turbo_frame: "_top" }:

      <%= link_to "เปิดฟอร์มเต็ม", edit_post_path(@post), class: "btn btn-link btn-sm",
            data: { turbo_frame: "_top" } %>

HTML ที่ได้จริง (ดึงจากหน้าใน Chrome):

<a class="btn btn-link btn-sm" data-turbo-frame="_top" href="/posts/30/edit">เปิดฟอร์มเต็ม</a>

ลองแล้วลบสองบรรทัดนั้นทิ้ง — โค้ดของบทนี้ไม่มีลิงก์นี้ · ของอื่นที่ Frames ทำได้ (สไลด์หน้า 43) — รู้จักไว้ก่อน ยังไม่ต้องพิมพ์:

อยากได้เขียนแบบไหนภาษาคน
ลิงก์ในกล่องไปเปลี่ยนกล่องอื่นdata: { turbo_frame: "sidebar" }ใส่ id ของกล่องปลายทางแทน _top · กล่องนั้นต้องมีอยู่ในหน้า และคำตอบต้องมีกล่อง id นั้น (กฎเดิม)
ส่วนที่ช้า ให้โหลดทีหลัง (lazy frame)<turbo-frame id="comments" src="…" loading="lazy"> + ข้อความ "กำลังโหลด…" ข้างในหน้าหลักขึ้นก่อน · กล่องไปขอ src เองตอนเลื่อนมาเห็น · ไม่ต้องเขียน JS · ตัวอย่างในสไลด์ใช้คอมเมนต์ ซึ่งยังไม่มีจนถึงบท 132
🗣 ศัพท์ที่เพิ่งเรียนไป
data-turbo-frame · _top
บอกว่าลิงก์/ฟอร์มนี้ให้ไปเปลี่ยนกล่องไหน · _top = ทั้งหน้า · ใส่ id = กล่องนั้น · พิมพ์บอก AI ได้ว่า “this link is inside a turbo frame but must navigate the whole page, add data-turbo-frame=_top”
lazy frame (src + loading="lazy")
กล่องที่ไปโหลดเนื้อหาเองทีหลัง · loading="lazy" = รอให้เลื่อนมาเห็นก่อนค่อยโหลด · ถาม senior ได้ว่า “ส่วนนี้ช้า ทำเป็น lazy turbo frame ดีไหม”

Frames เหมาะกับอะไร ไม่เหมาะกับอะไร

เหมาะ ✓ไม่เหมาะ ✗ → ใช้อะไรแทน
inline edit — แก้ของในที่ของมัน (ที่เพิ่งทำ)ต้องเปลี่ยนหลายที่พร้อมกัน เช่น หัวข้อ + breadcrumb + ข้อความ "บันทึกแล้ว" → Turbo Streams บท 132
แท็บ หรือ modal (หน้าต่างที่เด้งทับหน้า) ที่เนื้อหามาจาก serverต้องเปลี่ยนของนอกกล่อง เช่น ตัวเลขบน navbar → Turbo Streams บท 132
ส่วนที่ช้า ให้โหลดทีหลัง (lazy)ของที่ไม่ต้องคุยกับ server เลย เช่น เปิด/ปิดเมนู · นับตัวอักษรที่พิมพ์ → Stimulus บท 134
ฟอร์มค้นหาที่อัปเดตผลลัพธ์ในกล่องเด้งของใหม่ขึ้นทุกแท็บที่เปิดอยู่ → broadcast บท 133

หลักจำง่าย: กล่องเดียว เปลี่ยนตามที่ผู้ใช้กด → Frame · ผลของสิ่งที่กดกระทบหลายจุด → Stream · ไม่ต้องถาม server → Stimulus

ให้ AI ทำ แล้วตรวจยังไง

ตอนนี้คุณทำเองเป็นแล้ว · ครั้งหน้าจะสั่ง AI ทำ inline edit ช่องอื่นก็ได้ แต่ต้องตรวจเป็น · prompt สั้น ๆ แบบ 4 ส่วน (บท 121):

เป้าหมาย: ในหน้า show ของ Post ให้กด "แก้ไขหัวข้อ" แล้วแก้หัวข้อได้ในหน้าเดิมด้วย Turbo Frame
ข้อกำหนด: ใช้ turbo_frame_tag dom_id(post, :title) ทั้งสองฝั่ง · ฟอร์มเล็กอยู่ใน edit.html+turbo_frame.erb
          ตั้ง request.variant = :turbo_frame เมื่อ turbo_frame_request? (เฉพาะ edit, update)
ห้าม: แก้ edit.html.erb หน้าเต็ม · พิมพ์ id ของกล่องเอง · เพิ่ม JavaScript
เกณฑ์ตรวจรับ: ดู checklist ข้างล่าง

checklist ตรวจรับ (ทำเองได้ใน 3 นาที):

  1. grep -rn 'turbo_frame_tag "' app/views (ค้นคำในไฟล์ · บท 125) → ต้องไม่เจออะไร (เจอ = มีคนพิมพ์ id เองเป็น string)
  2. กด "แก้ไขหัวข้อ" → URL ไม่เปลี่ยน · Console ไม่มีข้อความ did not contain the expected <turbo-frame
  3. ส่งหัวข้อว่าง → error ขึ้นในกล่อง · log เป็น 422 และ render edit.html+turbo_frame.erb
  4. เปิด /posts/30/edit ตรง ๆ แล้วบันทึก → URL ไป /posts/30 และขึ้น "บันทึกการแก้ไขแล้ว" (หน้าเต็มไม่พัง)
  5. git diff → edit.html.erb ต้องไม่ถูกแตะ

ข้อ 4 คือจุดที่ AI พลาดบ่อยที่สุดเพราะมันทำตามแบบในบทความทั่วไป (ครอบหน้า edit ทั้งหน้า) · repo ของอาจารย์ (branch day3-c-done) ก็ใช้วิธี variant แบบเดียวกับเรา (หน้าตาไฟล์ต่างไปนิดหน่อย เช่น เขียน turbo_frame_tag post, :title ซึ่งได้ id เดียวกัน)

เมื่อทุกข้อผ่าน บันทึกงาน (ลบลิงก์ "เปิดฟอร์มเต็ม" ที่ลองไว้ออกก่อน ถ้ายังอยู่):

git status        # ต้องเห็น 2 ไฟล์ที่แก้ (posts_controller.rb · _post.html.erb) + 1 ไฟล์ใหม่ (edit.html+turbo_frame.erb)
git add .
git commit -m "แก้หัวข้อบทความในหน้าเดิมด้วย Turbo Frame"

🎯 ภารกิจ — ทำให้ id ไม่ตรง แล้วอ่านอาการเอง (~10 นาที)

ทำหลัง commit แล้ว (จะได้ใช้ git diff ตรวจว่าแก้กลับครบ) · เปิด bin/dev ไว้

  1. เปิด app/views/posts/edit.html+turbo_frame.erb · บรรทัด 2 ลบ , :title ออก ให้เหลือ turbo_frame_tag dom_id(@post) do (นี่คือความผิดที่เจอบ่อย — ลืมคำเติม) · บันทึกไฟล์
  2. เปิด http://localhost:3000/posts/30 · เปิด DevTools แท็บ Console · กด "แก้ไขหัวข้อ"
  3. จดสามอย่าง: ในกล่องขึ้นอะไร · URL เป็นอะไร · Console บอกว่า "expected" id อะไร
  4. ในเทอร์มินัลอีกหน้าต่าง ดูว่า server ส่งกล่อง id อะไรมา:
    curl -s -H "Turbo-Frame: title_post_30" localhost:3000/posts/30/edit | grep turbo-frame
  5. แก้บรรทัด 2 กลับเป็น dom_id(@post, :title) · สั่ง git diff → ต้องไม่มีอะไรขึ้นเลย · กด "แก้ไขหัวข้อ" อีกครั้ง ต้องได้ช่องพิมพ์

ก่อนกดเฉลย: ขั้น 4 คิดว่า server ส่ง id อะไรมา? และในหน้านี้มี element ไหนที่ใช้ id นั้นอยู่แล้ว?

สิ่งที่ควรเห็น (ผลจริง):

  • ในกล่อง: คำว่า Content missing ตัวหนา แทนหัวข้อ · URL ยังเป็น /posts/30
  • Console: The response (200) did not contain the expected <turbo-frame id="title_post_30"> and will be ignored. …
  • curl:
    --><turbo-frame id="post_30">
    </form></turbo-frame><!-- END app/views/posts/edit.html+turbo_frame.erb -->

หน้ารอ title_post_30 แต่ได้ post_30 · ซึ่งบังเอิญเป็น id ของ <article> ที่ครอบบทความทั้งชิ้นในหน้า show ด้วย · แต่ Turbo ไม่สนใจ <article> เลย มันหาแค่ <turbo-frame id="title_post_30"> ในคำตอบเท่านั้น · วิธีอ่านอาการนี้ใช้ได้ทุกครั้ง: เอา id ใน "expected" ของ Console มาเทียบกับ id ที่ curl … | grep turbo-frame เห็น

ถ้าเจออาการนี้ แปลว่า…

  • Content missing ทั้งที่ id ตรงกันทั้งสองไฟล์ — ชื่อไฟล์ variant ผิด (เช่น edit.html+turboframe.erb) หรือลืม before_action :set_turbo_frame_variant · Rails ไม่ error แต่ไปใช้ edit.html.erb ซึ่งไม่มีกล่อง (ผมลองชื่อไฟล์ผิดแล้ว ได้ Content missing ข้อความเดียวกันเป๊ะ) · ดู log: ถ้าเห็น Rendering posts/edit.html.erb แทน edit.html+turbo_frame.erb = ใช่เลย
  • กดแล้วทั้งหน้าเปลี่ยนไป /posts/30/edit — คุณกดปุ่ม "แก้ไข" ข้างล่าง (อยู่นอกกล่อง) ไม่ใช่ "แก้ไขหัวข้อ"
  • curl ได้หน้ายาว ๆ ไม่มีคำว่า turbo-frame เลย — ลืม -H "Turbo-Frame: title_post_30" · ไม่มีป้าย = ได้หน้าเต็ม (ตรงตามที่ควรเป็น)
  • git diff ยังมีบรรทัดขึ้น — แก้กลับไม่ครบ หรือเว้นวรรคต่างจากเดิม · สั่ง git restore app/views/posts/edit.html+turbo_frame.erb ได้เลย

กด "แก้ไขหัวข้อ" แล้วในกล่องขึ้นคำว่า Content missing · log ของ server ขึ้น Completed 200 OK · สาเหตุที่น่าจะเป็นที่สุดคืออะไร?

ตัวเลือกที่ 3 ถูก — server ตอบสำเร็จ (200) แต่ในคำตอบไม่มี <turbo-frame> id ที่หน้ารออยู่ · Turbo จึงไม่วางอะไรและเขียน Content missing ไว้ในกล่อง

ตัวเลือกที่ 1 ผิด เพราะ log บอก 200 OK ชัด ๆ · ถ้า server พังจะเป็น 500 · ตัวเลือกที่ 2 ผิด เพราะถ้า Turbo ยังไม่ทำงาน ลิงก์จะพาไปหน้า edit เต็ม (progressive enhancement) ไม่ใช่ขึ้น Content missing · ตัวเลือกที่ 4 ผิด เพราะบทความที่ไม่มีจริงจะได้ 404 ไม่ใช่ 200

จุดที่ต้องจำ: Content missing + 200 OK = id สองฝั่งไม่ตรง · เทียบ id ใน Console กับ id ที่ curl … | grep turbo-frame เห็น

ปุ่ม "แก้ไขหัวข้อ" (ในกล่อง) กับปุ่ม "แก้ไข" ข้างล่าง (นอกกล่อง) ลิงก์ไป /posts/30/edit เหมือนกัน · ทำไมได้ผลต่างกัน?

ตัวเลือกที่ 1 ถูก — Turbo แนบ header Turbo-Frame: title_post_30 เฉพาะคำขอที่เกิดในกล่อง · turbo_frame_request? เป็นจริง → variant → edit.html+turbo_frame.erb

ตัวเลือกที่ 2 ผิด เพราะทั้งคู่เข้า action edit เดิม (log เขียน PostsController#edit ทั้งสองครั้ง) · Turbo ไม่สร้าง action ให้ · ตัวเลือกที่ 3 ผิด เพราะลิงก์นอกกล่องก็ยังใช้ Turbo Drive ตามปกติ แค่เปลี่ยนทั้งหน้า · ตัวเลือกที่ 4 ผิด เพราะ log แสดงว่ามีคำขอไปถึง server จริงทั้งสองแบบ และได้คนละไฟล์

จุดที่ต้องจำ: URL เดียวกัน แต่ "อยู่ในกล่องหรือไม่" ทำให้ป้ายต่างกัน → server ตอบต่างกันได้

แก้หัวข้อในกล่องสำเร็จ หัวข้อใหม่โผล่แล้ว · แต่ breadcrumb ข้างบนยังเป็นชื่อเก่า และไม่มีข้อความ "บันทึกการแก้ไขแล้ว" · ข้อไหนอธิบายถูก?

ตัวเลือกที่ 4 ถูก — Turbo เอาแค่ข้างในกล่อง title_post_30 มาวาง · ส่วนอื่นของคำตอบทิ้งหมด รวมทั้ง breadcrumb กับข้อความ notice

ตัวเลือกที่ 1 ผิด เพราะ log มี Post Update … UPDATE "posts" แล้ว · รีเฟรชก็เห็นชื่อใหม่ทุกที่ · ตัวเลือกที่ 2 ผิด เพราะ breadcrumb ใช้ @post.title คอลัมน์เดียวกัน · แค่ไม่ได้ถูกวาดใหม่ · ตัวเลือกที่ 3 ผิด เพราะไม่เกี่ยวกับ cache — breadcrumb ไม่เคยถูกขอใหม่เลยตั้งแต่แรก

จุดที่ต้องจำ: Frame = เปลี่ยนกล่องเดียว · อยากให้หลายจุดเปลี่ยนพร้อมกัน → Turbo Streams (บท 132)

ในฟอร์มเล็กในกล่อง อยากมีลิงก์ "เปิดฟอร์มเต็ม" ที่พาไปหน้า /posts/30/edit แบบทั้งหน้า · ต้องทำอย่างไร?

ตัวเลือกที่ 2 ถูก — data-turbo-frame="_top" บอก Turbo ว่าลิงก์นี้เปลี่ยนทั้งหน้า · ผลจริง: URL ไป /posts/30/edit ได้ฟอร์มสามช่อง

ตัวเลือกที่ 1 ผิด เพราะ loading: "lazy" เป็นเรื่องของกล่องที่โหลดเนื้อหาเองทีหลัง ไม่เกี่ยวกับลิงก์ · ตัวเลือกที่ 3 ผิด เพราะลิงก์จะยังอยู่ในกล่อง (กล่องใหม่) ก็ยังไม่เปลี่ยนทั้งหน้า และคำตอบต้องมีกล่อง id นั้นด้วย · ตัวเลือกที่ 4 ผิด เพราะทำให้ปุ่ม "แก้ไขหัวข้อ" พังทั้งหมด — ได้ Content missing (ไม่มีไฟล์ไหนตอบกล่องกลับมา)

จุดที่ต้องจำ: ลิงก์ในกล่อง = เปลี่ยนในกล่องเสมอ · อยากหนีออกทั้งหน้า → _top

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริง (turbo-rails 2.0.23)ต้องสนใจไหม
41ครอบฟอร์มในหน้า edit ด้วย <turbo-frame> · "controller ไม่ต้องแก้อะไรเลย"ทำตามแล้วในกล่องได้ฟอร์ม 3 ช่อง และหน้า edit เต็มพัง (บันทึกแล้ว URL ค้าง /edit) · เราใช้ variant + ไฟล์ edit.html+turbo_frame.erb แทน (repo อาจารย์ day3-c-done ก็ใช้วิธีนี้)สนใจ — ถ้าทำตามสไลด์ หน้าเต็มจะพัง
41 · 42"ใช้ dom_id(post) ทั้งสองฝั่ง" · ตัวอย่าง id post_5ในแอปเรา post_30 ถูก <article> ใช้แล้ว → ใช้ dom_id(post, :title) = title_post_30 ทั้งสองฝั่งสนใจ — หลักการเดิม แค่เติมคำ
42id ไม่ตรง "กล่องจะว่างเปล่า"กล่องขึ้นคำว่า Content missingไม่ต้อง — อาการเดียวกัน
42console ขึ้น "Response has no matching <turbo-frame id=...>"ข้อความจริง: The response (200) did not contain the expected <turbo-frame id="…"> and will be ignored…สนใจถ้าจะค้นหาข้อความ error
43lazy frame ใช้ post_comments_path(post)ยังไม่มีคอมเมนต์ในแอปจนถึงบท 132 · พิมพ์ตามตอนนี้จะ errorไม่ต้อง — แค่รู้จักไว้

อ้างอิง