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

View components, presenters and Phlex

When a Rails 8.1 partial outgrows its file: presenters, ViewComponent with render tests and previews, Phlex classes, and the escaping each keeps.

  • Intermediate
  • 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

  • Identify when a partial and its helpers have outgrown their file
  • Build a presenter and a ViewComponent with render tests and previews
  • Compare plain partials, ViewComponent and Phlex for a team's shared UI
  • Recognise the explicit escape hatch of each tool and keep user text escaped

Before you start

What you’ll learn · Before you start

What you will learn

  • Identify when a partial and its helpers have outgrown their file
  • Build a presenter and a ViewComponent with render tests and previews
  • Compare plain partials, ViewComponent and Phlex for a team's shared UI
  • Recognise the explicit escape hatch of each tool and keep user text escaped

Before you start

On this page

Kaksha shows a fee badge on 14 screens: paid, pending, overdue, each with a colour and sometimes a student’s name. As a partial, one misspelt local makes the name vanish without an error, and the only way to see every state is to click through the app. The runs on this page take that one badge through four shapes Rails apps use: a partial, a presenter, a ViewComponent with three tests and four previews, and a Phlex class, and then put a name with a script element through each of them.

By the end you can tell when a partial has outgrown its file, move formatting into a presenter, build and test a component, compare it with Phlex, and spot the one line in each tool that would print markup unescaped.

A partial’s locals are a contract nobody checks

Here is the badge as a partial. Its locals are documented in a comment, and the optional name is read with defined?, the usual way to make a local optional:

app/views/shared/_fee_status_badge.html.erb ERB · kaksha_vc/app/views/shared/_fee_status_badge.html.erb
<%# app/views/shared/_fee_status_badge.html.erb
   A partial that shows a coloured badge for a student's fee status.
   Locals: status ("paid", "pending", "overdue"), student_name (optional) %>
<% colors = { "paid" => "green", "pending" => "orange", "overdue" => "red" } %>
<% labels = { "paid" => "Paid", "pending" => "Pending", "overdue" => "Overdue" } %>
<span class="badge badge-<%= colors.fetch(status, "gray") %>">
  <%= labels.fetch(status, status.capitalize) %>
  <% if defined?(student_name) && student_name %>
    for <%= student_name %>
  <% end %>
</span>

Nothing checks that comment. A caller that writes studentname: instead of student_name: gets a badge without the name and no error: the run in the ViewComponent section below shows exactly that. The colours and labels are rebuilt on every render inside the template, and testing the badge means rendering a page around it. None of this is wrong for a small partial; it becomes a cost when the partial is shared and its states multiply.

Sign What it costs
Optional locals read with defined? A misspelt local is silently ignored
Logic and data in the template Every state is checked by rendering a whole page
The same partial copied between screens Copies drift apart, one fix at a time
Many states (statuses, roles, empty, error) Nobody sees them all without a gallery

The first sign has a fix inside ERB itself: the strict-locals comment from the ERB lesson, <%# locals: (status:, student_name: nil) -%>, turns a misspelt local into an error. The other three are what presenters and components address.

Checkpoint

Check: When a partial outgrows its file

  1. Question 1 of 2 A caller renders the badge partial with studentname: "Asha" instead of student_name:. What does the page show?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An Overdue badge with no name, and no error

    A partial without strict locals accepts any locals and reads the ones it knows. The component, given the same typo, raised ArgumentError: unknown keyword: :studentname.

  2. Question 2 of 2 Which are signs that a partial has outgrown its file? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Many states that nobody can see without clicking through the app
    • Optional locals read with defined?
    • The same partial copied between screens

    None of these is wrong in a small partial; together, in a shared piece with many states, they are the cost a component removes.

A presenter moves formatting into plain Ruby

A presenter: A plain Ruby object that wraps one record and answers the questions a view asks about it, such as a date in local time or a fee in rupees, returning strings so the template only prints them. is a plain Ruby object that wraps a record and answers the questions a view asks: the start date in the reader’s time zone, the fee formatted for its market. The template then only prints what the presenter returns. This one uses Active Support’s own number helper with the locale grouping pattern the Rails API documents:

