Skip to content

Lesson 3 — Text, whitespace, and the difference from ERB

How text gets into the output

In Phlex, text content is output by returning a string from an element’s block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
require "phlex"
require "date"

class TextDemo < Phlex::HTML
  def view_template
    p { "This is text from a block return value." }
  end
end

puts TextDemo.new.call
# => <p>This is text from a block return value.</p>

This works because Phlex uses the block’s return value as the element’s text content — but only under a specific condition. The official Phlex documentation states it precisely:

“If the return value of the block is a String, Symbol, Integer or Float and no output methods were used, the return value will be output as text.”

That qualifying clause — and no output methods were used — is the most important thing to understand about text output in Phlex. Everything else follows from it.

What “no output methods were used” means in practice

The moment a tag method call appears inside a block, the return value mechanism is bypassed entirely. Only explicit output method calls produce output. Bare strings — even as the last expression — are silently discarded.

This is a common source of confusion. Here is what happens when you try to mix bare strings with tag method calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
require "phlex"
require "date"

class BrokenMix < Phlex::HTML
  def view_template
    p do
      "Visit "                          # discarded — a tag call follows
      a(href: "/docs") { "the documentation" }
      " for more details."              # discarded — tag method was called
    end                                 #             so return value is ignored
  end
end

puts BrokenMix.new.call
# => <p><a href="/docs">the documentation</a></p>

Both strings are lost. The a tag method outputs directly to the buffer and returns nil. Since an output method was used inside the block, Phlex ignores the block’s return value entirely — only what was explicitly written to the buffer appears in the output.

This is a significant difference from ERB, where <%= "Visit " %> would output that string regardless of what else appeared around it.

plain — explicit text output

plain is the solution. It writes text directly to the output buffer immediately, regardless of its position in the block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
require "phlex"
require "date"

class CorrectMix < Phlex::HTML
  def view_template
    p do
      plain "Visit "
      a(href: "/docs") { "the documentation" }
      plain " for more details." 
    end
  end
end

puts CorrectMix.new.call
# => <p>Visit <a href="/docs">the documentation</a> for more details.</p>

Each plain call writes its text to the buffer immediately and in order. The output is exactly what you would expect.

When to use plain and when not to

Use plain whenever:

  • You need text before or between elements inside a block
  • The block contains any tag method call and you need text output
  • You are building text from logic and want to be explicit about what is output

For a simple element containing only text, the block return value is fine and plain is unnecessary:

1
2
h1 { "Page title" }         # clear, unambiguous — no tag calls, return value works
p  { @user.display_name }   # same — single expression, obvious intent

Reserve plain for cases where output methods are also present in the block.

A realistic example

Here is a byline paragraph that requires plain — it cannot be built correctly using block return values alone:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require "phlex"
require "date"

class ArticleByline < Phlex::HTML
  def initialize(author:, date:, reading_time:)
    @author       = author
    @date         = date
    @reading_time = reading_time
  end

  def view_template
    p do
      plain "Written by "
      strong { @author }
      plain " on #{@date} · "
      span { "#{@reading_time} min read" }
    end
  end
end

puts ArticleByline.new(
  author:       "Alice",
  date:         "30 March 2025",
  reading_time: 4
).call

Output:

1
<p>Written by <strong>Alice</strong> on 30 March 2025 · <span>4 min read</span></p>

Without plain, "Written by " and " on 30 March 2025 · " would both be silently discarded because strong and span are output methods.

Whitespace

Phlex produces compact HTML with no extra whitespace between elements:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
require "phlex"
require "date"

class WhitespaceDemo < Phlex::HTML
  def view_template
    span { "First" }
    span { "Second" }
    span { "Third" }
  end
end

puts WhitespaceDemo.new.call
# => <span>First</span><span>Second</span><span>Third</span>

No spaces between the spans. For inline elements where text flow requires a space, be explicit with plain:

1
2
3
4
5
6
def view_template
  span { "Hello" }
  plain " " 
  span { "world" }
end
# => <span>Hello</span> <span>world</span>

In practice, use CSS for spacing between elements rather than injecting whitespace characters — it is more maintainable and avoids unexpected layout behaviour.

The whitespace helper

For cases where you genuinely need a whitespace character in the output, Phlex provides whitespace as a cleaner alternative to plain " ":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
require "phlex"
require "date"

class Links < Phlex::HTML
  def view_template
    a(href: "/")       { "Home" }
    whitespace
    a(href: "/about")  { "About" }
    whitespace
    a(href: "/contact") { "Contact" }
  end
end

puts Links.new.call
# => <a href="/">Home</a> <a href="/about">About</a> <a href="/contact">Contact</a>

Use whitespace when the sole purpose is adding a single space between inline elements. Use plain { " " } when the space is part of a larger string construction — plain { ", " } for example — to keep the intent consistent.

whitespace also accepts a block, in which case a space is output on both sides of the block’s content:

