Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Ruby on Rails  Module 4 – Views, forms and assets

ERB views, layouts, partials and helpers

Action View in Rails 8.1: how Rails picks a template and a layout, what a partial accepts as locals, and which output tags escape and which do not.

  • Beginner
  • 10 minutes
  • Examples run with Ruby 4.0.7 and rails 8.1.4
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Render a page from a template, a layout and a partial, with locals and a strict-locals signature
  • Trace how Rails chooses the template, the format and the layout for an action that renders nothing
  • Recognise the helpers that skip escaping, and sanitise a field that is meant to hold HTML
  • Use the built-in helpers for links, buttons, numbers, dates and DOM ids

Before you start

What you’ll learn · Before you start

What you will learn

  • Render a page from a template, a layout and a partial, with locals and a strict-locals signature
  • Trace how Rails chooses the template, the format and the layout for an action that renders nothing
  • Recognise the helpers that skip escaping, and sanitise a field that is meant to hold HTML
  • Use the built-in helpers for links, buttons, numbers, dates and DOM ids

Before you start

On this page

The batch page of Kaksha, the coaching-centre app this track builds, is an action of two lines with no render call in it, and the HTML that leaves it comes out of four files: a controller, a template, a layout and one partial rendered three times, plus the helpers called inside them. Two kinds of trouble start in that handover. One is a page that looks nothing like the one you edited, because Rails picked a different file. The other is a guardian’s typed text turning into a pop-up, because one output tag was written the wrong way.

This lesson follows one recorded render of that page. You will see which files Rails chose and why, how the layout’s three holes were filled, what a partial refuses when a local is misspelt, and a check counting the script elements that reached the page with the safe tag and with the unsafe one.

Rails picks the template from the controller, the action and the format

Here is the whole controller behind the page. The index action sets one instance variable and ends:

app/controllers/batches_controller.rb: an action that renders nothing Ruby · batchpage/app/controllers/batches_controller.rb
# The action sets one instance variable and renders nothing: Rails then renders app/views/batches/index.html.erb
# wrapped in app/views/layouts/batches.html.erb.
class BatchesController < ApplicationController
  def index
    @batches = Batch.ordered
  end

  def show
    @batch = Batch.find(params[:id])
  end
end

Because it calls neither render nor redirect_to, Action View has to work out what to render from two names it already has: the controller, batches, and the action, index. It looks for app/views/batches/index, then narrows that by the format the request asked for, html, and the last extension names the library that compiles the file. index.html.erb matches both. If the file is not there, the search does not stop: Rails walks the controller’s inheritance chain and tries app/views/application/index.html.erb too, which is why shared partials belong in app/views/application. Only when nothing is found anywhere does it raise.

Step through the lookup, one stage per step, and watch the names the recorded run of the page later confirms:

Interactive · stage by stage What Action View looks for after the action ends Step through the lookup. Each stage names the file Rails tries and what it does when that file is not there.
What Action View looks for after the action ends, step 6 of 6. Showing Action ends, Name the template (views/batches/index), Pick the format (index.html.erb found), Walk up the chain, Find the layout (layouts/batches.html.erb), One HTML body. In focus: One HTML body.Action endsno render callName the templateviews/batches/indexPick the formatindex.html.erb foundWalk up the chainviews/applicationFind the layoutlayouts/batches.html.erbOne HTML bodytemplate in layout

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The action sets data. BatchesController#index fills @batches and ends. It calls neither render nor redirect_to, so Action View takes over with the controller name and the action name in hand.
  2. The name comes from the route. The controller is batches and the action is index, so the lookup starts at app/views/batches/index. No file in the app says so: both names come from the route the router matched.
  3. Format comes from the request. The request asked for HTML, so Rails wants a template whose format is html, and the last extension names the library that compiles it. app/views/batches/index.html.erb matches both.
  4. A miss walks up the chain. Had that file been missing, Rails would have looked in app/views/application, the view folder of the parent controller, and raised only after that. This is why shared partials live there.
  5. The layout follows the name. Rails looks for app/views/layouts/batches.html.erb first, because the controller is BatchesController, and falls back to layouts/application.html.erb. This app has the first one.
  6. The layout wraps the result. The template is rendered first, its result reaches the layout at the unnamed yield, and the blocks the template collected fill the named ones. One HTML body goes back to the controller.