A BatchPresenter for dates in IST and fees in rupees Ruby · presenter_demo.rb
# A presenter: a plain Ruby object that formats a record for display, so the template only prints.
# It needs no Rails app, no request and no database, which is why it can be checked this quickly.
require "active_support"
require "active_support/number_helper"
require "active_support/core_ext/time"

Batch = Data.define(:name, :starts_at, :fee_paise)

class BatchPresenter
  # Indian grouping (12,50,000): the pattern the Rails API gives for number_to_delimited.
  LAKH_GROUPS = /(\d+?)(?=(\d\d)+(\d)(?!\d))/

  def initialize(batch)
    @batch = batch
  end

  def name = @batch.name

  def starts_on
    @batch.starts_at.in_time_zone("Asia/Kolkata").strftime("%-d %B %Y, %-I:%M %p IST")
  end

  def fee
    rupees = ActiveSupport::NumberHelper.number_to_delimited(@batch.fee_paise / 100, delimiter_pattern: LAKH_GROUPS)
    "₹#{rupees}"
  end
end

batches = [
  Batch.new("Data Science A1", Time.utc(2031, 5, 15, 3, 30), 125_000_00),
  Batch.new("Full-stack web, weekends", Time.utc(2031, 6, 1, 4, 0), 1_250_000_00),
  Batch.new("Spoken English trial", Time.utc(2031, 6, 7, 12, 30), 950_00),
]

batches.map { BatchPresenter.new(_1) }.each do |p|
  puts "#{p.name.ljust(25)} #{p.starts_on.ljust(27)} #{p.fee}"
end

Output

Data Science A1           15 May 2031, 9:00 AM IST    ₹1,25,000
Full-stack web, weekends  1 June 2031, 9:30 AM IST    ₹12,50,000
Spoken English trial      7 June 2031, 6:00 PM IST    ₹950

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

Three batches, three formats checked at once, with no Rails app, no request and no database: a UTC time became a local start time and each fee got its digits grouped for its market. A presenter returns strings, never markup, so it says nothing about which tag or colour the badge gets; it fits when formatting is the pain and the template is still small.

Rupee fees with lakh grouping

In the en-IN locale the example turns 03:30 UTC into 9:00 AM IST, groups a fee of 12,50,000 rupees in the Indian places — the last three digits, then pairs — and leaves ₹950 with none. The same presenter serves every institute whatever its market.

The split between what the value says and what the markup is runs through every tool in this lesson, so it is worth stating once as a rule.

Key idea

A presenter decides what a value says; a component decides what the markup is. Keep formatting rules in plain Ruby that runs without a page, and keep markup where a template or a component owns it.

Checkpoint

Check: Presenters for display formatting

  1. One question What does the lesson's BatchPresenter return for a batch's fee?

    Choose one answer.

    Show the answer: One question

    Answer: A string such as ₹12,50,000, with no markup

    It divides the paise by 100 and groups the rupees in the Indian way with number_to_delimited and the lakh pattern; the template only prints the result.

A ViewComponent is a class, a template and its arguments

ViewComponent: A gem that turns a piece of view into a Ruby class with its template beside it: initialize declares the arguments, and the component can be rendered, tested with render_inline and previewed on its own. turns the partial into a Ruby class that inherits from ViewComponent::Base, with its template in a file beside it. The class’s initialize takes the arguments, and the methods it defines are what the template calls:

app/components/fee_status_badge_component.rb Ruby · kaksha_vc/app/components/fee_status_badge_component.rb
# app/components/fee_status_badge_component.rb
# A ViewComponent that shows a coloured badge for a student's fee status.
class FeeStatusBadgeComponent < ViewComponent::Base
  COLORS = { "paid" => "green", "pending" => "orange", "overdue" => "red" }.freeze
  LABELS = { "paid" => "Paid", "pending" => "Pending", "overdue" => "Overdue" }.freeze

  def initialize(status:, student_name: nil)
    @status = status
    @student_name = student_name
  end

  def color
    COLORS.fetch(@status, "gray")
  end

  def label
    LABELS.fetch(@status, @status.capitalize)
  end
