Skip to content

Lesson 6 — Building HTML strings without a component class

When you don’t need a class

Sometimes you just want to build a small HTML fragment without the overhead of defining a full component class. Phlex provides two ways to do this.

Phlex::HTML.new with a block (inline rendering)

You can instantiate Phlex::HTML directly and pass a block:

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

html = Phlex::HTML.new do
  p { "This is rendered without a named class." }
  ul do
    %w[Apples Bananas Cherries].each { |item| li { item } }
  end
end

puts html.call

Output:

1
2
<p>This is rendered without a named class.</p>
<ul><li>Apples</li><li>Bananas</li><li>Cherries</li></ul>

Useful for one-off fragments in scripts, tests, or generators.

The Phlex.fragment helper

For even smaller inline fragments, Phlex.fragment produces an HTML string directly:

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

fragment = Phlex.fragment do
  strong { "Important" }
  plain ": pay attention to this."
end

puts fragment
# => <strong>Important</strong>: pay attention to this.

Practical use: generating HTML in a script

The real value of standalone Phlex without Rails is in scripts, data exporters, email generators, and test helpers. Here’s a realistic example — a script that generates an HTML report from a data structure:

 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
require "phlex"

# Domain data — in real life this would come from a database or API
REPORT_DATA = {
  title:     "Weekly Summary",
  generated: "2025-03-30",
  sections: [
    {
      heading: "New sign-ups",
      rows: [
        { name: "Alice",   email: "alice@example.com", plan: "Pro"  },
        { name: "Bob",     email: "bob@example.com",   plan: "Free" },
        { name: "Charlie", email: "charlie@example.com", plan: "Pro" },
      ]
    },
    {
      heading: "Churned accounts",
      rows: [
        { name: "Dave", email: "dave@example.com", plan: "Pro" },
      ]
    }
  ]
}

class ReportTable < Phlex::HTML
  def initialize(rows)
    @rows = rows
  end

  def view_template
    table do
      thead do
        tr do
          th { "Name" }
          th { "Email" }
          th { "Plan" }
        end
      end
      tbody do
        @rows.each do |row|
          tr do
            td { row[:name] }
            td { row[:email] }
            td { row[:plan] }
          end
        end
      end
    end
  end
end

class WeeklyReport < Phlex::HTML
  def initialize(data)
    @data = data
  end

  def view_template
    html do
      head do
        title { @data[:title] }
      end
      body do
        h1 { @data[:title] }
        p { "Generated: #{@data[:generated]}" }

        @data[:sections].each do |section|
          section do
            h2 { section[:heading] }
            render ReportTable.new(section[:rows])
          end
        end
      end
    end
  end
end

# Write the report to a file
File.write("report.html", WeeklyReport.new(REPORT_DATA).call)
puts "Report written to report.html"

Run it:

1
2
ruby 06_report.rb
# => Report written to report.html

Open report.html in a browser. It’s a fully rendered HTML document.

doctype

Notice that the example above calls html do ... end, which produces <html>...</html>. To get a proper <!DOCTYPE html> declaration, use the doctype method:

1
2
3
4
5
6
7
8
9
class FullDocument < Phlex::HTML
  def view_template
    doctype  # Outputs <!DOCTYPE html>
    html do
      head { title { "My Page" } }
      body { p { "Content" } }
    end
  end
end

Using Phlex::HTML in a Rack app (preview of what Rails does)

Just to make the bridge to Rails concrete, here’s a Phlex component serving HTTP responses through a bare Rack app:

 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"

class HelloPage < Phlex::HTML
  def initialize(name)
    @name = name
  end
  def view_template
    doctype
    html do
      head { title { "Hello" } }
      body { h1 { "Hello, #{@name}!" } }
    end
  end
end

# A minimal Rack app
App = lambda do |env|
  name = env["QUERY_STRING"].match(/name=(\w+)/)&.captures&.first || "World"
  html = HelloPage.new(name).call
  [200, { "content-type" => "text/html" }, [html]]
end


# Run with: rackup -p 3000 config.ru
# You will need a config.ru file that contains:
#
# require_relative "hello_page"
# run App

# Or test inline (comment out the run App above and uncomment below):
# env = { "QUERY_STRING" => "name=Alice" }
# status, headers, body = App.call(env)
# puts body.first
# body = App.call(env)
# puts body.first

This is almost exactly what Rails does under the hood: a controller calls a view object’s render method and returns the HTML string as the response body. Rails adds routing, middleware, and convention — but the Phlex rendering mechanism is identical to this.

Exercise

Create 06_exercise.rb. Write a script that generates a standalone HTML page containing a table of five fictional products — name, price, and stock count. Use at least two component classes (ProductTable and a ProductRow sub-component). Call doctype and wrap everything in an html/head/body structure. Write the output to products.html and open it in a browser.


Solution to Exercise 06
 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 06_exercise.rb
require "phlex"
require "date"

PRODUCTS = [
  { name: "Leather Notebook",  price: 12.99, stock: 42 },
  { name: "Brass Fountain Pen",price: 34.50, stock: 17 },
  { name: "Wooden Ruler",      price:  4.99, stock: 83 },
  { name: "Wax Seal Kit",      price: 19.99, stock:  9 },
  { name: "Ink Cartridges",    price:  8.75, stock: 56 },
]

class ProductRow < Phlex::HTML
  def initialize(product)
    @product = product
  end

  def view_template
    tr do
      td { @product[:name] }
      td { "$#{"%.2f" % @product[:price]}" }
      td { @product[:stock].to_s }
    end
  end
end

class ProductTable < Phlex::HTML
  def initialize(products)
    @products = products
  end

  def view_template
    doctype
    html do
      head do
        title { "Product Catalogue" }
      end
      body do
        h1 { "Product Catalogue" }
        table do
          thead do
            tr do
              th { "Product" }
              th { "Price"   }
              th { "Stock"   }
            end
          end
          tbody do
            @products.each do |product|
              render ProductRow.new(product)
            end
          end
        end
      end
    end
  end
end

File.write("products.html", ProductTable.new(PRODUCTS).call)
system("open products.html")
puts "Written to products.html"

Module 2 summary

You now have a complete mental model of Phlex without any Rails magic obscuring it:

  • A Phlex component is a Ruby class with a view_template method
  • HTML tags are Ruby method calls; nesting is done with blocks
  • Data comes in through initialize — the interface is always explicit
  • Attributes are keyword arguments; booleans, data:, and aria: all follow natural Ruby patterns
  • Text content is always escaped — XSS is structurally prevented, not bolted on
  • Raw HTML requires an explicit safe() declaration — no accidental bypasses
  • SVG components inherit from Phlex::SVG and work identically to HTML components
  • You can generate HTML strings without a component class using Phlex.fragment or inline blocks
  • A Phlex component is just an object that returns an HTML string from call — Rails uses this, but it doesn’t require Rails

In Module 3 we take these primitives and start building real component architecture: base classes, composition, kits, and the first components of Phlex::UI.