Now the render itself. Action View is asked for the page outside a request, so nothing listens on a port, and the HTML is printed as it would be sent. Read the comments in the output first: in development a new app sets config.action_view.annotate_rendered_view_with_filenames to true, so every template, partial and layout marks where its own output begins and ends.

The batch page, rendered by Action View Ruby · sessions/batch_page_session.rb
# The batch page of Kaksha, rendered by the real Action View. ../batchpage holds the files this lesson adds to the
# generated app (the controller, the model, the template, the partial, the layout and the routes); ../render_batch_page.rb
# renders BatchesController#index and prints the HTML.
require_relative "session"

Session.run(<<~SH)
  cd ../kaksha
  cp -R ../batchpage/app ../batchpage/config .
  bin/rails runner ../render_batch_page.rb
SH

Output

$ cd ../kaksha
$ cp -R ../batchpage/app ../batchpage/config .
$ bin/rails runner ../render_batch_page.rb
<!-- BEGIN app/views/layouts/batches.html.erb
--><!DOCTYPE html>
<html lang="en">
  <head>
    <title>Batches | Kaksha</title>
      <meta name="description" content="Batches running at the Pune centre">

  </head>
  <body>
    <header>Kaksha, Pune centre</header>
    <main><!-- BEGIN app/views/batches/index.html.erb
-->
<h1>3 batches running</h1>

<!-- BEGIN app/views/batches/_batch.html.erb
--><article id="batch_1">
  <h2><a href="/batches/1">Class 10 maths, morning</a></h2>
  <p>Teacher Asha Rane, 28 seats</p>
    <p>Fee ₹12,000 a term</p>
</article>
<!-- END app/views/batches/_batch.html.erb --><!-- BEGIN app/views/batches/_batch.html.erb
--><article id="batch_2">
  <h2><a href="/batches/2">Class 12 physics, evening</a></h2>
  <p>Teacher Imran Shah, 22 seats</p>
    <p>Fee ₹18,000 a term</p>
</article>
<!-- END app/views/batches/_batch.html.erb --><!-- BEGIN app/views/batches/_batch.html.erb
--><article id="batch_3">
  <h2><a href="/batches/3">Spoken English, weekend</a></h2>
  <p>Teacher Nila Menon, 35 seats</p>
    <p>Fee ₹6,000 a term</p>
</article>
<!-- END app/views/batches/_batch.html.erb -->
<!-- END app/views/batches/index.html.erb --></main>
  </body>
</html>
<!-- END app/views/layouts/batches.html.erb -->
--- partial rendered 3 times, one per batch

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby batch_page_session.rb

Three files signed their work in those comments: the layout opened the body, batches/index.html.erb produced the heading, and batches/_batch.html.erb appears three times, once per row of the collection. The two numbers in the output are the lesson’s own: the heading says three batches, and the last line counts three renders of the partial.

Checkpoint

Check: How Rails finds the template

  1. One question BatchesController#index sets @batches and ends. Which file did the recorded run render first, and why that one?

    Choose one answer.

    Show the answer: One question

    Answer: app/views/batches/index.html.erb, named after the controller and the action

    With no render call, Action View builds the lookup from the two names it already has, the controller and the action, then narrows by format and handler. The annotation comments of the development run name every file it used, and the first content comes from app/views/batches/index.html.erb.

A layout wraps the template, and named blocks fill its holes

Rails renders the template first and hands the result to a layout, and the layout it picks follows the controller’s name: app/views/layouts/batches.html.erb for anything BatchesController renders, and app/views/layouts/application.html.erb when no such file exists. The one in this app is deliberately small:

app/views/layouts/batches.html.erb: three places to receive content ERB · batchpage/app/views/layouts/batches.html.erb
<%# A layout named after the controller: Rails prefers app/views/layouts/batches.html.erb over application.html.erb
    for every action of BatchesController. The unnamed yield receives the whole template; yield :head receives
    whatever the template collected under content_for :head. %>
<!DOCTYPE html>
<html lang="en">
  <head>
    <title><%= content_for(:title) || "Kaksha" %></title>
    <%= yield :head %>
  </head>
  <body>
    <header>Kaksha, Pune centre</header>
    <main><%= yield %></main>
  </body>
</html>