end
app/components/fee_status_badge_component.html.erb ERB · kaksha_vc/app/components/fee_status_badge_component.html.erb
<span class="badge badge-<%= color %>">
  <%= label %>
  <% if @student_name %>
    for <%= @student_name %>
  <% end %>
</span>

Step through what one render of it does, from the call to the HTML it hands back:

Interactive · message by message What render does with a ViewComponent Step through one badge, from the call in a page to the escaped HTML it returns, and the typo that never gets that far.
What render does with a ViewComponent, step 6 of 6. Messages so far: The page to FeeStatusBadgeComponent: new(status:, student_name:); The page to FeeStatusBadgeComponent: render_in(view_context); FeeStatusBadgeComponent to Its template: compile once; FeeStatusBadgeComponent to Its template: color and label; Its template to The page: <span class="badge-red">.The pageany view or controllerFeeStatusBadgeComponentrender? is trueIts templatecompiled into a methodnew(status:, student_name:)render_in(view_context)compile oncecolor and label<span class="badge-red">

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

Every step as text
  1. new checks the arguments. The page builds FeeStatusBadgeComponent.new(status: "overdue", student_name: "Asha"). initialize takes keyword arguments, so a misspelt name such as studentname raises ArgumentError here, before anything renders.
  2. render hands over the view. render calls the component's render_in with the page's view context. Helpers stay available through the component's helpers method, but its own data is only what initialize stored.
  3. Compiled once per class. The first render of the class compiles the template beside it into a method, and later renders reuse it. A missing or broken template fails at this point, for every badge at once.
  4. before_render, then render?. before_render runs first; then render? decides whether there is anything to show. Both are ordinary methods a component can override, so a badge for no status can render nothing at all.
  5. The template runs as the object. Inside the template, self is the component: color and label are its own methods, and @student_name is what new stored. Output tags escape exactly as in any ERB template.
  6. Escaped HTML comes back. The page receives one span with class badge-red and the text Overdue for Asha. In development Rails wraps it in BEGIN and END comments that name the component's template file.

This run renders the badge as the partial, the component and the Phlex class of a later section, then passes the same typo to the partial and to the component, and finally asks for the component’s previews:

One badge, three ways, one typo, and the previews Ruby · sessions/badge_session.rb
require_relative "session"

# The Kaksha app beside this folder: a default `rails new` (Rails 8.1.4) plus view_component 4.16.0 and phlex-rails 2.4.0.
Dir.chdir File.expand_path("../kaksha_vc", __dir__)

Session.run(<<~'SH')
  # The same badge as a partial, a ViewComponent and a Phlex class, then one typo:
  bin/rails runner ../render_badges.rb
SH

Output

# The same badge as a partial, a ViewComponent and a Phlex class, then one typo:
$ bin/rails runner ../render_badges.rb
== The partial: render "shared/fee_status_badge", status: "overdue", student_name: "Asha"
<!-- BEGIN app/views/shared/_fee_status_badge.html.erb
--><span class="badge badge-red">
  Overdue
    for Asha
</span>
<!-- END app/views/shared/_fee_status_badge.html.erb -->

== The ViewComponent: render FeeStatusBadgeComponent.new(status: "overdue", student_name: "Asha")
<!-- BEGIN app/components/fee_status_badge_component.html.erb
--><span class="badge badge-red">
  Overdue
    for Asha
</span>
<!-- END app/components/fee_status_badge_component.html.erb -->

== The Phlex class: render FeeStatusBadge.new(status: "overdue", student_name: "Asha")
<span class="badge badge-red">Overdue for Asha</span>

== A typo, studentname instead of student_name, passed to the partial
<!-- BEGIN app/views/shared/_fee_status_badge.html.erb
--><span class="badge badge-red">
  Overdue
</span>
<!-- END app/views/shared/_fee_status_badge.html.erb -->

== The same typo passed to the component
ArgumentError: unknown keyword: :studentname

