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

layout, partial และ locals — แม่พิมพ์ที่ใช้ซ้ำได้

~20 นาที · ควรผ่าน บทเรียน 122 (ERB) และ บท 120 (เปิดโปรเจกต์) มาก่อน · ใช้ของจาก บท 103 (layout) และ บท 118 (partial ที่ scaffold สร้าง) ด้วย

หน้าเว็บทุกหน้าของเรามีส่วนที่ซ้ำกัน — หัวเว็บเหมือนกันทุกหน้า · กล่องบทความหน้าตาเดียวกันทั้งในหน้ารายการและหน้าบทความ
บทนี้คุณจะอ่าน layout ของจริงในโปรเจกต์ออกทุกบรรทัด · ตั้งชื่อแท็บเบราว์เซอร์ให้แต่ละบทความได้ · เข้าใจว่า render @posts บรรทัดเดียวทำอะไรให้บ้าง · และรู้ว่าทำไมชิ้นส่วนที่ใช้ซ้ำ (partial) ควรรับของผ่านมือ ไม่ใช่ไปหยิบ @ตัวแปร เอง

🔁 ทวนก่อนเริ่ม — คำพวกนี้จะโผล่ในบทนี้ทั้งหมด

🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 18 (สไลด์และ repo ของอาจารย์เขียน Ruby 4.0.5 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน)
📍 โค้ดเริ่มจาก: โปรเจกต์ที่ทำเสร็จในบท 120 (หมวด 5 · บทความ 30 ชิ้น) · บทนี้ทดลองแก้ไฟล์แล้วคืนทิ้งตอนจบด้วย git restore — partial ตัวจริงเราทำกันใน Lab บท 126
🖥 เปิดค้างไว้: bin/dev ในหน้าต่าง terminal หนึ่ง (เว็บอยู่ที่ localhost:3000) · อีกหน้าต่างไว้พิมพ์ curl · ผลรันทุกกล่องมาจากเครื่องจริง · รหัส csrf-token · เวลา ms · เลข 0x… ของคุณจะไม่ตรงกับของผม

1 · layout ของจริงในโปรเจกต์ — จานรองที่ยังไม่มีโลโก้ร้าน

ทวนสั้น ๆ: layout คือไฟล์ที่ครอบทุกหน้าให้อัตโนมัติ · ของที่ทุกหน้าต้องมี (หัวเว็บ · CSS · JavaScript) เขียนไว้ที่เดียว · ส่วนเนื้อหาของแต่ละหน้าถูกวางลงตรง <%= yield %>

เปิดไฟล์จริงของโปรเจกต์ app/views/layouts/application.html.erb (ทั้งไฟล์ 31 บรรทัด · ผมใส่เลขบรรทัดไว้ข้างหน้าให้อ้างถึงง่าย — เลขพวกนี้ไม่มีในไฟล์จริง):

 1  <!DOCTYPE html>
 2  <html>
 3    <head>
 4      <title><%= content_for(:title) || "Wongnok Blog All In One" %></title>
 5      <meta name="viewport" content="width=device-width,initial-scale=1">
 6      <meta name="apple-mobile-web-app-capable" content="yes">
 7      <meta name="application-name" content="Wongnok Blog All In One">
 8      <meta name="mobile-web-app-capable" content="yes">
 9      <%= csrf_meta_tags %>
10      <%= csp_meta_tag %>
11
12      <%= yield :head %>
13
14      <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
15      <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
16
17      <link rel="icon" href="/icon.png" type="image/png">
18      <link rel="icon" href="/icon.svg" type="image/svg+xml">
19      <link rel="apple-touch-icon" href="/icon.png">
20
21      <%# Includes all stylesheet files in app/assets/stylesheets %>
22      <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
23      <%= javascript_importmap_tags %>
24    </head>
25
26    <body>
27      <main class="container mx-auto mt-28 px-5 flex">
28        <%= yield %>
29      </main>
30    </body>
31  </html>

ไม่ต้องเข้าใจทุกบรรทัดตอนนี้ · ตารางนี้บอกว่าแต่ละบรรทัดคืออะไร และต้องสนใจเมื่อไหร่:

บรรทัดคืออะไร (ภาษาคน)ต้องสนใจเมื่อไหร่
4 content_for(:title) || "…"ชื่อแท็บของเบราว์เซอร์ · "ถ้าหน้านั้นตั้งชื่อมาก็ใช้ ถ้าไม่ตั้งก็ใช้ชื่อแอป"บทนี้ หัวข้อ 2
5–8 <meta …>ป้ายบอกมือถือว่าย่อขยายหน้ายังไง · ชื่อแอปเวลาบันทึกลงหน้าจอโทรศัพท์ไม่ต้องสนตอนนี้
9 csrf_meta_tags"บัตรคิว" กันใบสั่งปลอมจากเว็บอื่น (CSRF)ทวน บท 103
10 csp_meta_tagป้ายความปลอดภัยอีกชนิด · แอปนี้ยังไม่ได้เปิดใช้ จึงไม่พิมพ์อะไรออกมาเลยไม่ต้องสนตอนนี้
12 yield :headช่องว่างใน <head> ที่แต่ละหน้าฝากของมาใส่ได้บทนี้ หัวข้อ 2
14–15, 21 <%# … %>คอมเมนต์ (บท 122) · บรรทัด 15 เป็น <%#= ก็ยังเป็นคอมเมนต์ เพราะขึ้นต้นด้วย <%#ข้ามได้
17–19 <link rel="icon">ไอคอนเล็ก ๆ บนแท็บเบราว์เซอร์ข้ามได้
22–23 stylesheet_link_tag · javascript_importmap_tagsดึงไฟล์ CSS และ JavaScript ของแอปเข้ามา · "data-turbo-track" เป็นป้ายเสริมของ Turbo ข้ามไปก่อนได้บท 127
27–29 <main> + yieldกรอบกลางหน้า · ช่องที่เนื้อหาของแต่ละหน้าถูกวางลงไป · class ยาว ๆ คือ Tailwind (บท 72)ทวน บท 103

สังเกตว่ายังไม่มีแถบเมนูด้านบน (navbar) และส่วนท้าย (footer) — layout นี้มาจาก rails new ตรง ๆ · เราจะเติม navbar ใน Lab บท 129