A layout: A template that wraps the one an action renders: the plain yield receives the whole template's output and each named yield receives a block the template collected with content_for. has one hole that is always filled and as many named ones as you like. The plain yield receives the whole template; yield :head receives whatever the template collected under that name with content_for. The generated application.html.erb of a new Rails 8.1 app uses both ideas already: it has yield :head inside its head element, and its title element reads content_for(:title) || "Kaksha", which is the page’s own title when a template set one and the app’s name when none did.

The template fills all three holes and renders nothing else of its own except a heading and the collection:

app/views/batches/index.html.erb: two blocks collected, one collection rendered ERB · batchpage/app/views/batches/index.html.erb
<%# app/views/batches/index.html.erb: the template Rails renders for BatchesController#index when the action
    renders nothing itself. content_for fills the layout's two holes; render @batches renders one partial per row. %>
<% content_for :title, "Batches | Kaksha" %>
<% content_for :head do %>
  <meta name="description" content="Batches running at the Pune centre">
<% end %>

<h1><%= pluralize(@batches.size, "batch") %> running</h1>

<%= render @batches %>

The order is what makes this work, and the diagram below is worth reading next to the recorded output above: the template runs before the layout, so by the time the layout asks for :title and :head, both are already collected.

A template collects a title and a head block and renders a body; the layout takes all three and returns one HTML body.app/views/batches/index.html.erbthe template for this actioncontent_for :titlea stringcontent_for :heada block of markupeverything the template rendersthe heading and three partialsapp/views/layouts/batches.html.erbtitle | yield :head | yieldone HTML bodysent to the browsercollectscollectsrendersfills the titlefills yield :headfills the plain yield

The three holes of the batches layout, and what fills each

Text description of the diagram

The diagram runs from top to bottom.

  1. At the top sits app/views/batches/index.html.erb, the template Rails renders for this action.
  2. Three arrows leave it. Two are labelled "collects": one leads to the title that content_for :title holds as a string, the other to the markup that content_for :head holds as a block. The third, labelled "renders", leads to everything the template itself produces: its heading and the three batch partials.
  3. All three lead into app/views/layouts/batches.html.erb, which has three places to receive them: the title element, yield :head and the plain yield. The arrows are labelled "fills the title", "fills yield :head" and "fills the plain yield".
  4. The layout sends out one HTML body, which is what reaches the browser.

The order matters: the template is rendered before the layout, which is why the layout can read blocks the template collected.

In the recorded page, Batches | Kaksha is in the title element and the description meta element sits in the head, neither of them written in the layout. Everything else the template produced landed at the plain yield.

Keep the page furniture in the layout, the page facts in the template

A layout that needs an if for every controller is a sign that the parts differ: give that controller its own layout file, or move the varying piece into a content_for block that each template fills.

Partials follow the same inheritance rule as templates, so a piece of furniture that two controllers share can live in app/views/application and be rendered from either template by name.

Key idea

The template is rendered first and the layout second, so a layout can only show what a template has already collected: the plain yield for the body, a named yield for everything else.

Checkpoint

Check: Layouts and named blocks

  1. Question 1 of 2 The app has both app/views/layouts/batches.html.erb and app/views/layouts/application.html.erb. Which one wraps a page rendered by BatchesController?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: layouts/batches.html.erb, because it carries the controller's name

    The layout lookup is the template lookup's sibling: the controller's own name first, then application. Adding app/views/layouts/batches.html.erb to an app is enough to change the layout of every BatchesController action, with no declaration anywhere.

  2. Question 2 of 2 The template sets content_for :title and a content_for :head block. In the recorded HTML, where did each end up? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • The title text is inside the title element of the layout's head
    • The description meta element is where the layout writes yield :head

    content_for puts content aside under a name; a named yield in the layout prints it. Because the template is rendered before the layout, the collection has already happened by the time the layout asks, and the recorded head holds a title and a meta element that the layout file itself does not contain.

Partials take locals, and a strict-locals line turns a typo into an error

A partial: A template whose file name starts with an underscore, rendered from another template and referred to without that underscore, so that one piece of markup can be reused with different data. is a template whose file name starts with an underscore, rendered from another template and referred to without that underscore: render "batch" renders _batch.html.erb. Partials exist to be called with different data each time, so what they receive matters more than what they contain. This one declares what it accepts on its first line:

app/views/batches/_batch.html.erb: a strict-locals signature, then the markup ERB · batchpage/app/views/batches/_batch.html.erb
<%# locals: (batch:, show_fee: true) -%>
<article id="<%= dom_id(batch) %>">
  <h2><%= link_to batch.name, batch_path(batch) %></h2>
  <p>Teacher <%= batch.teacher %>, <%= batch.seats %> seats</p>
  <% if show_fee %>
    <p>Fee <%= number_to_currency(batch.fee_paise / 100, unit: "₹", precision: 0) %> a term</p>
  <% end %>
</article>

That first line is an ERB comment that Action View reads as a signature. Without it a partial accepts any locals at all, and a misspelt name silently becomes nil, because partials are compiled into Ruby methods and one version is compiled for every combination of locals passed in. With it, exactly one version is compiled, batch is required, and show_fee has a default of true. Four renders of the same file show what that buys:

The same partial, with four different sets of locals Ruby · sessions/strict_locals_session.rb
# Four renders of the same partial through its strict-locals signature: both locals given, the default used, a
# required local missing, and a local whose name is misspelt.
require_relative "session"

Session.run(<<~SH)
  cd ../kaksha
  cp -R ../batchpage/app ../batchpage/config .
  bin/rails runner ../strict_locals_probe.rb
SH

Output

$ cd ../kaksha
$ cp -R ../batchpage/app ../batchpage/config .
$ bin/rails runner ../strict_locals_probe.rb
both locals given: rendered, fee paragraph left out
show_fee left out: rendered, fee paragraph included
batch left out   : ActionView::StrictLocalsError: missing local: :batch for app/views/batches/_batch.html.erb
show_fees misspelt: ActionView::StrictLocalsError: unknown local: :show_fees for app/views/batches/_batch.html.erb

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby strict_locals_session.rb

The first render passed show_fee: false and the fee paragraph is gone. The second passed only batch and the default put the fee back. The third and fourth raise ActionView::StrictLocalsError and name the file: once for missing local: :batch, once for unknown local: :show_fees, the misspelling that would otherwise have rendered a page with no fee on it and no error anywhere.

The template never passed locals by hand, though. render @batches is the collection form: Rails asks each record for its partial path, renders _batch once per record, and passes the record as a local named after the partial, which is exactly the batch the signature requires. The table gives that form and the three options worth knowing beside it:

Written as What Rails does
render @batches One _batch partial per record, the record in a local called batch
render partial: "batch", collection: @batches, as: :row The same, with the local renamed to row
batch_counter inside the partial The position of this render, starting at 0
render partial: "batch", collection: @batches, cached: true Reads and writes each row’s HTML in the cache store, where caching is switched on

An empty collection makes render return nil, so render(@batches) || "No batches yet." is the usual way to write the empty case.

Looping instead of rendering the collection

Writing @batches.each do |batch| around render "batch", batch: batch gives the same HTML, but it gives up the counter, the renaming, the empty-collection result and cached: true, and it reads the partial path once per row. Let render @batches do it.

Checkpoint

Check: Partials, locals and collections

  1. Question 1 of 2 The partial begins with the signature locals: (batch:, show_fee: true). The run rendered it with only show_fee: true. What happened?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raised ActionView::StrictLocalsError, missing local :batch

    A strict-locals signature is compiled into the partial's method signature, so Ruby's own keyword rules apply: a keyword without a default is required. The recorded run prints missing local: :batch for app/views/batches/_batch.html.erb.

  2. Question 2 of 2 The template renders the collection with render @batches and the partial requires a local called batch. Where does that local come from?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Rails passes each record in a local named after the partial

    render @batches asks each record for its partial path, so Batch records render batches/_batch, and the local is named after the partial. Rename it with as: row when the partial's own name reads badly, and read the position with batch_counter, which starts at 0.

The default output tag escapes, and raw is where cross-site scripting starts

Everything so far assumed the data is harmless. A coaching centre’s data is typed by people: a batch name arrives from a form, and someone will eventually type markup into it. The OWASP Cross Site Scripting Prevention Cheat Sheet asks for encoding at the place where untrusted data is written into the page, and in Action View that place is the output tag. <%= %> escapes what it prints; <% %> prints nothing at all and is for conditions and loops.

Three renders of one batch whose name is a script element show what each way produces. The check under each render is Nokogiri parsing the result and counting the script elements a browser would have run, which is the only count that matters:

One dangerous name, three ways of printing it Ruby · escaping_demo.rb
# Output escaping in Action View, with a check that proves it. A batch whose name is a <script> element is rendered
# three ways; Nokogiri then counts the script elements that reached the page, which is what a browser would run.
# The raw version is here only to be recognised and never written: it is the usual start of a stored XSS defect.
require "action_view"
require "nokogiri"