== GET /rails/view_components (the list of previews)
200: /rails/view_components/fee_status_badge_component/overdue, /rails/view_components/fee_status_badge_component/paid, /rails/view_components/fee_status_badge_component/pending, /rails/view_components/fee_status_badge_component/unknown_status

== GET /rails/view_components/fee_status_badge_component/unknown_status
200: <span class="badge badge-gray"> Waived </span>

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

The partial and the component produce the same span, each wrapped in development in BEGIN and END comments that name its file. The typo is where they part: the partial renders “Overdue” with no name, while the component raises ArgumentError: unknown keyword: :studentname before anything is drawn.

Checkpoint

Check: A ViewComponent class and its template

  1. Question 1 of 2 Why did FeeStatusBadgeComponent.new(status: "overdue", studentname: "Asha") raise ArgumentError?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: initialize takes keyword arguments, and studentname is not one of them

    The arguments of a component are the signature of its initialize, so a typo fails loudly at the call, which the partial without strict locals never does.

  2. Question 2 of 2 Inside fee_status_badge_component.html.erb, what are color and label?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Methods of the component, because the template runs with the component as self

    The template is compiled once into a method of the class, so it sees the component's methods and the instance variables initialize set.

Tests and previews need no page around the component

Because a component is an object with its own arguments, a test can render it alone. ViewComponent::TestCase provides render_inline, which returns the HTML as a Nokogiri fragment, and, when Capybara is in the bundle, lets the test use assert_selector and assert_text on it. One test per state:

test/components/fee_status_badge_component_test.rb Ruby · kaksha_vc/test/components/fee_status_badge_component_test.rb
require "test_helper"

# test/components/fee_status_badge_component_test.rb: one test per state of the badge. render_inline renders the
# component with no request and no browser; assert_selector and assert_text read the HTML it produced.
class FeeStatusBadgeComponentTest < ViewComponent::TestCase
  def test_overdue_is_red_and_names_the_student
    render_inline(FeeStatusBadgeComponent.new(status: "overdue", student_name: "Ravi"))

    assert_selector "span.badge.badge-red", text: "Overdue"
    assert_text "for Ravi"
  end

  def test_an_unknown_status_is_gray
    render_inline(FeeStatusBadgeComponent.new(status: "waived"))

    assert_selector "span.badge.badge-gray", text: "Waived"
  end

  def test_a_name_with_markup_stays_text
    render_inline(FeeStatusBadgeComponent.new(status: "paid", student_name: "<b>Asha</b>"))

    assert_no_selector "b"
    assert_text "for <b>Asha</b>"
  end
end
The component tests, run with bin/rails test Ruby · sessions/component_test_session.rb
require_relative "session"

# The Kaksha app beside this folder: a default `rails new` (Rails 8.1.4) plus view_component 4.16.0 and phlex-rails 2.4.0.
Dir.chdir File.expand_path("../kaksha_vc", __dir__)

Session.run(<<~'SH')
  # The component test, run like every other test of the app:
  bin/rails test test/components/fee_status_badge_component_test.rb
SH

Output

# The component test, run like every other test of the app:
$ bin/rails test test/components/fee_status_badge_component_test.rb
Running 3 tests in a single process (parallelization threshold is 50)
Run options: --seed …

# Running:

...

Finished in …s, … runs/s, … assertions/s.
3 runs, 5 assertions, 0 failures, 0 errors, 0 skips

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

Three runs and five assertions passed with no controller, route or browser involved, including the check that a name with a <b> element stays text. Previews show the same states to people. A class in test/components/previews with one method per state gives each its own page in development:

test/components/previews/fee_status_badge_component_preview.rb Ruby · kaksha_vc/test/components/previews/fee_status_badge_component_preview.rb
# test/components/previews/fee_status_badge_component_preview.rb: every state of the badge, one page each, at
# /rails/view_components/fee_status_badge_component/<method> while the app runs in development.
class FeeStatusBadgeComponentPreview < ViewComponent::Preview
  def paid = render(FeeStatusBadgeComponent.new(status: "paid", student_name: "Asha"))
  def pending = render(FeeStatusBadgeComponent.new(status: "pending", student_name: "Asha"))
  def overdue = render(FeeStatusBadgeComponent.new(status: "overdue", student_name: "Asha"))
  def unknown_status = render(FeeStatusBadgeComponent.new(status: "waived"))