1
2
whitespace { span { "surrounded by spaces" } }
# => " <span>surrounded by spaces</span> "

String interpolation

String interpolation works naturally inside any text-producing block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
require "phlex"
require "date"

class Greeting < Phlex::HTML
  def initialize(name:, time_of_day: "day")
    @name        = name
    @time_of_day = time_of_day
  end

  def view_template
    h1 { "Good #{@time_of_day}, #{@name}!" }
  end
end

puts Greeting.new(name: "Alice", time_of_day: "morning").call
# => <h1>Good morning, Alice!</h1>

There is nothing special to learn here — it is just Ruby string interpolation. The contrast with ERB is worth noting though. In ERB you would write:

<h1>Good <%= @time_of_day %>, <%= @name %>!</h1>

Mixing two syntaxes inside one element. In Phlex it is one Ruby string with normal interpolation — no context switching.

Where things get more interesting is combining interpolation with plain for multi-part text. Because it is all Ruby, any expression works inside the interpolation — ternaries, method calls, formatting — without reaching for a helper:

1
2
3
4
5
6
def view_template
  p do
    plain "Hello, #{@name}. "
    plain "You have #{@count} #{"message".then { |w| @count == 1 ? w : "#{w}s" }}."
  end
end

Helper methods for sub-templates

Because view_template is a plain Ruby method, you can extract parts of it into private helper methods. This is the idiomatic way to break up a large template without creating separate component files:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
require "phlex"
require "date"

class Dashboard < Phlex::HTML
  def initialize(user:, stats:)
    @user  = user
    @stats = stats
  end

  def view_template
    div do
      render_header
      render_stats
    end
  end

  private

  def render_header
    header do
      h1 { "Welcome back, #{@user}" }
    end
  end

  def render_stats
    section do
      @stats.each do |label, value|
        div do
          span { label }
          strong { value.to_s }
        end
      end
    end
  end
end

puts Dashboard.new(
  user:  "Alice",
  stats: { "Boards" => 4, "Cards" => 37, "Members" => 12 }
).call

Output:

1
2
3
4
5
6
7
8
<div>
  <header><h1>Welcome back, Alice</h1></header>
  <section>
    <div><span>Boards</span><strong>4</strong></div>
    <div><span>Cards</span><strong>37</strong></div>
    <div><span>Members</span><strong>12</strong></div>
  </section>
</div>

Private helper methods are the natural way to decompose a complex view_template without the overhead of separate component classes. Use them freely — they are just Ruby methods.

ERB vs Phlex: the mental model shift

In ERB the template is a string with Ruby interpolated into it. The default mode is “output everything”, and you opt in to Ruby with <% %> and <%= %>:

<p>
  Written by <%= @author %> on <%= @date %>
</p>

In Phlex the default mode is “execute Ruby”, and you explicitly opt in to output with method calls — tag methods, plain, and render. Nothing is output unless you call a method that outputs it. The block return value is a convenience shortcut that only applies when no output methods are present.

This distinction has a practical consequence: in ERB it is easy to accidentally output nothing (used <% rather than <%= …), output too much (stray return value), or create invisible whitespace. In Phlex, if something appears in the output, you put it there deliberately.

Exercise

Create 03_exercise.rb. Build a ProseComponent that renders an article with:

  • A heading
  • A byline built from multiple parts: "By ", then a <strong> with the author name, then " · ", then the date — all inside a single <p> tag
  • Two body paragraphs passed in as an array

For example, calling it like this:

1
2
3
4
5
6
7
8
9
puts ProseComponent.new(
  title:  "Getting started with Phlex",
  author: "Alice",
  date:   "30 March 2025",
  body:   [
    "Phlex is a Ruby gem for building HTML components.",
    "It uses plain Ruby methods to describe HTML structure."
  ]
).call

Should produce:

1
2
3
4
5
6
<article>
  <h1>Getting started with Phlex</h1>
  <p>By <strong>Alice</strong> · 30 March 2025</p>
  <p>Phlex is a Ruby gem for building HTML components.</p>
  <p>It uses plain Ruby methods to describe HTML structure.</p>
</article>

The byline <p> cannot be built correctly using block return values alone — you must use plain. The body paragraphs can be rendered with a simple each loop.


Solution for Exercise 03
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require "phlex"
require "date"

class ProseComponent < Phlex::HTML
  def initialize(title:, author:, date:, body:)
    @title = title
    @author = author
    @date = date
    @body = body
  end

  def view_template
    article do
      h1 { @title }
      p do
        plain "By "
        strong { @author }
        plain " . #{@date}"
      end
      @body.each do |line|
        p { line }
      end
    end
  end
end

puts ProseComponent.new(
  title:  "Getting started with Phlex",
  author: "Alice",
  date:   "30 March 2025",
  body:   [
    "Phlex is a Ruby gem for building HTML components.",
    "It uses plain Ruby methods to describe HTML structure."
  ]
).call