NAME = "<script>alert(1)</script>"
NOTICE = "<p>Fees due <b>Friday</b>.</p><script>alert(2)</script>"

view = ActionView::Base.with_empty_template_cache.new(ActionView::LookupContext.new([]), {}, nil)

def check(html)
  found = Nokogiri::HTML5.fragment(html).css("script").size
  puts "#{html}"
  puts "  check: #{found} script element(s) reached the page -> #{found.zero? ? "PASS" : "FAIL"}"
end

puts "1. <%= %>, the default:"
check(view.render(inline: "<h2><%= name %></h2>", locals: { name: NAME }))

puts "2. raw, never do this with anything a user typed:"
check(view.render(inline: "<h2><%= raw name %></h2>", locals: { name: NAME }))

puts "3. sanitize with an allowlist, for a field that is meant to hold HTML:"
check(view.render(inline: "<div><%= sanitize notice, tags: %w[p b i a], attributes: %w[href] %></div>",
                   locals: { notice: NOTICE }))

puts "4. the same three values as Ruby strings:"
puts "  html_safe? of the name          #{NAME.html_safe?}"
puts "  html_safe? after .html_safe     #{NAME.html_safe.html_safe?}"
puts "  html_safe? of sanitize's result #{view.sanitize(NOTICE).html_safe?}"

Output

1. <%= %>, the default:
<h2>&lt;script&gt;alert(1)&lt;/script&gt;</h2>
  check: 0 script element(s) reached the page -> PASS
2. raw, never do this with anything a user typed:
<h2><script>alert(1)</script></h2>
  check: 1 script element(s) reached the page -> FAIL
3. sanitize with an allowlist, for a field that is meant to hold HTML:
<div><p>Fees due <b>Friday</b>.</p>alert(2)</div>
  check: 0 script element(s) reached the page -> PASS
4. the same three values as Ruby strings:
  html_safe? of the name          false
  html_safe? after .html_safe     true
  html_safe? of sanitize's result true

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby escaping_demo.rb

With <%= %> the angle brackets came out as &lt; and &gt;, the page shows the name as text, and the check found no script element. With raw the element went straight through and the check found one: that is a stored cross-site-scripting defect, and the only reason it is in this lesson is so that you can recognise the shape in a code review. The third render is the case where HTML really is wanted: sanitize keeps an allowlist of tags and attributes, so the p and b elements survived, the script element did not, and its text was left behind as plain words.

Four ways of printing a value, the first safe and the other three not, are worth knowing by name:

Written as What it does
<%= %> Escapes, unless the value is already marked as HTML-safe. The default, and the right choice
raw value Prints the value unescaped; it is value.to_s.html_safe under another name
<%== value %> The same as raw, in tag form, and easy to mistake for <%= %>
value.html_safe Marks a string as safe, so every later <%= %> prints it unchanged

The last block of the recorded output is the state they share: Action View asks every string it prints whether it is HTML-safe: A flag on a string that tells Action View it may be printed unescaped; sanitize sets it on markup it has checked against an allowlist, and html_safe sets it on any string at all.. A plain batch name answers no, so it is escaped. The same name after .html_safe answers yes, so it is not. The result of sanitize answers yes too, and that answer is earned: the markup was checked against the allowlist before it was marked. The default sanitizer in Rails 8.1 is Rails::HTML5::SafeListSanitizer, and its documentation warns that adding tags or attributes to the allowlist may introduce vulnerabilities, so the default lists are the ones to keep.

Reaching for raw to make a layout look right

raw and html_safe on a value that came from a form, a request or a database column written by a user is how output escaping: Replacing the characters that would start markup, such as the angle brackets and the ampersand, with character references at the place where a value is printed into a page, so that typed text stays text. gets lost. If markup is genuinely needed, pass the field through sanitize and let the allowlist decide; if only the look is wrong, fix it in the markup around the value.

Checkpoint