end

The end of the badge run above asked for them: /rails/view_components listed four pages, one per method, and the unknown_status page answered 200 with a gray “Waived” badge. Lookbook, a separate gem, builds a browsable gallery from the same preview classes and also supports Phlex.

Checkpoint

Check: Component tests and previews

  1. Question 1 of 2 What does render_inline return in a ViewComponent test?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The rendered HTML as a Nokogiri fragment, with Capybara assertions available when Capybara is bundled

    ViewComponent::TestCase renders the component alone, which is why the badge's three states were tested without a page around it.

  2. Question 2 of 2 The preview class has four methods: paid, pending, overdue and unknown_status. What does /rails/view_components list in development?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Four pages, one per method

    A class under test/components/previews that inherits ViewComponent::Preview gets one page per public method.

Phlex writes the same badge in Ruby

Phlex: A Ruby gem for views written as Ruby classes: a view_template method calls one method per HTML element and escapes text written with plain; phlex-rails lets Rails render such a class. drops the template file: the whole view is a Ruby class whose view_template method calls one method per HTML element, span(class: …) { … }, and plain for text:

app/components/fee_status_badge.rb Ruby · kaksha_vc/app/components/fee_status_badge.rb
# app/components/fee_status_badge.rb
# A Phlex component that shows a coloured badge for a student's fee status.
class FeeStatusBadge < Phlex::HTML
  COLORS = { "paid" => "green", "pending" => "orange", "overdue" => "red" }.freeze
  LABELS = { "paid" => "Paid", "pending" => "Pending", "overdue" => "Overdue" }.freeze

  def initialize(status:, student_name: nil)
    @status = status
    @student_name = student_name
  end

  def view_template
    span(class: "badge badge-#{color}") do
      plain label
      if @student_name
        plain " for "
        plain @student_name
      end
    end
  end

  private

  def color = COLORS.fetch(@status, "gray")
  def label = LABELS.fetch(@status, @status.capitalize)
end

In the badge run, Phlex printed <span class="badge badge-red">Overdue for Asha</span> on one line and without the annotation comments, because it builds the HTML itself instead of going through an ERB template. The phlex-rails gem lets render take a Phlex object anywhere a partial or a component would go. The three shapes compare like this:

Partial ViewComponent Phlex
Files one .html.erb a class and a template a class
Arguments checked with a strict-locals comment by initialize by initialize
Tested alone no, through a page render_inline calling the object
Previews none ViewComponent::Preview, Lookbook Lookbook
Markup written in ERB ERB Ruby methods
Extra gem none view_component phlex-rails

Choosing for a team

Keep partials for markup used in one or two places. Move a piece into ViewComponent when it is shared, has several states, and deserves its own tests and previews; choose Phlex when the team would rather read views as Ruby than as HTML. Convert one component at a time, starting with the partial that has the most states.

Checkpoint

Check: Phlex, and choosing between the three

  1. Question 1 of 2 Why did the Phlex badge come out without the BEGIN and END comments the other two had?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Phlex builds the HTML itself in Ruby, instead of going through an ERB template that Rails annotates

    The annotations name template files, and only the partial and the component have one.

  2. Question 2 of 2 A badge used on one screen, with two states and no tests of its own: which shape fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A partial, perhaps with a strict-locals comment

    Keep partials for markup used in one or two places, and move a piece into a component when it is shared and its states multiply.

Escaping still applies, and each tool has one way around it

A component is still a template, so the rules of the ERB lesson hold: output is escaped unless something marks it safe. This run gives the badge a student name that carries a script element and counts the script elements that reached the HTML, first through each tool’s normal output and then through each tool’s explicit opt-out, shown so you can recognise it in code you own:

One name with a script element, through each tool and its opt-out Ruby · sessions/escaping_session.rb
require_relative "session"

