Skip to content

Lesson 5 — Named slots: the problem and the solution

A panel with a single yield point is clean and simple. But what about a Card component with a distinct header, body, and footer — each needing independent rich content?

The naive attempt — and why it fails

The instinct is to define methods for each slot and call them from the template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# NAIVE — this does not work
module Components
  class Card < Base
    def view_template
      article do
        header { @header }
        div    { @body   }
        footer { @footer }
      end
    end

    def header(&) = @header = capture(&)
    def body(&)   = @body   = capture(&)
    def footer(&) = @footer = capture(&)
  end
end

Used like this:

1
2
3
4
5
html = Components::Card.new do |card|
  card.header { h3 { "Card title" } }
  card.body   { p  { "Card body." } }
  card.footer { button { "Save"   } }
end

Run it and the output is:

1
2
3
4
5
<article>
  <header></header>
  <div></div>
  <footer></footer>
</article>

Empty elements. The slots are not populated.

Why it fails

The problem is timing. When the component is invoked, view_template runs immediately. At that point @header, @body, and @footer are all nil — the three empty elements are rendered. Then the block executes, populating the instance variables — but the HTML has already been built and returned. The slot content arrives too late.

The fix is to yield the block before rendering any HTML, so the slot methods run and populate their instance variables first.

vanish — yield first, render second

vanish yields the block and discards any HTML output it produces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def view_template(&)
  vanish(&)   # execute the block — populate @header, @body, @footer
              # discard any HTML output the block produces

  article do
    header { raw safe(@header) } if @header
    div    { raw safe(@body)   } if @body
    footer { raw safe(@footer) } if @footer
  end
end

Now the sequence is correct:

  1. vanish yields the block — card.header, card.body, card.footer run
  2. Each slot method calls capture(&) — storing rendered HTML in an instance variable
  3. vanish returns — all three instance variables are populated
  4. The article renders — raw safe(@header) etc. output the captured HTML

capture and raw safe

capture runs a block and returns its Phlex output as a string rather than writing it to the buffer. This lets us store rendered HTML for later use.

raw safe outputs a pre-rendered HTML string back to the buffer, bypassing escaping. This is safe here because the content came from capture — it was already escaped by Phlex when each element inside the block was rendered.

vanish vs yield(self) if block_given?

Both yield the block with the component as the argument. The difference:

Yields self Discards HTML output
yield(self) if block_given? Yes No
vanish(&) Yes Yes

For named slot components, vanish is always the right choice. The slot methods use capture internally, so their output goes into instance variables rather than the buffer. Any HTML that accidentally reaches the buffer during the block execution would appear in the wrong place — vanish prevents this.

Use yield only when you genuinely want the block’s HTML output to appear at that point in the template — the simple single yield case from Lesson 4.

The full Card component

Pico styles <article>, <header>, and <footer> natively — the structure is genuinely semantic HTML with no extra classes needed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# app/components/card.rb
require_relative "base"

module Components
  class Card < Base
    def view_template(&)
      vanish(&)

      article do
        div(class: "card-header") { raw safe(@header)  if @header  }
        div(class: "card-body")   { raw safe(@body)  if @body  }
        div(class: "card-footer") { raw safe(@footer)  if @footer  }
      end
    end

    def header(&) = @header = capture(&)
    def body(&) = @body = capture(&)
    def footer(&) = @footer = capture(&)
  end
end

Adding Card to the demo

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

def show_cards
  section_header("Cards")
  div(class: "demo-grid-2") do

    # Simple form — title only, body as a plain block
    Card(title: "Simple card") {
      p { "Just a title and a body." }
    }

    # Full slots
    Card().call do |card|
      card.header { h3 { "Full card" } }
      card.body   { p { "With header, body, and footer slots." } }
      card.footer { Button(label: "Action", variant: :secondary) }
    end

  end
end

Block syntax note: When calling Kit methods like Card(...) with a simple block, use {} braces. When calling .call directly with a multi-line block, use do...end or assign to a variable first. See the block precedence note in Lesson 6 for a full explanation.

Exercise — Components::Section

Create app/components/section.rb. Build a Components::Section component with a title: prop and two named slots — body and aside.

Expected usage:

1
2
3
4
html = Components::Section.new(title: "Latest posts").call do |s|
  s.body  { p { "Main content here." } }
  s.aside { p { "Sidebar content here." } }
end

Expected output:

1
2
3
4
5
<section>
  <h2>Latest posts</h2>
  <div class="section-body"><p>Main content here.</p></div>
  <div class="section-aside"><p>Sidebar content here.</p></div>
</section>

Then add it to the demo:

1
2
3
4
5
6
7
def show_sections
  section_header("Sections")
  Section(title: "Featured content").call do |s|
    s.body  { p { "This is the main body area." } }
    s.aside { p { "This is the sidebar area."  } }
  end
end

Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# app/components/section.rb
require_relative "base"

module Components
  class Section < Base
    prop :title, String

    def view_template(&)
      vanish(&)

      section do
        h2 { @title }
        div(class: "section-body")  { raw safe(@body)  if @body  }
        div(class: "section-aside") { raw safe(@aside) if @aside }
      end
    end

    def body(&)  = @body  = capture(&)
    def aside(&) = @aside = capture(&)
  end
end