Check: Escaping and the helpers that skip it

  1. Question 1 of 2 A batch is named with a script element and the template prints it with the plain output tag. How many script elements did the check find in the result?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: None: the angle brackets came out as character references

    Escaping at the output tag is encoding at the sink, which is what the OWASP cheat sheet asks for: the data stays as it was typed and the page shows it as text. The recorded check counts script elements with Nokogiri, so the claim is measured rather than asserted.

  2. Question 2 of 2 An announcement field is meant to carry bold text and links. Which choices keep that formatting without putting a script element on the page? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Print it with sanitize and an allowlist of tags and attributes
    • Store the field as typed and sanitise where it is printed

    A field that is meant to hold markup is the one case for sanitize, whose allowlist decides what survives. raw and html_safe skip the decision instead of making it, which is why they turn up in defect reports rather than in templates.

The helpers almost every page uses

A view helper: A method available inside templates, such as link_to or number_to_currency, that builds a piece of markup or formats a value, so that templates hold markup rather than logic. is a method available inside templates, and Action View ships with several hundred. The batch page used four of them: pluralize in the template, and dom_id, link_to and number_to_currency in the partial. This run prints each on the first batch, so the output and the page agree value for value:

The helpers of the batch page, on one record Ruby · sessions/helper_tour_session.rb
# The built-in view helpers on one batch of Kaksha. The render happens outside a request, so there is no session and
# the form that button_to builds carries no hidden authenticity_token field; in a real request Rails adds one.
require_relative "session"

Session.run(<<~SH)
  cd ../kaksha
  cp -R ../batchpage/app ../batchpage/config .
  bin/rails runner ../helper_tour.rb
SH

Output

$ cd ../kaksha
$ cp -R ../batchpage/app ../batchpage/config .
$ bin/rails runner ../helper_tour.rb
link_to          <a href="/batches/1">Class 10 maths, morning</a>
dom_id           batch_1 and new_batch
dom_class        batch
number_to_currency ₹12,000
number_with_delimiter 12,000
l(:long)         May 10, 2031
l(:short)        May 10
time_ago_in_words 3 days and 35 minutes
pluralize        1 batch / 3 batches
truncate         Class 10 maths,...

button_to builds a form that posts, not a link:
<!-- BEGIN inline template
--><form class="button_to" method="post" action="/batches/1/attendance"><button type="submit">Mark attendance</button></form>
<!-- END inline template -->

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby helper_tour_session.rb

link_to built the anchor, with the path from the route helper rather than a hand-written string, so a change to config/routes.rb cannot leave a dead link behind. number_to_currency and number_with_delimiter grouped the amount; l formatted the date twice, in the :long and :short formats the locale file defines; time_ago_in_words turned two moments into “3 days” and “35 minutes”; pluralize and truncate did the small jobs that otherwise fill a template with ifs.

Two of the lines deserve a second look. dom_id returned batch_1 for a saved record and new_batch for an unsaved one, and dom_class returned batch: these are the conventional ids and classes that form_with writes and that Turbo targets, so using the helper instead of typing "batch_#{batch.id}" keeps your markup and the framework’s in step. And button_to produced a form element with method="post", not an anchor. That difference is the point: a state change belongs in a non-GET request, and the Rails security guide’s countermeasure for cross-site request forgery is a security token in non-GET requests, added automatically because config.action_controller.default_protect_from_forgery is true in a new app. Rails puts that token in the forms it builds, so button_to is protected where a styled link is not. The recorded form has no token field only because the render happened outside a request, where there is no session to tie a token to.

India-specific

The amounts in the run are rupees, printed by number_to_currency with the unit given in the template. The two fees, 12,000 and 18,000, happen to be grouped the same way in the default en locale as they would be in the Indian convention; a seven-digit total would not be, coming out as 1,200,000 rather than 12,00,000. The internationalisation module of this track sets up that grouping and a locale of your own.

One thing about these templates will change in the next minor release, and it is worth building the habit now.

Version note

Rails 8.1 compiles ERB templates with Erubi. In Rails 8.2 config.action_view.erb_implementation also accepts :herb, and the 8.2 framework defaults choose it for templates whose format is HTML, leaving every other format on Erubi. Herb parses the HTML as well as the ERB, so markup that is well formed today, with every element closed in the right order, is markup that needs no fixing after that upgrade.

Checkpoint

Check: The built-in view helpers

  1. One question The run printed dom_id for a saved batch and for Batch.new. What were the two values?

    Choose one answer.

    Show the answer: One question

    Answer: batch_1 and new_batch

    dom_id and dom_class write the ids and classes that form_with already produces and that Turbo targets, so a template that calls the helper stays in step with the markup the framework generates.

Practise: predict what each file renders

