Skip to content

Lesson 2 — Attributes, booleans and data attributes

Passing attributes to tags

Every tag method accepts a hash of attributes as its first argument:

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

class AttributeDemo < Phlex::HTML
  def view_template
    div(id: "main", class: "container") do
      a(href: "https://phlex.fun", target: "_blank") { "Phlex docs" }
    end
  end
end

puts AttributeDemo.new.call
# => <div id="main" class="container"><a href="https://phlex.fun" target="_blank">Phlex docs</a></div>

Attribute names map directly to HTML attribute names. Use symbols or strings — both work, though symbols are conventional.

Class is a reserved word — what about class:?

class is a Ruby reserved word, but Phlex handles it cleanly: pass class: as a keyword argument and Phlex writes class="..." in the output.

1
2
div(class: "flex items-center gap-4") { "content" }
# => <div class="flex items-center gap-4">content</div>

Dynamic attributes

Attributes are just Ruby expressions, so they can be dynamic:

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

class StatusBadge < Phlex::HTML
  COLOURS = {
    "active"   => "badge-green",
    "inactive" => "badge-gray",
    "banned"   => "badge-red",
  }

  def initialize(status)
    @status = status
  end

  def view_template
    span(class: COLOURS.fetch(@status, "badge-gray")) { @status }
  end
end

puts StatusBadge.new("active").call
puts StatusBadge.new("banned").call
puts StatusBadge.new("unknown").call

Output:

1
2
3
<span class="badge-green">active</span>
<span class="badge-red">banned</span>
<span class="badge-gray">unknown</span>

Boolean attributes

HTML has boolean attributes — disabled, checked, required, readonly, multiple — that are either present or absent. In Phlex, pass true to include them and false (or omit) to exclude them:

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

class FormDemo < Phlex::HTML
  def view_template
    # disabled: true  => renders disabled="disabled"
    # disabled: false => attribute is omitted entirely
    input(type: "text",     disabled: true,  value: "Can't edit this")
    input(type: "checkbox", checked: true)
    input(type: "text",     required: true,  placeholder: "Required field")
    input(type: "text",     disabled: false, placeholder: "Normal field")
  end
end

puts FormDemo.new.call

Output:

1
2
3
4
<input type="text" disabled value="Can't edit this">
<input type="checkbox" checked>
<input type="text" required placeholder="Required field">
<input type="text" placeholder="Normal field">

false means the attribute is not written at all — exactly the right HTML behaviour.

Data attributes

Pass data attributes as a nested hash under data::

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

class DropdownButton < Phlex::HTML
  def initialize(label:, target:)
    @label  = label
    @target = target
  end

  def view_template
    button(
      type: "button",
      data: {
        controller: "dropdown",
        dropdown_target: @target,
        dropdown_open_value: false
      }
    ) { @label }
  end
end

puts DropdownButton.new(label: "Open menu", target: "menu").call

Output:

1
2
3
4
5
6
<button type="button"
        data-controller="dropdown"
        data-dropdown-target="menu"
        data-dropdown-open-value="false">
  Open menu
</button>

Phlex automatically converts nested hash keys to kebab-case data attributes. Underscores become hyphens: dropdown_target:data-dropdown-target. This is exactly the format Stimulus expects — no string wrangling required.

Aria attributes

The same pattern works for aria::

1
2
3
4
5
button(
  type: "button",
  aria: { expanded: false, controls: "main-menu" }
) { "Toggle menu" }
# => <button type="button" aria-expanded="false" aria-controls="main-menu">Toggle menu</button>

Combining multiple classes dynamically

A common pattern is building a class string from multiple sources — a base class, a variant, and a conditional modifier:

 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 Button < Phlex::HTML
  BASE    = "inline-flex items-center rounded font-medium"
  VARIANTS = {
    primary:   "bg-blue-600 text-white",
    secondary: "bg-gray-100 text-gray-900",
    danger:    "bg-red-600 text-white",
  }
  SIZES = {
    sm: "px-3 py-1.5 text-sm",
    md: "px-4 py-2 text-base",
    lg: "px-6 py-3 text-lg",
  }

  def initialize(label, variant: :primary, size: :md, disabled: false)
    @label    = label
    @variant  = variant
    @size     = size
    @disabled = disabled
  end

  def view_template
    button(
      type: "button",
      class: [BASE, VARIANTS[@variant], SIZES[@size], ("opacity-50 cursor-not-allowed" if @disabled)].compact.join(" "),
      disabled: @disabled
    ) { @label }
  end
end

puts Button.new("Save",   variant: :primary).call
puts Button.new("Cancel", variant: :secondary, size: :sm).call
puts Button.new("Delete", variant: :danger,    disabled: true).call

Output:

1
2
3
<button type="button" class="inline-flex items-center rounded font-medium bg-blue-600 text-white px-4 py-2 text-base">Save</button>
<button type="button" class="inline-flex items-center rounded font-medium bg-gray-100 text-gray-900 px-3 py-1.5 text-sm">Cancel</button>
<button type="button" class="inline-flex items-center rounded font-medium bg-red-600 text-white px-4 py-2 text-base opacity-50 cursor-not-allowed" disabled="disabled">Delete</button>

We’ll refine this pattern considerably in Module 7 when we introduce design tokens, but this is the foundation.

Exercise

Create 02_exercise.rb. Build an InputComponent that renders an <input> element accepting type:, name:, placeholder:, required:, and disabled: arguments. Add a data: hash that includes a controller key set to "input-validation".

For example, calling it like this:

ruby

1
2
3
4
5
6
7
puts InputComponent.new(
  type:        "email",
  name:        "user_email",
  placeholder: "Enter your email",
  required:    true,
  disabled:    false
).call

Should produce:

html

1
2
3
4
5
<input type="email"
       name="user_email"
       placeholder="Enter your email"
       required="required"
       data-controller="input-validation">

Notice that required: true becomes required="required", disabled: false is omitted entirely from the output, and the data: { controller: "input-validation" } hash becomes data-controller="input-validation".

Test it with at least three different configurations — try a disabled text field, a required password field, and a plain text field with no required or disabled state — so you can see how the boolean attributes behave in each case.


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

class InputComponent < Phlex::HTML
  def initialize(type:, name:, placeholder: nil, required: false, disabled: false)
    @type        = type
    @name        = name
    @placeholder = placeholder
    @required    = required
    @disabled    = disabled
  end

  def view_template
    input(
      type:        @type,
      name:        @name,
      placeholder: @placeholder,
      required:    @required,
      disabled:    @disabled,
      data:        { controller: "input-validation" }
    )
  end
end

# A required email field
puts InputComponent.new(
  type:        "email",
  name:        "user_email",
  placeholder: "Enter your email",
  required:    true
).call

# A disabled text field
puts InputComponent.new(
  type:     "text",
  name:     "username",
  disabled: true
).call

# A plain password field
puts InputComponent.new(
  type: "password",
  name: "user_password"
).call