Skip to content

Lesson 4 — Yielding: the single yield point

So far all our components render entirely from their props. But what about components that need to contain arbitrary content — a panel with a list, a card with a table inside it? We cannot pass rich HTML as a prop safely. We need yielding.

The simplest case

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# app/components/panel.rb
require_relative "base"

module Components
  class Panel < Base
    prop :title, String

    def view_template
      section do
        h4 { @title }
        yield
      end
    end
  end
end

Usage:

1
2
3
html = Components::Panel.new(title: "Recent activity") do
  p { "Nothing to show yet." }
end

Output:

1
2
3
4
<section>
  <h4>Recent activity</h4>
  <p>Nothing to show yet.</p>
</section>

The block passed to the component is yielded inside the section. Whatever Phlex output the block produces appears at that point in the template.

yield(self) — what the block argument actually is

When you write:

1
2
3
Panel(title: "Stats") do |panel|
  panel.something
end

You might assume panel is some kind of proxy object. It is not - panel is the Panel instance itself.

Phlex automatically upgrades yield to yield(self) inside view_template. This means the block receives the component as its argument, allowing callers to invoke methods on the component from within the block. We will use this extensively in Lesson 5 for named slots.

Adding Panel to the demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require_relative "app/components/panel"

def show_panels
  section_header("Panels")
  div(class: "demo-grid-3") do
    Panel(title: "Fruits") do
      ul do
        %w[Apple Banana Cherry].each { |f| li { f } }
      end
    end

    Panel(title: "About Phlex") do
      p { "Phlex is a Ruby gem for building HTML components." }
    end

    Panel(title: "Status") do
      div(class: "demo-row") do
        Badge(label: "Active",   variant: :success)
        Badge(label: "Pending",  variant: :warning)
        Badge(label: "Archived", variant: :default)
      end
    end
  end
end

The third panel demonstrates composition — Badge components used freely inside the yield block of a Panel.

Exercise

Add a fourth panel to show_panels that renders a nested Button and a short paragraph of text inside it. This reinforces that any component can be composed inside a yield block.


Solution
1
2
3
4
Panel(title: "Actions") do
  p { "Ready to proceed?" }
  Button(label: "Continue", variant: :primary)
end