These questions are taken from the four runs above: the rendered page, the four sets of locals, the three ways of printing a dangerous name, and the helper output. Answer one and its explanation opens at once:

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 The same action is asked for JSON and app/views/batches/index.json.jbuilder exists beside index.html.erb. Which file renders, and what decides?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: index.json.jbuilder: the format from the request picks it, and the last extension names the library that compiles it

    A template's name has three parts that the lookup reads: batches/index from the controller and action, the format, and the handler. The Action View overview lists erb, jbuilder and builder as handlers chosen by that last extension.

  2. Question 2 of 11 Four controllers must share one empty-state partial. Where do you put the file so that every one of them can render it as "empty state" with no path?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: app/views/application/_empty_state.html.erb

    The lookup order for a controller is its own view folder, then the folders of its ancestors, ending at app/views/application. That makes app/views/application the conventional home for partials several controllers render, with no path in any call.

  3. Question 3 of 11 A template renders without ever calling content_for :title. The layout element reads <title><%= content_for(:title) || "Kaksha" %></title>. What does the title element show on that page?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: Kaksha

    content_for :name without a block returns what the template collected under :name, which is empty when it was never called; the layout's || fallback is what makes a missing block safe. The recorded page sets :title in the template and the title element reads Batches | Kaksha; a page that said nothing would read Kaksha.

  4. Question 4 of 11 Every page should show a sidebar, but three pages need a different one. Which arrangement keeps conditions out of the layout?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: A yield :sidebar in the layout with a default, and content_for :sidebar in the three templates that differ

    A named yield with a sensible default is the layout's way of asking a question that each template may answer. The layout stays one file with no knowledge of which controller is rendering, and a page that says nothing gets the default.

  5. Question 5 of 11 A template renders the batch partial with show_fees: true instead of show_fee: true. What does a reader of the page see, with the strict-locals signature and without it?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: With the signature, an error naming the unknown local; without it, a page whose fee is quietly missing

    This is the whole argument for the signature. The failure mode without it is silence, and a missing fee on a page is the kind of defect that reaches a guardian before it reaches a developer.

  6. Question 6 of 11 A template calls render "batch". Which file does Action View look for?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: _batch.html.erb, in the current controller's view folder and then up the chain

    The leading underscore is the only naming difference between a partial and a template, and render never writes it. A partial from another folder is rendered with its folder, as in render "application/menu".

  7. Question 7 of 11 In the recorded run, sanitize was given an announcement holding a paragraph, a bold element and a script element. What did the output contain?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: The paragraph and the bold element, and the script element's text as plain words

    sanitize scrubs rather than refuses: allowed elements stay, others are removed, and text inside a removed element is kept. Rails 8.1 uses Rails::HTML5::SafeListSanitizer by default, whose documentation advises against widening the default lists.

  8. Question 8 of 11 A model method returns a string built with html_safe so that a template can print a badge. A year later a column it reads starts holding text typed by users. What has gone wrong, and where?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: The marking is far from the template, so the new data is printed unescaped with nothing in the view to show it

    Marking a string safe is a promise about every future print of it, made in a place where the data's origin may change later. Build such strings from escaped pieces, with helpers such as safe_join or tag, or sanitise at the output tag where the context is known.

  9. Question 9 of 11 Marking attendance changes data. Why does the lesson use button_to for it rather than a styled link_to?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: button_to builds a form that posts, and Rails puts its forgery token in the forms it builds

    A state change belongs in a non-GET request, and the token Rails adds to its forms is what makes that request hard to forge from another site. In the recorded run the form has no token field only because the render happened outside a request, where there is no session to tie one to.

  10. Question 10 of 11 A batch stores its term fee as 1800000 paise. The template prints it with number_to_currency(fee_paise / 100, precision: 0). How many whole currency units does the amount shown come to?

    Type a number.

    Show the answer: Question 10 of 11

    Answer: 18000 units

    100 paise make one unit, so 1,800,000 paise is 18,000 units, and precision 0 prints no decimal part. The recorded run shows the same arithmetic for the first batch, whose 1,200,000 paise print as 12,000 with the grouping the locale gives.

  11. Question 11 of 11 The recorded helper tour called pluralize(1, "batch") and pluralize(3, "batch"). What did it print for the two calls, separated by a slash?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: 1 batch / 3 batches

    pluralize is one of the "small jobs that otherwise fill a template with ifs" the lesson calls out: it writes the count and then the singular or the plural, so a template never has to say "if n == 1". The ActiveSupport Inflector decides the plural; batches is its default for batch.