# The Kaksha app beside this folder: a default `rails new` (Rails 8.1.4) plus view_component 4.16.0 and phlex-rails 2.4.0.
Dir.chdir File.expand_path("../kaksha_vc", __dir__)

Session.run(<<~'SH')
  # One name with a script element, through each tool and each tool's opt-out:
  bin/rails runner ../escaping_badges.rb
SH

Output

# One name with a script element, through each tool and each tool's opt-out:
$ bin/rails runner ../escaping_badges.rb
The default output of each tool:
  PASS  partial, <%= student_name %>: 0 script elements
  PASS  ViewComponent, <%= @student_name %>: 0 script elements
  PASS  Phlex, plain @student_name: 0 script elements

The explicit opt-outs (never for user text):
  FAIL  partial, <%= raw student_name %>: 1 script element
  FAIL  ViewComponent, given NAME.html_safe: 1 script element
  FAIL  Phlex, raw(safe(@html)): 1 script element
  ----  Phlex, raw(@html) with a plain String: Phlex::ArgumentError: You passed an unsafe object to `raw`.

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

All three normal paths printed 0 script elements: <%= %> in the partial and in the component template, and plain in Phlex, all escape. Each opt-out printed 1: raw in a partial, a string marked html_safe before it reaches a component, and raw(safe(…)) in Phlex. Phlex is the strictest of the three: raw with a plain string raised Phlex::ArgumentError, so markup can only get through when someone wrote safe on purpose.

Search for the opt-outs, not for the tags