⚠️ สไลด์หน้า 12 มีบรรทัด <%= render "shared/navbar" %> และ "shared/footer" ใน layout — ไฟล์พวกนั้นยังไม่มีในโปรเจกต์ · ถ้าพิมพ์ตามตอนนี้ ทุกหน้าพังด้วย Missing partial shared/_navbar with {locale: [:en], formats: [:html, … (ผมลองแล้ว) · อย่าเพิ่งเติม

ป้ายบอกว่า HTML ก้อนไหนมาจากไฟล์ไหน

โปรเจกต์นี้เปิดตัวช่วยไว้ตัวหนึ่ง (บรรทัด config.action_view.annotate_rendered_view_with_filenames = true ใน config/environments/development.rb) · มันแปะคอมเมนต์ HTML (<!-- … --> ข้อความที่เบราว์เซอร์ไม่แสดง) บอกต้นและท้ายของแต่ละไฟล์ · ลองดึงหน้าบทความแรกแล้วกรองเอาเฉพาะป้ายพวกนี้:

$ curl -s localhost:3000/posts/1 | grep -E "BEGIN|END"
<!-- BEGIN app/views/layouts/application.html.erb
      <!-- BEGIN app/views/posts/show.html.erb
  <!-- BEGIN app/views/posts/_post.html.erb
<!-- END app/views/posts/_post.html.erb -->
<!-- END app/views/posts/show.html.erb -->
<!-- END app/views/layouts/application.html.erb -->
layouts/application.html.erb ............ จานรอง (ทุกหน้าใช้) │ <head> … <title> … yield :head … │ ├─ posts/show.html.erb ................ อาหารของเมนูนี้ (วางตรง yield) │ │ <h1>Showing post</h1> │ │ │ ├─ posts/_post.html.erb ............ ถ้วยมาตรฐาน (partial) │ │ Title · Description · Category │ │ │ │ ปุ่ม Edit · Back · Destroy │ └─ (จบ body · จบ html)

มี layout ได้หลายอัน (แค่รู้จักไว้): ถ้าวันหนึ่งหน้าผู้ดูแลระบบต้องหน้าตาต่างจากหน้าผู้อ่าน · เขียน layout "admin" ไว้ใน controller ตัวนั้น แล้ว Rails จะใช้ไฟล์ app/views/layouts/admin.html.erb เป็นจานรองแทน · controller อื่นยังใช้ application เหมือนเดิม · โปรเจกต์เราตอนนี้มีอันเดียว

# app/controllers/admin_controller.rb (ตัวอย่างจากสไลด์หน้า 12 · โปรเจกต์เรายังไม่มีไฟล์นี้)
class AdminController < ApplicationController
  layout "admin"
end

2 · content_for — ฝากป้ายชื่อเมนูขึ้นไปติดหัวจานรอง

ของในไฟล์ show.html.erb ถูกวางลงตรง yield ซึ่งอยู่ใน <body> · แต่ชื่อแท็บเบราว์เซอร์ (<title>) อยู่ใน <head> ข้างบนโน่น · หน้า show จะส่งชื่อบทความขึ้นไปตรงนั้นได้ยังไง?

คำตอบคือ content_for = ฝากของใส่กล่องที่ติดชื่อไว้ · view เขียน "ฝากใส่กล่องชื่อ :title" · layout เขียน "ตรงนี้เอาของในกล่อง :title มาวาง" · ทำแบบนี้ได้เพราะ Rails จัด view ให้เสร็จก่อน แล้วค่อยเอาไปวางในจานรอง — ตอน layout ทำงาน กล่องมีของรออยู่แล้ว

ลำดับนี้เห็นได้จริงใน log ของ server (หน้าต่างที่รัน bin/dev หรือไฟล์ log/development.log) · ตัวอย่างนี้มาจากตอนเปิดหน้า /posts:

  Rendered posts/index.html.erb within layouts/application (Duration: 9.6ms | GC: 4.2ms)
  Rendered layout layouts/application.html.erb (Duration: 10.6ms | GC: 4.2ms)

บรรทัดแรก = "จัด index เสร็จแล้ว (เพื่อวางใน layout application)" · บรรทัดที่สอง = "จัดจานรองเสร็จ" — view มาก่อนเสมอ · ตัวเลขในวงเล็บคือเวลาที่ใช้ (GC = เวลาที่ Ruby เก็บกวาดหน่วยความจำ) ไม่ต้องสน

ลองจริง: ให้แท็บเบราว์เซอร์ขึ้นชื่อบทความ

scaffold ตั้งชื่อหน้าไว้ให้แล้วในบรรทัดแรกของ app/views/posts/show.html.erb · ดูผลก่อนแก้:

<% content_for :title, "Showing post" %>
$ curl -s localhost:3000/posts/1 | grep '<title>'
    <title>Showing post</title>

ทุกบทความได้ชื่อแท็บว่า "Showing post" เหมือนกันหมด · เปิด 5 แท็บก็แยกไม่ออกว่าแท็บไหนบทความอะไร · แก้บรรทัดนั้น (แทนที่ ไม่ใช่เพิ่มบรรทัดใหม่) ให้เป็น:

<% content_for :title, @post.title %>
$ curl -s localhost:3000/posts/1 | grep '<title>'
    <title>ต้มยำกุ้งน้ำข้น</title>

$ curl -s localhost:3000/posts/2 | grep '<title>'
    <title>ก๋วยเตี๋ยวเรืออยุธยา</title>

ฝั่ง layout บรรทัด 4 content_for(:title) || "Wongnok Blog All In One" คือการเปิดกล่องดู · มีของก็ใช้ · กล่องว่างก็ได้ nil แล้ว || หยิบชื่อแอปมาแทน (ผมลองลบบรรทัด content_for ทิ้ง ได้ <title>Wongnok Blog All In One</title> จริง)

⚠️ ถ้าเพิ่มบรรทัดใหม่ใต้บรรทัดเดิมแทนที่จะแก้ — content_for ไม่ได้เขียนทับ แต่ต่อท้ายของที่อยู่ในกล่องอยู่แล้ว · ผมลองมีสองบรรทัด ("Showing post" แล้วตามด้วย @post.title) ได้ <title>Showing postต้มยำกุ้งน้ำข้น</title> · หนึ่งหน้าควรมี content_for :title บรรทัดเดียว

ฝากของที่ยาวกว่าหนึ่งบรรทัด — content_for :head do … end

บรรทัด 12 ของ layout (<%= yield :head %>) คือกล่องอีกใบชื่อ :head · ใช้ฝาก <meta> ที่อยากให้มีเฉพาะบางหน้า · เช่น <meta name="description"> = คำโปรยสั้น ๆ ที่ Google หรือแอปแชตเอาไปโชว์ใต้ลิงก์ · เติมต่อจากบรรทัด title ในไฟล์ show.html.erb:

<% content_for :title, @post.title %>
<% content_for :head do %>
  <meta name="description" content="<%= @post.description %>">
<% end %>
$ curl -s localhost:3000/posts/1 | grep -B3 'name="description"'
<meta name="csrf-token" content="0C_BIl1dCpfY4FWzXI23fn7TsfMv_5doRL0Hpn9qXuxYmswNS4dyO4l6KLVki1UAdhJBxrJyZqUAYTWzZu-V5g" />


      <meta name="description" content="ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อม กุ้งแม่น้ำตัวใหญ่ หอมตะไคร้ ข่า ใบมะกรูด กินกับข้าวสวยร้อน ๆ ได้ทั้งถ้วย ร้านนี้เปิดมาสามสิบปีแล้ว คิวยาวทุกวันเสาร์อาทิตย์">
🏷 เทียบให้เห็นภาพ — ป้ายชื่อเมนูที่ส่งขึ้นไปติดหัวจานรอง

คนจัดจานจัดอาหารของเมนูนี้ (view) เสร็จก่อน แล้วเขียนป้ายชื่อเมนูใส่ช่องฝากของที่ติดชื่อ "title" ไว้ · ตอนวางลงจานรอง (layout) จานรองมีช่องเสียบป้ายอยู่ที่หัวจาน มันหยิบป้ายจากช่อง "title" มาเสียบ · ไม่มีป้ายก็ใช้ป้ายชื่อร้านแทน · ช่องตรงกลางจาน (yield เฉย ๆ) ยังเป็นที่วางอาหารหลักเหมือนเดิม

ที่ไม่เหมือนกัน: ป้ายจริงเสียบใหม่ก็ทับป้ายเก่า · content_for ต่อป้ายใหม่ท้ายป้ายเก่า (ได้ "Showing postต้มยำกุ้งน้ำข้น") · และช่องฝากไม่ได้มีแค่ title — ตั้งชื่อช่องอะไรก็ได้ (:head · :sidebar …) ขอแค่ layout มี yield ชื่อเดียวกันรออยู่ ไม่งั้นของที่ฝากไว้ก็ไม่ได้ขึ้นจาน

เทียบกับ Next.js: ใน บท 83 เราเห็น export const metadata ใน app/layout.tsx — ที่ตั้งชื่อหน้าของ Next.js (เป็น object) · content_for :title คืองานเดียวกันฝั่ง Rails แต่ใช้วิธี "ฝากใส่กล่อง": หน้าลูกเป็นคนบอกชื่อ · layout เป็นคนวาง

🗣 ศัพท์ที่เพิ่งเรียนไป
content_for · named yield (yield :head)
content_for :ชื่อ, ของ หรือ content_for :ชื่อ do … end = ฝากของ · yield :ชื่อ หรือ content_for(:ชื่อ) ใน layout = เอาของออกมาวาง · พิมพ์บอก AI ได้ว่า "set the page title with content_for :title in show.html.erb, don't hardcode it in the layout"
layout ของ controller (layout "admin")
เลือกจานรองคนละแบบให้ controller บางตัว · ไฟล์อยู่ที่ app/views/layouts/ชื่อ.html.erb · ถาม senior ได้ว่า "หน้า admin ควรแยก layout หรือแค่ซ่อนเมนูใน layout เดิม"

3 · partial — แม่พิมพ์ที่ scaffold ทำไว้ให้แล้ว

partial คือไฟล์ view ชิ้นเล็กที่ชื่อขึ้นต้นด้วย _ · ไม่เคยถูกเปิดเป็นหน้าเดี่ยว ๆ ต้องมีไฟล์อื่นเรียกใช้เสมอ · มีไว้ให้ของที่หน้าตาเหมือนกันหลายที่ (กล่องบทความหนึ่งชิ้น · ฟอร์ม) เขียนครั้งเดียว · แก้ที่เดียวเปลี่ยนทุกที่

scaffold สร้าง partial ของบทความไว้แล้ว · เปิดไฟล์ app/views/posts/_post.html.erb ของจริง:

<div id="<%= dom_id post %>" class="w-full sm:w-auto my-5 space-y-5">
  <div>
    <strong class="block font-medium mb-1">Title:</strong>
    <%= post.title %>
  </div>
  <div>
    <strong class="block font-medium mb-1">Description:</strong>
    <%= post.description %>
  </div>
  <div>
    <strong class="block font-medium mb-1">Category:</strong>
    <%= post.category_id %>
  </div>
</div>

แล้วใครเรียกมัน? เปิดสองไฟล์นี้ดู (ตัดมาเฉพาะบรรทัดที่เกี่ยว):

<%# app/views/posts/show.html.erb (บรรทัด 10) %>
<%= render @post %>

<%# app/views/posts/index.html.erb (บรรทัด 15–17 · ในลูปที่วนทีละบทความ) %>
<% @posts.each do |post| %>
  …
    <%= render post %>

ทั้งสองที่ไม่ได้บอกชื่อไฟล์เลย · Rails ดูจากของที่ส่งให้: "นี่คือ object ของ class Post → ไฟล์ต้องชื่อ posts/_post.html.erb → ยื่นของชิ้นนี้เข้าไปในชื่อ post" · นี่คือเหตุผลที่ไฟล์ต้องชื่อ _post เป๊ะ ๆ

🍚 เทียบให้เห็นภาพ — แม่พิมพ์ตักข้าว / ถ้วยน้ำจิ้มมาตรฐาน

ร้านมีแม่พิมพ์ตักข้าวอันเดียว ใช้กับทุกจานที่มีข้าว — ข้าวกะเพรา ข้าวมันไก่ ได้ข้าวทรงเดียวกันหมด · partial _post คือแม่พิมพ์ "กล่องบทความหนึ่งชิ้น" ที่ทั้งหน้ารายการ (30 ครั้ง) และหน้าบทความ (1 ครั้ง) ใช้ร่วมกัน · อยากเปลี่ยนทรง → แก้แม่พิมพ์อันเดียว ทุกจานเปลี่ยนตาม

ที่ไม่เหมือนกัน: แม่พิมพ์จริงไม่สนว่าตักข้าวหอมมะลิหรือข้าวกล้อง · partial ที่เรียกแบบสั้น (render @post) ถูกเลือกจากชนิดของของ — ยื่นบทความ ได้แม่พิมพ์ _post · ยื่นหมวด ได้ _category · ชื่อไฟล์ผิดแม้ตัวเดียว Rails ก็หาไม่เจอ

เทียบกับ React: partial ≈ component (บท 71) · <%= render post %> ≈ <PostCard post={post} /> · ต่างกันที่ React ต้องเขียนชื่อ component เสมอ ส่วน Rails เดาชื่อไฟล์จาก class ของ object ได้

4 · render @posts — บรรทัดเดียวที่ Rails เดาให้ 4 อย่าง

หน้ารายการใน scaffold เขียนลูป each เอง เพราะต้องวางปุ่ม Show/Edit/Destroy ข้างแต่ละบทความ · แต่ถ้าแค่อยากวางกล่องบทความเรียงกัน มีวิธีเขียนแบบยาวกับแบบสั้นที่ได้ผลเหมือนกัน

ทดลอง: ลบทุกอย่างใน app/views/posts/index.html.erb แล้วใส่แบบยาวแทน (ปุ่มต่าง ๆ จะหายไปชั่วคราว จบบทเรา git restore คืน):

แบบยาว — วนเอง ยื่นของเอง

<% @posts.each do |post| %>
  <%= render "post", post: post %>
<% end %>

แบบสั้น — Rails เดาให้หมด

<%= render @posts %>

ภาพในร้าน: แบบยาว = คนจัดจานหยิบแม่พิมพ์ตักข้าวทีละจาน บอกทุกรอบว่า "จานนี้นะ" · แบบสั้น = ยื่นถาดทั้ง 30 จานให้แล้วบอกคำเดียวว่า "ตักให้ทุกจาน" — Rails รู้เองว่าต้องใช้แม่พิมพ์ไหน

ใส่แบบยาวก่อน แล้วสั่งคำสั่งข้างล่าง · จากนั้นเปลี่ยนเป็นแบบสั้น แล้วสั่งซ้ำ · ทุกกล่องมีบรรทัด id="post_…" หนึ่งบรรทัด (มาจาก dom_id ในแม่พิมพ์) · grep -c = นับจำนวนบรรทัดที่เจอ:

$ curl -s localhost:3000/posts | grep -c 'id="post_'
30

ทั้งสองแบบได้ 30 เท่ากัน · แต่ log ของ server เล่าไม่เหมือนกัน:

log ตอนใช้แบบยาว (มี 30 บรรทัด ตัดมาให้ดู 3)

  Rendered posts/_post.html.erb (Duration: 0.2ms | GC: 0.0ms)
  Rendered posts/_post.html.erb (Duration: 0.0ms | GC: 0.0ms)
  Rendered posts/_post.html.erb (Duration: 0.0ms | GC: 0.0ms)
  …

log ตอนใช้แบบสั้น

  Rendered collection of posts/_post.html.erb [30 times] (Duration: 0.7ms | GC: 0.2ms)

แบบยาว = เรียกแม่พิมพ์ 30 ครั้งแยกกัน · แบบสั้น = Rails รู้ว่านี่คือรายการ (collection) จึงจัดการรวดเดียวแล้วรายงานว่า "[30 times]" · สิ่งที่ render @posts เดาให้มี 4 อย่าง:

#Rails เดาอะไรถ้าเขียนแบบยาวต้องทำเอง
1ของในรายการเป็น Post → ใช้ไฟล์ posts/_post.html.erbเขียน "post"
2วนให้ทีละชิ้นเองเขียน each do |post| … end
3ยื่นของแต่ละรอบเข้าไปในชื่อ post (ตั้งตามชื่อไฟล์)เขียน post: post
4รายการว่าง → ไม่วางอะไรเลย และคืน nilเขียน if @posts.any? … else เอง (แบบที่ scaffold ทำ)

เทียบกับ React: <%= render @posts %> ≈ {posts.map(post => <PostCard key={post.id} post={post} />)} (key = ป้ายที่ React ใช้แยกแต่ละชิ้นในรายการ) · .map คือ "แปลงของทุกชิ้นในรายการ" (คล้าย each แต่ได้รายการใหม่กลับมา) · => คือการเขียนฟังก์ชันสั้น ๆ "รับ post แล้วคืน PostCard" · ฝั่ง React ต้องเขียนลูปเองทุกครั้ง

# ทดลองให้รายการว่าง: ใน app/controllers/posts_controller.rb ตรง def index
# แก้บรรทัด @posts = Post.all ชั่วคราวเป็น
@posts = Post.where(title: "ไม่มีจริง")   # ไม่มีบทความไหนชื่อนี้ → ได้รายการว่าง

<%# app/views/posts/index.html.erb — สามบรรทัดนี้แทนของเดิม %>
<p>A: <%= render(@posts).inspect %></p>
<p>B: <%= render(@posts) || "ไม่มีบทความ" %></p>
<p>C: <%= render @posts || "ไม่มีบทความ" %></p>

.inspect = "แสดงค่าตามที่ Ruby เห็น" (เช่น nil จะขึ้นเป็นคำว่า nil ไม่ใช่ช่องว่าง) · หน้า /posts จะขึ้นอะไรหลัง A: · B: · C: — สังเกตว่า B กับ C ต่างกันแค่วงเล็บ

$ curl -s localhost:3000/posts | grep -E '<p>[ABC]:'
--><p>A: nil</p>
<p>B: ไม่มีบทความ</p>
<p>C: </p>
  • --> หน้าบรรทัดแรกคือท้ายป้าย <!-- BEGIN app/views/posts/index.html.erb (หัวข้อ 1) — ไม่ใช่ของเรา
  • A: nil — รายการว่าง render ไม่วางอะไรเลย และคืน nil (ไม่ใช่ข้อความว่าง) · คู่มือ Rails เขียนไว้ตรง ๆ ว่าตั้งใจให้ใช้ท่า B ได้
  • B: ไม่มีบทความ — nil || "ไม่มีบทความ" = "ถ้าไม่มี ใช้ตัวขวา" (บท 94) · ท่านี้แทน if … else ทั้งก้อนได้
  • C: ว่างเปล่า — ไม่มีวงเล็บ Ruby อ่านเป็น render(@posts || "ไม่มีบทความ") · @posts เป็นรายการ (แม้ว่างก็ไม่ใช่ nil) || จึงเลือก @posts แล้ว render ได้ nil · ข้อความสำรองไม่เคยถูกใช้

ดูเสร็จแล้วคืนไฟล์ controller ด้วย git restore app/controllers/posts_controller.rb แล้วใส่แบบสั้น <%= render @posts %> กลับใน index เพื่อลองหัวข้อถัดไป

ของแถม — post_counter (ได้เฉพาะแบบสั้น)

ตอน render เป็นรายการ Rails แถมตัวแปรอีกตัวชื่อ post_counter = "นี่คือชิ้นที่เท่าไหร่" นับจาก 0 · ชื่อมาจากชื่อไฟล์ + _counter · ลองเพิ่มบรรทัดที่ 2 ใน _post.html.erb (index ยังเป็น <%= render @posts %>):

<div id="<%= dom_id post %>" class="w-full sm:w-auto my-5 space-y-5">
  <p>ลำดับที่ <%= post_counter %></p>
  …
$ curl -s localhost:3000/posts | grep 'ลำดับที่' | head -3
  <p>ลำดับที่ 0</p>
  <p>ลำดับที่ 1</p>
  <p>ลำดับที่ 2</p>

$ curl -s localhost:3000/posts | grep 'ลำดับที่' | tail -1
  <p>ลำดับที่ 29</p>

head -3 = เอา 3 บรรทัดแรก · tail -1 = เอาบรรทัดสุดท้าย · 30 บทความ นับ 0 ถึง 29

<%# _post.html.erb ยังมีบรรทัด ลำดับที่ <%= post_counter %> อยู่ %>
<%# show.html.erb เรียกแม่พิมพ์ตัวเดียวกันด้วย %>
<%= render @post %>

เปิด localhost:3000/posts/1 จะเห็น "ลำดับที่ 0" · "ลำดับที่" เปล่า ๆ · หรืออย่างอื่น?

$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/posts/1
500

ใน log:

ActionView::Template::Error (undefined local variable or method 'post_counter' for an instance of #<Class:0x0000000129e72f00>)
  • #<Class:0x…> = ชื่อภายในที่ Rails ตั้งให้ view ตอนทำงาน ไม่ต้องสน (เลขของคุณจะต่าง) · ส่วนสำคัญคือ 'post_counter'
  • -o /dev/null = ทิ้งเนื้อหาหน้า · -w '%{http_code}\n' = พิมพ์แค่ status · 500 = server พัง (เบราว์เซอร์จะขึ้นหน้า error สีแดงของ Rails)
  • render @post คือ render ของชิ้นเดียว ไม่ใช่รายการ จึงไม่มีใครนับ · post_counter ไม่ถูกสร้าง · "undefined local variable" = "ไม่รู้จักตัวแปรชื่อนี้"
  • หน้า index ตัวเดิมของ scaffold (ที่วน each แล้ว render post ทีละตัว) ก็พังแบบเดียวกัน — ผมลองแล้ว · มีแค่การ render เป็นรายการที่ได้ post_counter

ลบบรรทัด "ลำดับที่" ออกจาก _post.html.erb ก่อนไปหัวข้อถัดไป (หรือ git restore app/views/posts/_post.html.erb)

🗣 ศัพท์ที่เพิ่งเรียนไป
partial · rendering a collection
partial = ไฟล์ view ชื่อขึ้นต้น _ · render @posts = render รายการ (collection) ใช้ partial ตามชื่อ model วนให้เอง · พิมพ์บอก AI ได้ว่า "replace this each loop with render @posts and keep the partial name posts/_post"
post_counter (counter variable)
เลขลำดับของชิ้นในรายการ เริ่มที่ 0 · มีเฉพาะตอน render เป็นรายการ · ถาม senior ได้ว่า "partial นี้ถูกเรียกแบบ collection ทุกที่ไหม ถึงจะใช้ post_counter ได้"

5 · locals กับ @ตัวแปร — ยื่นใส่มือ หรือให้ไปควานหาบนถาดเอง

partial รับข้อมูลได้สองทาง: ยื่นเข้าไปตอนเรียก (local เช่น post) กับไปหยิบ @post ที่ controller วางไว้บนถาดเอง · ทางที่สองดูง่ายกว่า แต่ @post มีอยู่เฉพาะหน้าที่ action ของ controller ตั้งค่าไว้เท่านั้น

ลองทำผิดดูก่อน · ขั้นที่ 1 คืน index เป็นของ scaffold เดิม: git restore app/views/posts/index.html.erb · ขั้นที่ 2 ในไฟล์ _post.html.erb เปลี่ยน <%= post.title %> เป็น <%= @post.title %> บรรทัดเดียว

$ curl -s localhost:3000/posts/1 | grep -A1 'Title:'
    <strong class="block font-medium mb-1">Title:</strong>
    ต้มยำกุ้งน้ำข้น

$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/posts
500

หน้าบทความยังทำงาน · หน้ารายการพัง · เบราว์เซอร์ขึ้นหัวหน้า error ว่า NoMethodError in Posts#index และชี้ไปที่ _post.html.erb พร้อมข้อความ:

undefined method 'title' for nil
🤲 เทียบให้เห็นภาพ — ยื่นของใส่มือแม่พิมพ์ vs แม่พิมพ์ที่ไปควานหาบนถาดเอง

local = คนจัดจานยื่นข้าวใส่มือแม่พิมพ์ชัด ๆ ทุกครั้ง ("นี่ข้าว 1 ทัพพี") · แม่พิมพ์ใช้ได้ทุกโต๊ะ ทุกเมนู ขอแค่มีคนยื่นข้าวให้ · @post ใน partial = แม่พิมพ์ที่เดินไปควานหาของบนถาดหน้าครัวเอง · ใช้ได้เฉพาะครัวที่บังเอิญวางของชื่อนั้นไว้ · ไปใช้ครัวอื่นที่วางแค่ "ตะกร้าบทความ" (@posts) ก็หาไม่เจอ ได้มือเปล่า (nil) กลับมา

ที่ไม่เหมือนกัน: แม่พิมพ์จริงหาของไม่เจอก็รู้ตัวและหยุด · partial ไม่ได้ฟ้องว่า "ไม่มี @post" · มันได้ nil แล้วทำต่อจนไปพังที่ .title · ถ้าบรรทัดนั้นแค่ <%= @post %> เฉย ๆ จะได้ช่องว่างเงียบ ๆ ไม่มี error ให้เห็นเลย

เทียบกับ React: local = props ที่ยื่นเข้าไปตอนเรียก (<PostCard post={post} />) · @post = ตัวแปรกลาง (global) ของ request นี้ ที่ทุกไฟล์มองเห็น · React บังคับให้ส่งของผ่าน props ด้วยเหตุผลเดียวกัน — เปิดไฟล์ component แล้วรู้ทันทีว่ามันต้องการอะไร และเอาไปวางที่ไหนก็ได้

✅ กฎที่ใช้ (สไลด์หน้า 14):
• ไฟล์หน้าหลัก (index · show · new · edit) ใช้ @ ได้ตามสบาย — มันคู่กับ action ของมันอยู่แล้ว
• partial ห้ามแตะ @ — รับของผ่าน local เท่านั้น เพราะมันถูกสร้างมาเพื่อใช้ซ้ำหลายที่
scaffold ทำตามกฎนี้อยู่แล้ว: หน้า new.html.erb เรียก <%= render "form", post: @post %> — หยิบ @post จากถาดในไฟล์หน้าหลัก แล้วยื่นเข้าไปในชื่อ post

🗣 ศัพท์ที่เพิ่งเรียนไป
locals (local variables ของ partial)
ของที่ยื่นเข้า partial ตอนเรียก เช่น render "post", post: x · มองเห็นเฉพาะในไฟล์ partial นั้น · พิมพ์บอก AI ได้ว่า "this partial must not read instance variables; pass everything it needs as locals"
instance variable (@ivar) ใน view
ตัวแปรที่ controller ตั้งไว้ (ของบนถาด) · ทุก view ใน request เดียวกันมองเห็น · ตัวที่ไม่มีใครตั้ง = nil ไม่ใช่ error · เวลาตรวจโค้ด ถาม senior ได้ว่า "partial ตัวไหนยังแอบอ่าน @ อยู่บ้าง" — ค้นง่าย ๆ ด้วย grep -n "@" app/views/*/_*.erb (ในโปรเจกต์ตอนนี้ไม่ขึ้นอะไรเลย = ยังไม่มี partial ไหนแอบอ่าน @)

6 · strict locals — ป้ายติดแม่พิมพ์ "ต้องยื่นอะไรบ้าง"

ตอนนี้ _post.html.erb ไม่ได้บอกไว้ตรงไหนเลยว่า "ต้องยื่น post ให้ฉันนะ" · ต้องอ่านทั้งไฟล์ถึงจะรู้ · Rails ให้เราเขียนบรรทัดประกาศไว้บนสุดของ partialได้ว่ารับอะไรบ้าง แล้ว Rails จะตรวจทุกครั้งที่มีคนเรียก เรียกว่า strict locals

คืน _post.html.erb เป็นของเดิมก่อน (git restore app/views/posts/_post.html.erb) แล้วเพิ่มบรรทัดนี้เป็นบรรทัดแรก:

<%# locals: (post:) %>
<div id="<%= dom_id post %>" class="w-full sm:w-auto my-5 space-y-5">
  …

หน้าเดิมยังทำงานปกติ (เพราะทุกที่ยื่น post ให้อยู่แล้ว):

$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/posts
200
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/posts/1
200

ทีนี้ลองเรียกผิดสองแบบใน show.html.erb (แก้บรรทัด <%= render @post %> ทีละแบบ) แล้วเทียบกับตอนไม่มีบรรทัดประกาศ:

เรียกแบบนี้ไม่มีบรรทัด locals:มี <%# locals: (post:) %>
render "post"
(ลืมยื่น post)
500 · undefined local variable or method 'post' for an instance of #<Class:0x…>
— บอกแค่ว่า "ไม่รู้จัก post" ต้องเดาเองว่าลืมส่ง
500 · missing local: :post for app/views/posts/_post.html.erb
— บอกตรง ๆ ว่า "ขาด post" ที่ partial ไหน
render "post", post: @post, color: "red"
(ยื่นของที่ไม่ได้ขอ)
200 · ไม่มีอะไรเกิดขึ้นเลย · color ถูกทิ้งเงียบ ๆ 500 · unknown local: :color for app/views/posts/_post.html.erb
— "ไม่รู้จัก color"

แถวที่สองสำคัญที่สุด · สมมติจะยื่น show_category: (local ที่เราจะเพิ่มในหัวข้อย่อยถัดไป) แต่พิมพ์ผิดเป็น show_categroy: · ไม่มี strict locals = ไม่มีใครบอก แค่ไม่ทำงาน · มี strict locals = เปิดหน้าแล้วรู้ทันที · ในเบราว์เซอร์จะขึ้นหัวหน้า error ว่า ActionView::StrictLocalsError

local ที่ไม่บังคับ — ใส่ค่าเริ่มต้นไว้

เขียน <%# locals: (post:, show_category: false) %> = "post ต้องส่ง · show_category จะส่งหรือไม่ก็ได้ ถ้าไม่ส่งให้ถือว่า false" · เหมือน keyword argument ที่มีค่าเริ่มต้นในบท 93 ทุกอย่าง · ภารกิจข้างล่างจะใช้ท่านี้

⚠️ ใช้ strict locals แล้ว post_counter หายไป — ผมลอง (post:) คู่กับ render @posts แล้วได้ undefined local variable or method 'post_counter' · ถ้าจะใช้ต้องประกาศด้วย เช่น (post:, post_counter: nil) (ลองแล้วได้ 0, 1, 2… ตามเดิม)

📋 เทียบให้เห็นภาพ — ป้ายติดแม่พิมพ์

strict locals คือป้ายติดข้างแม่พิมพ์: "ต้องยื่น: ข้าว · ไม่บังคับ: ไข่ดาว (ไม่ยื่น = ไม่ใส่)" · คนจัดจานคนใหม่ไม่ต้องลองผิดลองถูก · และแม่พิมพ์ปฏิเสธทันทีถ้าลืมยื่นข้าว หรือยื่นของที่ป้ายไม่ได้เขียนไว้ (เช่น "ซอสแดง")

ที่ไม่เหมือนกัน: ป้ายจริงคนอ่านก่อนลงมือ · strict locals ตรวจตอนมีคนเปิดหน้านั้นจริงเท่านั้น — หน้าที่ยังไม่มีใครเปิด เรียกผิดก็ยังไม่มีใครรู้ · ต่างจาก TypeScript ที่ tsc ฟ้องตั้งแต่ก่อนรัน

เทียบกับ React + TypeScript: <%# locals: (post:, show_category: false) %> ≈ type ของ props ในบท 71 · type PostCardProps = { post: Post; showCategory?: boolean } (? = ไม่บังคับ) · บรรทัดเดียวบอกได้ว่า partial นี้รับอะไร โดยไม่ต้องอ่านทั้งไฟล์

🗣 ศัพท์ที่เพิ่งเรียนไป
strict locals (<%# locals: (…) %>)
บรรทัดแรกของ partial ที่ประกาศว่ารับ local อะไร · ขาด = missing local · เกิน = unknown local · พิมพ์บอก AI ได้ว่า "add strict locals to every partial you create, with defaults for optional ones"
default value ของ local
show_category: false ในวงเล็บ = ไม่ส่งมาก็ได้ ใช้ค่านี้แทน · ถาม senior ได้ว่า "local ตัวนี้ควรบังคับส่ง หรือมีค่าเริ่มต้นดีกว่า" — คำถามเดียวกับการออกแบบ props ใน React

ภารกิจ — กล่องบทความที่โชว์หมวดได้ตามสั่ง

เป้าหมาย: หน้ารายการ (/posts) ไม่ต้องโชว์หมวด (ให้รายการสั้นลง) · หน้าบทความ (/posts/1) โชว์ชื่อหมวด (ไม่ใช่เลข) · ใช้แม่พิมพ์ _post อันเดียวกัน · ใช้เวลาราว 10 นาที

  1. คืนทุกอย่างเป็นของเดิมก่อน: git restore app/views app/controllers แล้ว git status ต้องขึ้น nothing to commit, working tree clean
  2. ใน app/views/posts/_post.html.erb:
    • เพิ่มบรรทัดแรก <%# locals: (post:, show_category: false) %>
    • ครอบกล่อง Category (<div> สามบรรทัดที่มีคำว่า Category:) ด้วย <% if show_category %> ข้างบน และ <% end %> ข้างล่าง
    • เปลี่ยน post.category_id เป็น post.category.name (ชื่อหมวดจาก association · บท 115)
  3. ใน app/views/posts/show.html.erb เปลี่ยน <%= render @post %> เป็น <%= render @post, show_category: true %> · ไม่ต้องแตะ index
  4. ตรวจด้วยสามคำสั่งนี้:
    curl -s localhost:3000/posts/1 | grep -A1 'Category:'
    curl -s localhost:3000/posts | grep -c 'Category:'
    curl -s localhost:3000/posts | grep -c 'id="post_'

ควรเห็น: คำสั่งแรกได้ Category: ตามด้วย อาหารไทย · คำสั่งที่สองได้ 0 (หน้ารายการไม่มีกล่องหมวดเลย) · คำสั่งที่สามได้ 30 (บทความยังครบ)

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

เห็นอะไรแปลว่า
หน้า /posts พัง · missing local: :show_category for app/views/posts/_post.html.erb ลืมใส่ค่าเริ่มต้น เขียนเป็น (post:, show_category:) → index ไม่ได้ส่งมา Rails จึงถือว่าขาด · เติม : false (ผมลองแล้วได้ error นี้จริง)
หน้า /posts/1 พัง · unknown local: :show_categroy for … สะกดชื่อใน show.html.erb ผิด · ดูชื่อหลัง unknown local: แล้วเทียบกับบรรทัดประกาศ
หน้า /posts/1 ไม่พัง แต่ไม่มีกล่อง Category บรรทัด render ใน show ยังไม่ได้ส่ง show_category: true → ใช้ค่าเริ่มต้น false
Category ขึ้นเป็นเลข 1 ยังเป็น post.category_id อยู่ · เปลี่ยนเป็น post.category.name
<%# app/views/posts/_post.html.erb %>
<%# locals: (post:, show_category: false) %>
<div id="<%= dom_id post %>" class="w-full sm:w-auto my-5 space-y-5">
  <div>
    <strong class="block font-medium mb-1">Title:</strong>
    <%= post.title %>
  </div>
  <div>
    <strong class="block font-medium mb-1">Description:</strong>
    <%= post.description %>
  </div>
  <% if show_category %>
    <div>
      <strong class="block font-medium mb-1">Category:</strong>
      <%= post.category.name %>
    </div>
  <% end %>
</div>

<%# app/views/posts/show.html.erb (บรรทัด 10) %>
<%= render @post, show_category: true %>

ผลรันจริง:

$ curl -s localhost:3000/posts/1 | grep -A1 'Category:'
      <strong class="block font-medium mb-1">Category:</strong>
      อาหารไทย

$ curl -s localhost:3000/posts | grep -c 'Category:'
0

$ curl -s localhost:3000/posts | grep -c 'id="post_'
30
  • render @post, show_category: true = แบบสั้น (Rails เดาไฟล์และชื่อ post ให้) แล้วยื่นของเพิ่มหนึ่งชิ้น · เขียนแบบยาว render "post", post: @post, show_category: true ก็ได้ผลเดียวกัน (ผมลองแล้ว)
  • index ไม่ต้องแก้เลย เพราะ render post ไม่ส่ง show_category → ได้ค่าเริ่มต้น false · นี่คือข้อดีของค่าเริ่มต้น: เพิ่มความสามารถใหม่โดยไม่ต้องไล่แก้ทุกที่ที่เรียก
  • ทำไมไม่โชว์หมวดในหน้ารายการ? นอกจากรายการสั้นลง · post.category.name ในลูป 30 รอบคือ N+1 (บท 114) — เรื่องนี้กลับมาอีกทีในบท 125

เสร็จแล้วคืนไฟล์ทิ้ง: git restore app/views · Lab บท 126 จะทำ partial ตัวจริงตามแบบที่ตกลงกันในคอร์ส

ทดสอบความเข้าใจ

1. หน้า show มีบรรทัด <% content_for :title, "Showing post" %> อยู่แล้ว · เพื่อนเพิ่มบรรทัด <% content_for :title, @post.title %> ไว้ข้างใต้ (ไม่ได้ลบบรรทัดเดิม) · แท็บเบราว์เซอร์ของบทความที่ 1 จะขึ้นว่าอะไร?

ตัวเลือกที่ 3 ถูก · ผมลองจริงได้ <title>Showing postต้มยำกุ้งน้ำข้น</title> · content_for คือ "เติมใส่กล่อง" ไม่ใช่ "เปลี่ยนของในกล่อง" ของสองชิ้นจึงติดกัน

ตัวเลือกที่ 1 ผิด — ไม่มีการเขียนทับ ของชิ้นแรกยังอยู่ · ตัวเลือกที่ 2 ผิด — layout เอาของทั้งหมดในกล่องมาวาง ไม่ได้หยิบแค่ชิ้นแรก · ตัวเลือกที่ 4 ผิด — ชื่อแอปใช้เฉพาะตอนกล่องว่าง (nil) · กล่องนี้มีของสองชิ้น

จุดที่ต้องจำ: หนึ่งหน้า หนึ่ง content_for :title · จะเปลี่ยนชื่อหน้า ให้แก้บรรทัดเดิม อย่าเพิ่มบรรทัดใหม่

2. partial _post.html.erb เขียน <%= @post.title %> · หน้า /posts/1 ทำงานปกติ แต่หน้า /posts ขึ้น undefined method 'title' for nil · เพราะอะไร?

ตัวเลือกที่ 1 ถูก · @post มีค่าเฉพาะใน request ที่ action ตั้งไว้ (show ตั้งผ่าน set_post) · index ตั้งแค่ @posts · ตัวแปร @ ที่ไม่มีใครตั้งคือ nil แล้ว nil.title ก็พัง

ตัวเลือกที่ 2 ผิด — Rails ไม่ได้ห้าม partial อ่าน @ (หน้า show ยังอ่านได้จริง) · "ห้าม" เป็นกฎที่เราตั้งเอง · ตัวเลือกที่ 3 ผิด — index โหลดครบ 30 บทความแล้ว (กล่องบทความยังถูกเรียก 30 ครั้ง) · แค่อยู่ใน @posts ไม่ใช่ @post · ตัวเลือกที่ 4 ผิด — post กับ @post เป็นตัวแปรคนละตัวกัน Ruby ไม่สับสน

จุดที่ต้องจำ: partial ที่อ่าน @ ใช้ได้แค่หน้าที่บังเอิญตั้ง @ นั้นไว้ · ยื่นของผ่าน local แล้วแม่พิมพ์ไปได้ทุกที่

3. อยากให้หน้ารายการขึ้นว่า "ไม่มีบทความ" ตอนไม่มีบทความเลย · บรรทัดไหนได้ผลตามต้องการ?

ตัวเลือกที่ 2 ถูก · รายการว่าง render(@posts) คืน nil แล้ว || หยิบข้อความสำรอง · ผมลองได้ <p>B: ไม่มีบทความ</p>

ตัวเลือกที่ 1 ผิด — ไม่มีวงเล็บ Ruby อ่านเป็น render(@posts || "…") · รายการว่างไม่ใช่ nil จึงไม่เคยถึงข้อความสำรอง · ผมลองได้ <p>C: </p> ว่างเปล่า · ตัวเลือกที่ 3 ผิด — <% %> ไม่มี = คือทำเงียบ ๆ ไม่ขึ้นจาน (บท 122) ทั้งบทความและข้อความสำรองหายหมด · ตัวเลือกที่ 4 ผิด — ของชิ้นที่สองของ render คือ locals ที่ยื่นเข้า partial (ต้องเป็น hash แบบ show_category: true) ไม่ใช่ข้อความสำรอง · ผมลองแล้ว ตอนรายการว่างหน้าไม่พังแต่ไม่ขึ้นอะไรเลย · พอมีบทความ หน้าพังด้วย undefined method 'keys' for an instance of String

จุดที่ต้องจำ: render รายการว่างคืน nil · ใช้คู่กับ || ได้ แต่ต้องใส่วงเล็บให้ render

4. _post.html.erb ขึ้นต้นด้วย <%# locals: (post:, show_category: false) %> · การเรียกแบบไหนทำให้หน้าพัง?

ตัวเลือกที่ 4 ถูก · เรียกด้วยชื่อไฟล์ "post" แต่ไม่ได้ยื่น post ให้ · post: ไม่มีค่าเริ่มต้น = บังคับ → missing local: :post for app/views/posts/_post.html.erb

ตัวเลือกที่ 1 ผิด — แบบสั้นยื่น post ให้เอง และ show_category ใช้ค่าเริ่มต้น false · ตัวเลือกที่ 2 ผิด — ยื่นครบและชื่อตรงกับที่ประกาศ (นี่คือเฉลยภารกิจ) · ตัวเลือกที่ 3 ผิด — ยื่น post ครบ · ไม่ส่ง show_category ก็ได้เพราะมีค่าเริ่มต้น

จุดที่ต้องจำ: local ที่ไม่มีค่าเริ่มต้น = ต้องส่งทุกครั้ง · แบบสั้น (render @post) ส่ง local ที่ชื่อตามไฟล์ให้เอง · แบบยาว (render "post") ต้องส่งเอง

ถามกลับได้เลย — ถ้าเจอหน้า error สีแดงแล้วไม่รู้ว่าพังที่ไฟล์ไหน ให้ก็อปหัวหน้า error (เช่น ActionView::StrictLocalsError in Posts#index) · ข้อความบรรทัดถัดมา และบรรทัด render ที่เรียก partial นั้นมาถาม · สามอย่างนี้บอกได้เกือบทุกครั้งว่ายื่นของผิดตรงไหน

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

หน้าสไลด์เขียนของจริง (Rails 8.1.4 · โปรเจกต์ของคอร์ส)ต้องสนใจไหม
12layout มี render "shared/navbar" · "shared/footer" layout ของจริงยังไม่มี และยังไม่มีไฟล์ใน shared/ · พิมพ์ตามได้ Missing partial shared/_navbar ใช่ — อย่าเพิ่งเติม · navbar มาใน Lab บท 129
12stylesheet_link_tag "application" ของจริงเป็น stylesheet_link_tag :app และมีบรรทัด meta/ไอคอนอีกหลายบรรทัด ไม่ต้อง — บท 127
13_post เป็น <article class="card"> มี truncate และ post.category.name ไฟล์ _post ที่ scaffold สร้างหน้าตาคนละแบบ (Title/Description/Category เป็นเลข) รู้ไว้ — Lab บท 126 ทำแบบสไลด์ · truncate อยู่บท 124
13post_counter "ได้มาฟรีในทุกรอบ" ได้เฉพาะตอน render เป็นรายการ · render post ในลูป หรือ render @post ได้ error · ใช้ strict locals ต้องประกาศเพิ่ม ใช่ — หัวข้อ 4 · 6
14"Rails 8 บังคับได้ด้วย" (strict locals) มีมาตั้งแต่ Rails 7.1 (release notes ของ 7.1) · ใช้ใน Rails 8 ได้ตามสไลด์ ไม่ต้อง

อ้างอิง