Interview questions

Warm-up: which Action View tag escapes its output, and which three ways of printing a value do not?

<%= %> escapes, unless the value it prints is already marked HTML-safe, so &, ", < and > reach the page as character references and a typed script element is shown as text. Three ways skip that. raw value prints the value unchanged and is only value.to_s.html_safe written differently. <%== value %> is the same thing in tag form, which makes it easy to read as <%= %> in a code review. And value.html_safe marks the string itself, so every later <%= %> prints it as it is, however far from the template that marking happened. The safe way to print a field that is meant to carry markup is sanitize, which checks the markup against an allowlist of tags and attributes and marks only the result as safe.

The module’s question bank takes two longer cases from this page, one on a field that must keep its formatting and one on where shared page furniture belongs. Write down your own answer to both before you read what each question sets out to test:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A page shows teachers' announcements, which may carry bold text and links, so the template prints the field with the raw helper. A guardian reports a pop-up on that page. How do you make the page safe without losing the formatting, and how do you show that no other page has the same hole?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Three teams keep adding to one layout: a notice bar, a term switcher and a print footer, each wanted on some pages only. Decide where each piece belongs among a controller's own layout, a named block the template fills, and a shared partial, and defend the split.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. An action renders nothing. Which template does Rails look for, and where does it search before it raises?

    The template named after the controller and the action, for the request's format. If that is missing, Rails walks up the controller's parent classes, through app/views/application, before raising an error.

  2. Why can a layout show a page title that its template set with content_for?

    The template renders first and the layout second. The plain yield takes the body and a named yield takes what content_for collected. Rails picks the layout by the controller's name, then falls back to layouts/application.html.erb.

  3. What does a strict-locals line at the top of a partial change?

    A local it lists without a default becomes required, one with a default may be left out, and a name it does not list, such as a misspelling, raises ActionView::StrictLocalsError instead of rendering a page with a value silently missing.

  4. What does render @batches do?

    It renders the _batch partial once for each record, with the record in a local named batch and its position, from 0, in batch_counter. An empty collection makes render return nil, so a fallback text can follow it after ||.

  5. The <%= %> tag escapes what it prints. Which three ways of printing skip that?

    raw, the <%== %> tag and a string marked with html_safe. In the recorded run only the raw version let a script element into the page, so none of the three may carry text a person typed.

  6. Which helper should a state change such as a delete use, and which helper builds the ids Turbo expects?

    button_to, which writes a small form that sends a non-GET request; link_to with a route helper stays for plain navigation. dom_id builds the element ids that forms and Turbo already look for.

Key takeaways

  • An action that renders nothing gets the template named after its controller and action, narrowed by the request’s format, and Rails walks the controller’s inheritance chain, through app/views/application, before it raises.
  • The template is rendered before the layout, which Rails picks by controller name and then by layouts/application.html.erb; the plain yield takes the body and a named yield takes whatever content_for collected.
  • A partial’s strict-locals signature makes required locals required and misspelt ones an error, which is why the recorded run raised ActionView::StrictLocalsError instead of rendering a page with a missing fee.
  • render @batches renders one partial per record with the record in a local named after the partial, and brings the counter, renaming, the empty-collection result and cached: true with it.
  • <%= %> escapes by default, and raw, <%== %> and html_safe are the three ways that defect reports begin: the check on the recorded run found a script element in the page only in the raw version.
  • The built-in helpers keep markup in step with the framework: route helpers inside link_to, button_to for a state change in a non-GET form, and dom_id for the ids that forms and Turbo already expect.

Terms in this lesson

HTML-safe
A flag on a string that tells Action View it may be printed unescaped; sanitize sets it on markup it has checked against an allowlist, and html_safe sets it on any string at all.
layout
A template that wraps the one an action renders: the plain yield receives the whole template's output and each named yield receives a block the template collected with content_for.
output escaping
Replacing the characters that would start markup, such as the angle brackets and the ampersand, with character references at the place where a value is printed into a page, so that typed text stays text.
partial
A template whose file name starts with an underscore, rendered from another template and referred to without that underscore, so that one piece of markup can be reused with different data.
view helper
A method available inside templates, such as link_to or number_to_currency, that builds a piece of markup or formats a value, so that templates hold markup rather than logic.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.