In a code base you own, search the views, components and helpers for raw, html_safe, <%== and safe(. Each hit must print markup your app built, never text a person typed; for user text with formatting, use sanitize with an allowlist, as the ERB lesson shows.

Checkpoint

Check: Escaping in partials, components and Phlex

  1. Question 1 of 2 A student name containing a script element goes through the partial, the component and the Phlex class, each with its normal output. How many script elements reach the HTML?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: None, in all three

    Moving markup into a component or a Phlex class changes nothing about escaping: user text stays text.

  2. Question 2 of 2 Which of these print markup unescaped? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • raw(safe(@html)) in a Phlex class
    • <%= raw student_name %> in a partial
    • A string marked html_safe before it reaches a component

    Each tool has one explicit way around escaping. In code you own, search for raw, html_safe, <%== and safe( and make sure none of them carries text a person typed.

Practise

Every question below comes from the runs on this page: the presenter’s three batches, the typo that one tool ignored and the other refused, the three tests and four previews, Phlex’s one-line span, and the script elements counted in each tool. Answer one and its explanation opens at once.

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 7 You must keep the badge as a partial but want the studentname typo to fail loudly. What do you add?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: A strict-locals comment: <%# locals: (status:, student_name: nil) -%>

    Strict locals give a partial a signature, the one advantage of a component's initialize that ERB can have too.

  2. Question 2 of 7 The presenter formats a fee of 95,000 paise. What does it return?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: ₹950

    The presenter divides by 100 first, then groups: three digits need no comma at all.

  3. Question 3 of 7 A teammate wants BatchPresenter#fee to return <strong>₹12,50,000</strong> so overdue fees stand out. What do you suggest?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: Keep fee a plain string, and let the template or component decide the tag

    Presenters answer what a value says; components and templates own the markup, which keeps escaping in one place.

  4. Question 4 of 7 How many assertions did the three component tests make?

    Type a number.

    Show the answer: Question 4 of 7

    Answer: 5 assertions

    Two in the overdue test (the selector and the name), one in the unknown-status test, two in the markup test (no b element, the text shown): 3 runs, 5 assertions, 0 failures.

  5. Question 5 of 7 A designer wants to see every badge state; CI must catch a broken state. Which do you write?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: Both: a preview method per state for people, a render_inline test per state for CI

    The same component serves both: render_inline for assertions, ViewComponent::Preview (and Lookbook) for eyes.

  6. Question 6 of 7 In a Phlex view_template, which call writes text into the current element?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: plain

    Element methods such as span open tags, and plain fills them with escaped text.

  7. Question 7 of 7 A component prints a teacher's note that may contain bold text and links. Which change keeps the formatting and stays safe?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: Pass the note through sanitize with an allowlist of tags before printing it

    Formatting from a person needs an allowlist, never an opt-out: sanitize is the tool, as in the ERB lesson.

Interview questions

Warm-up: when would you use a presenter instead of a helper?

When the formatting rule belongs to one kind of record and deserves its own tests. A helper is a module mixed into every view, so its methods take the record as an argument and grow into a long shared list. A presenter wraps one record and answers its display questions, such as a fee formatted for its market or the start time in the reader’s zone, as plain Ruby that runs without a request; the lesson’s presenter formatted three batches with no app running. A helper still suits a small, general piece of markup used everywhere, like a link with an icon. A presenter returns strings, never markup, so the tags stay in the template or component.

Warm-up: what does ViewComponent’s render_inline return, and what can a test do with it?

It renders the component on its own, with no request, controller or browser, and returns the HTML as a Nokogiri fragment. When Capybara is in the bundle, as it is in a new Rails app, ViewComponent::TestCase also lets the test call assert_selector and assert_text on that HTML, so a test can check the span’s class, its text and that a name with markup stayed text. In the lesson’s run, three such tests with five assertions passed, one for each state of the badge.

Two questions in the module bank go further than this page: escaping inside components, and deciding when a team should adopt them. Work out your own answer first, then open the model answer.

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

  1. Mid to staff

    A security audit of an app that uses partials, ViewComponent and Phlex asks how output escaping works in each and where it can be bypassed. Explain it, and describe how you would check the code base.

    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

    A Rails app has 40 partials; 3 of them are copied across two apps and have many states. The team asks whether to adopt ViewComponent, Phlex, or neither. What do you ask first, what do you recommend, and how do you roll it out?

    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. What checks a partial's locals when it declares no strict locals?

    Nothing. A misspelt local silently vanishes, and each state of the partial can be tested only by rendering a whole page around it.

  2. What is a presenter, and what should its methods return?

    A plain Ruby object that formats one record for display and can be tested without the app running. It returns strings, never markup.

  3. How is a ViewComponent built and tested?

    It is a class with its template beside it: initialize checks the arguments, and the template runs with the component as self. render_inline tests and preview classes show each state on its own.

  4. How does Phlex write a view?

    As a Ruby class whose methods produce the HTML, one call per element. phlex-rails lets render take such a class the way it takes a partial or a component.

  5. Do partials, ViewComponents and Phlex views escape their output?

    All three escape by default, and each has one explicit way around it, such as raw, html_safe or Phlex's raw(safe(...)). That way must never carry text a person typed.

  6. When does a presenter beat a helper?

    When a formatting rule belongs to one kind of record and deserves its own tests. A helper is mixed into every view and suits a small piece of markup used everywhere, such as a link with an icon.

Key takeaways

  • A partial’s locals are checked by nothing unless it declares strict locals, so a misspelt local silently disappears, and every state of it is tested only through a whole page.
  • A presenter is plain Ruby that formats one record for display, testable without an app, and it returns strings, never markup.
  • A ViewComponent is a class with its template beside it: initialize checks the arguments, the template runs with the component as self, and render_inline tests and preview classes show every state on its own.
  • Phlex builds the same HTML from Ruby methods in one class, and phlex-rails lets render take it like a partial or a component.
  • All three escape output by default, and each has one explicit opt-out (raw, html_safe, raw(safe(…))) that must never carry text a person typed.

Terms in this lesson

Phlex
A Ruby gem for views written as Ruby classes: a view_template method calls one method per HTML element and escapes text written with plain; phlex-rails lets Rails render such a class.
presenter
A plain Ruby object that wraps one record and answers the questions a view asks about it, such as a date in local time or a fee in rupees, returning strings so the template only prints them.
ViewComponent
A gem that turns a piece of view into a Ruby class with its template beside it: initialize declares the arguments, and the component can be rendered, tested with render_inline and previewed on its own.

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.