Skip to content

Lesson 1 — Your first component: view_template, tags, nesting

What is a Phlex component?

A Phlex component is a Ruby class that inherits from Phlex::HTML and defines a method called view_template. That method describes the HTML structure using Ruby method calls.

Every HTML tag has a corresponding Ruby method. You nest elements by passing blocks. That’s the entire mental model.

Hello, Phlex

Create 01_hello.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 01_hello.rb

require "phlex"
require "date"

class HelloComponent < Phlex::HTML
  def view_template
    h1 { "Hello, Phlex!" }
  end
end

puts HelloComponent.new.call

Run it:

1
2
ruby 01_hello.rb
# => <h1>Hello, Phlex!</h1>

One method call. One HTML element. The block’s return value becomes the element’s text content.

Nesting elements

HTML is a tree. Phlex mirrors that with nested blocks:

 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
# 02_my_article.rb

require "phlex"
require "date"

class ArticleComponent < Phlex::HTML
  def view_template
    article do
      header do
        h1 { "My Article" }
        p { "Published today" }
      end

      section do
        p { "First paragraph of content." }
        p { "Second paragraph of content." }
      end

      footer do
        p { "Thanks for reading." }
      end
    end
  end
end

puts ArticleComponent.new.call

Output (formatted for readability):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<article>
  <header>
    <h1>My Article</h1>
    <p>Published today</p>
  </header>
  <section>
    <p>First paragraph of content.</p>
    <p>Second paragraph of content.</p>
  </section>
  <footer>
    <p>Thanks for reading.</p>
  </footer>
</article>

The indentation in the Ruby mirrors the HTML structure exactly. This is one of Phlex’s great readability wins over ERB — the nesting is visible in the code, not implied by open/close tags.

Accepting data via initialize

A component that only renders static content isn’t very useful. Components accept data through their initializer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 03_params.rb

require "phlex"
require "date"

class UserCard < Phlex::HTML
  def initialize(name:, email:, role: "member")
    @name  = name
    @email = email
    @role  = role
  end

  def view_template
    div do
      h2 { @name }
      p  { @email }
      span { @role }
    end
  end
end

puts UserCard.new(name: "Alice", email: "alice@example.com", role: "admin").call
puts UserCard.new(name: "Bob",   email: "bob@example.com").call

Output:

1
2
<div><h2>Alice</h2><p>alice@example.com</p><span>admin</span></div>
<div><h2>Bob</h2><p>bob@example.com</p><span>member</span></div>

Notice the explicit interface: keyword arguments, defaults, no surprises. Anyone reading UserCard.new(...) knows exactly what data the component needs. This is the first big advantage over ERB partials, which accept locals implicitly.

Ruby logic inside view_template

Because view_template is just a Ruby method, you can use any Ruby you like:

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

class PostList < Phlex::HTML
  POSTS = [
    { title: "Getting started with Phlex", published: true  },
    { title: "Advanced composition",        published: true  },
    { title: "Draft: Theming deep dive",    published: false },
  ]

  def view_template
    ul do
      POSTS.each do |post|
        li do
          span { post[:title] }
          span { post[:published] ? " (published)" : " (draft)" }
        end
      end
    end
  end
end

puts PostList.new.call

Output:

1
2
3
4
5
<ul>
  <li><span>Getting started with Phlex</span><span> (published)</span></li>
  <li><span>Advanced composition</span><span> (published)</span></li>
  <li><span>Draft: Theming deep dive</span><span> (draft)</span></li>
</ul>

each, map, if, case, unless — all of it works, because it’s all just Ruby.

Rendering one component inside another

Components compose naturally:

 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
require "phlex"
require "date"

class Badge < Phlex::HTML
  def initialize(text)
    @text = text
  end

  def view_template
    span { @text }
  end
end

class UserCard < Phlex::HTML
  def initialize(name:, role:)
    @name = name
    @role = role
  end

  def view_template
    div do
      h2 { @name }
      render Badge.new(@role)
    end
  end
end

puts UserCard.new(name: "Alice", role: "admin").call

Output:

1
<div><h2>Alice</h2><span>admin</span></div>

render is how you embed one component inside another. The rendered component’s output is inserted at that point in the parent’s HTML tree.

Exercise

Create 01_exercise.rb. Build a NavigationComponent that renders a <nav> containing a <ul> with three <li> items: Home, About, Contact. Each <li> should contain an <a> tag. Pass the nav items in as an array of hashes [{ label:, href: }] via initialize.

Note: An <a> tag with an href attribute looks like this: a(href: "/") { "Home" } — we’ll cover attributes properly in Lesson 2.

Calling it like:

1
2
3
4
5
NavigationComponent.new(items: [
  { label: "Home",    href: "/" },
  { label: "About",   href: "/about" },
  { label: "Contact", href: "/contact" }
]).call

should produce:

1
2
3
4
5
6
7
<nav>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Solution to Exercise 01
 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
require "phlex"
require "date"

class NavigationComponent < Phlex::HTML

  def initialize(items:)
    @items = items
  end

  def view_template
    nav do
      ul do
        @items.each do |item|
          li do
            a(href: item[:href]) { item[:label]}
          end
        end
      end
    end
  end
end

puts NavigationComponent.new(items: [
  { label: "Home",    href: "/" },
  { label: "About",   href: "/about" },
  { label: "Contact", href: "/contact" }
]).call