🔁 ทวนก่อนเริ่ม — คำพวกนี้จะโผล่ในบทนี้ทั้งหมด
- layout ·
<%= yield %>(บท 103) — จานรองลายเดียวกันที่ครอบทุกหน้า ·yieldคือช่องกลางจานที่ view ของแต่ละหน้าถูกวางลงไป <%= %>กับ<% %>·<%# %>(บท 122) — เติมแล้ววางบนจาน · ทำในครัวเงียบ ๆ ไม่ขึ้นจาน · คอมเมนต์@post(บท 102) — ของที่หัวหน้าเชฟ (controller) วางไว้บนถาดหน้าครัวให้คนจัดจาน (view) หยิบ · ถ้าไม่มีใครวาง =nil- partial ·
render "form", post: @post(บท 118) — ไฟล์ view ชื่อขึ้นต้นด้วย_ที่ใช้ซ้ำได้ · หน้า new กับ edit เรียกฟอร์มชิ้นเดียวกัน - props (บท 71) — ของที่ยื่นเข้าไปให้ component ตอนเรียก เช่น
<Button disabled>· และ keyword argument (บท 93) — ส่งของโดยบอกชื่อช่องmake_post(title: "…")
🧰 เวอร์ชันที่ใช้ตรวจ: 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 -->
curl -s= ขอหน้าเว็บแบบเงียบ ๆ (ไม่โชว์แถบความคืบหน้า) ·| grep -E "BEGIN|END"= เลือกเฉพาะบรรทัดที่มีคำว่า BEGIN หรือ END- ป้าย BEGIN ไม่มี
-->ปิดท้าย เพราะ Rails ไปปิดไว้บรรทัดถัดไป · grep เลือกมาแค่บรรทัดที่มีคำว่า BEGIN จึงไม่เห็นตัวปิด - หนึ่งหน้าเว็บประกอบจากสามไฟล์ซ้อนกัน: layout ครอบ
show.html.erbและในนั้นยังมี_post.html.erbอีกชั้น - ป้ายนี้โผล่เฉพาะตอนพัฒนาในเครื่อง · ใช้มันได้ทั้งบทเวลาสงสัยว่า "ข้อความตรงนี้มาจากไฟล์ไหน"
มี 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 %>
<% %>ไม่มี=— เพราะเราไม่ได้อยากให้ชื่อโผล่ตรงนี้ในเนื้อหน้า แค่ฝากไว้ (บท 122: ทำในครัวเงียบ ๆ ไม่ขึ้นจาน)content_for :title, …= "ฝากของชิ้นนี้ใส่กล่องชื่อ:title" (:titleคือ symbol · บท 92)@post.title= ชื่อบทความที่ controller วางไว้บนถาด
$ 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 %>
content_for :head do…end= ฝากของทั้งก้อนระหว่าง do กับ end (block · บท 93) · ใช้เมื่อของที่ฝากเป็น HTML หลายบรรทัด<%= @post.description %>ข้างใน = เติมคำบรรยายลงในช่องcontent="…"ก่อนฝาก
$ curl -s localhost:3000/posts/1 | grep -B3 'name="description"'
<meta name="csrf-token" content="0C_BIl1dCpfY4FWzXI23fn7TsfMv_5doRL0Hpn9qXuxYmswNS4dyO4l6KLVki1UAdhJBxrJyZqUAYTWzZu-V5g" />
<meta name="description" content="ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อม กุ้งแม่น้ำตัวใหญ่ หอมตะไคร้ ข่า ใบมะกรูด กินกับข้าวสวยร้อน ๆ ได้ทั้งถ้วย ร้านนี้เปิดมาสามสิบปีแล้ว คิวยาวทุกวันเสาร์อาทิตย์">
grep -B3= โชว์บรรทัดที่เจอ พร้อม 3 บรรทัดก่อนหน้า (B = before) — จะได้เห็นว่ามันไปอยู่ตรงไหน- บรรทัดแรกคือ csrf-token ที่
csrf_meta_tags(บรรทัด 9) พิมพ์ออกมา · บรรทัดว่างสองบรรทัดคือcsp_meta_tagที่ไม่พิมพ์อะไร กับบรรทัด 11 ที่ว่างอยู่แล้ว · แล้วถึง meta ของเรา = ตำแหน่งของyield :head(บรรทัด 12) พอดี <meta>ไม่โผล่บนหน้าเว็บ (มันอยู่ใน head) · หน้า/postsไม่ได้ฝากอะไรไว้ กล่อง:headของหน้านั้นจึงว่าง
คนจัดจานจัดอาหารของเมนูนี้ (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>
post(ไม่มี @) = บทความหนึ่งชิ้นที่ "ถูกยื่นเข้ามา" ตอนเรียก partial · เรียกว่า local (ตัวแปรเฉพาะในไฟล์นี้) · หัวข้อ 5 จะอธิบายว่าทำไมต้องไม่มี @dom_id post= helper สร้าง id ให้กล่อง เช่นpost_1(ป้ายเลขถ้วย — ได้ใช้จริงในบท 131)post.category_id= โชว์เลขหมวด (เช่น1) ไม่ใช่ชื่อหมวด — scaffold ไม่รู้ว่าเราอยากเห็นอะไร · ภารกิจท้ายบทจะลองเปลี่ยน- class ยาว ๆ เป็น Tailwind (บท 72) — เรื่องหน้าตา ข้ามไปก่อนได้ (บท 128 ว่ากันละเอียด)
แล้วใครเรียกมัน? เปิดสองไฟล์นี้ดู (ตัดมาเฉพาะบรรทัดที่เกี่ยว):
<%# 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 %>
render "post", post: post= "ใช้แม่พิมพ์ชื่อpost(ไฟล์_post· ไม่ต้องพิมพ์ขีดล่าง) แล้วยื่นpostของรอบนี้เข้าไปในชื่อpost" ·post: postคือ keyword argument (บท 93) — ซ้ายคือชื่อช่องในแม่พิมพ์ · ขวาคือของที่ยื่น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
grep -A1= บรรทัดที่เจอ + 1 บรรทัดหลังถัดไป (A = after)- หน้า show: action
showวาง@postไว้บนถาด (ผ่านbefore_action :set_post· ท่าเดียวกับset_categoryในบท 118) → partial ไปหยิบเจอ - หน้า index: action
indexวางแค่@posts(มี s) ·@postไม่มีใครวาง จึงเป็นnil(บท 102) · แล้วถามnilว่า "ชื่อเรื่องอะไร" → "nil ไม่มี method ชื่อ title" - ที่น่ากลัวคือหน้าที่เราลองเปิดดูมันทำงาน · บั๊กไปโผล่ที่หน้าอื่นที่เรียกแม่พิมพ์ตัวเดียวกัน
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">
…
- หน้าตาเหมือนคอมเมนต์ (
<%#) แต่รูปแบบlocals: (…)นี้ Rails อ่านจริง — คอมเมนต์ปกติยังถูกข้ามเหมือนเดิม · เฉพาะที่ขึ้นต้นว่าlocals:ตามด้วยวงเล็บเท่านั้นที่มีผล (post:)เขียนแบบเดียวกับ keyword argument ของ method (บท 93) ·post:ที่ไม่มีค่าตามหลัง = ต้องส่งมาเสมอ
หน้าเดิมยังทำงานปกติ (เพราะทุกที่ยื่น 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 นาที
- คืนทุกอย่างเป็นของเดิมก่อน:
git restore app/views app/controllersแล้วgit statusต้องขึ้นnothing to commit, working tree clean - ใน
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)
- เพิ่มบรรทัดแรก
- ใน
app/views/posts/show.html.erbเปลี่ยน<%= render @post %>เป็น<%= render @post, show_category: true %>· ไม่ต้องแตะ index - ตรวจด้วยสามคำสั่งนี้:
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 · โปรเจกต์ของคอร์ส) | ต้องสนใจไหม |
|---|---|---|---|
| 12 | layout มี render "shared/navbar" · "shared/footer" |
layout ของจริงยังไม่มี และยังไม่มีไฟล์ใน shared/ · พิมพ์ตามได้ Missing partial shared/_navbar |
ใช่ — อย่าเพิ่งเติม · navbar มาใน Lab บท 129 |
| 12 | stylesheet_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 |
| 13 | post_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 ได้ตามสไลด์ | ไม่ต้อง |
อ้างอิง
- Rails Guides — Layouts and Rendering · Using the content_for Method —
อ่านหัวข้อนี้ต่อจาก Understanding yield ข้างบนมัน · แล้วข้ามไป Specifying Layouts for Controllers ถ้าอยากรู้เรื่อง
layout "admin" - Rails Guides — Rendering Collections —
render @productsแบบเดียวกับrender @posts· ในหัวข้อนี้มีประโยค "render will return nil" กับตัวอย่างrender(@products) || "…"ที่เราลองใน predict - Rails Guides — Action View Overview · Strict Locals —
อ่านหัวข้อนี้ และ Counter Variables ข้างบนมัน (ที่มาของ
post_counter) · ส่วน "partials are compiled into regular Ruby methods" ข้ามได้ - Rails API — CaptureHelper#content_for —
อ่านย่อหน้า "Note that content_for concatenates…" (ที่มาของชื่อแท็บติดกันในหัวข้อ 2) และ option
flush: trueถ้าอยากให้เขียนทับแทน - Rails 7.1 Release Notes — ค้นคำว่า strict locals ในหน้า (ที่มาของแถวสุดท้ายในตารางสไลด์)