Skip to main content

Overview

Phlex includes built-in helpers that make it easier to work with attributes, especially for merging classes and data attributes. The two main helpers are mix and grab.

The mix Helper

The mix helper intelligently merges hashes, particularly useful for combining attributes:
class Button < Phlex::HTML
  def initialize(type: "button", **attributes)
    @type = type
    @attributes = attributes
  end

  def view_template(&block)
    button(**default_attributes, **@attributes, &block)
  end

  private

  def default_attributes
    { type: @type, class: "btn" }
  end
end
But this doesn’t merge classes properly. Use mix instead:
class Button < Phlex::HTML
  def initialize(type: "button", **attributes)
    @type = type
    @attributes = attributes
  end

  def view_template(&block)
    button(**mix(default_attributes, @attributes), &block)
  end

  private

  def default_attributes
    { type: @type, class: "btn" }
  end
end

Button.new(class: "btn-primary").call { "Click" }
# => <button type="button" class="btn btn-primary">Click</button>

Merging Behavior

The mix helper has intelligent merging rules:

Strings

Strings are joined with spaces:
mix(
  { class: "btn" },
  { class: "btn-primary" }
)
# => { class: "btn btn-primary" }

Arrays

Arrays are concatenated:
mix(
  { class: ["btn"] },
  { class: ["btn-primary"] }
)
# => { class: ["btn", "btn-primary"] }

Sets

Sets are merged:
mix(
  { class: Set["btn"] },
  { class: Set["btn-primary"] }
)
# => { class: #<Set: {"btn", "btn-primary"}> }

Hashes

Hashes are recursively merged:
mix(
  { data: { controller: "dropdown" } },
  { data: { action: "click->dropdown#toggle" } }
)
# => { data: { controller: "dropdown", action: "click->dropdown#toggle" } }

Mixed Types

Different types are intelligently combined:
# String + Array
mix(
  { class: "btn" },
  { class: ["btn-primary", "active"] }
)
# => { class: ["btn", "btn-primary", "active"] }

# String + Set
mix(
  { class: "btn" },
  { class: Set["btn-primary"] }
)
# => { class: ["btn", "btn-primary"] }

Nil Values

Nil values don’t override existing values:
mix(
  { class: "btn" },
  { class: nil }
)
# => { class: "btn" }

Practical Examples

Base Component with Default Styles

class Card < Phlex::HTML
  def initialize(**attributes)
    @attributes = attributes
  end

  def view_template(&block)
    div(**mix(base_attributes, @attributes), &block)
  end

  private

  def base_attributes
    {
      class: "card",
      data: { controller: "card" }
    }
  end
end

# Usage
render Card.new(
  class: "card-highlighted",
  data: { card_id_value: 123 }
) do
  h3 { "Title" }
end

# Result:
# <div class="card card-highlighted" 
#      data-controller="card"
#      data-card-id-value="123">
#   <h3>Title</h3>
# </div>

Conditional Attributes

class Alert < Phlex::HTML
  def initialize(type:, dismissible: false, **attributes)
    @type = type
    @dismissible = dismissible
    @attributes = attributes
  end

  def view_template(&block)
    div(**mix(base_attributes, @attributes), &block)
  end

  private

  def base_attributes
    mix(
      { class: ["alert", "alert-#{@type}"] },
      @dismissible ? dismissible_attributes : {}
    )
  end

  def dismissible_attributes
    {
      class: "alert-dismissible",
      data: { controller: "alert" }
    }
  end
end

Multiple Levels of Inheritance

class BaseButton < Phlex::HTML
  def initialize(**attributes)
    @attributes = attributes
  end

  def view_template(&block)
    button(**mix(base_attributes, @attributes), &block)
  end

  private

  def base_attributes
    { class: "btn", type: "button" }
  end
end

class PrimaryButton < BaseButton
  private

  def base_attributes
    mix(super, { class: "btn-primary" })
  end
end

class LargePrimaryButton < PrimaryButton
  private

  def base_attributes
    mix(super, { class: "btn-lg" })
  end
end

LargePrimaryButton.new(class: "custom").call { "Click" }
# => <button type="button" class="btn btn-primary btn-lg custom">Click</button>

The grab Helper

The grab helper extracts values from keyword arguments:
def view_template
  title = grab(title: "Default Title")
  # title = "Default Title"
end

Single Value

Extract one value:
class Modal < Phlex::HTML
  def initialize(**options)
    @title = grab(options, title: "Untitled")
    @size = grab(options, size: "medium")
  end

  def view_template
    div class: "modal modal-#{@size}" do
      h2 { @title }
    end
  end
end

Multiple Values

Extract multiple values as an array:
class Form < Phlex::HTML
  def initialize(**options)
    @method, @action = grab(options, method: "post", action: "/submit")
  end

  def view_template(&block)
    form method: @method, action: @action, &block
  end
end
When you pass multiple bindings to grab, it returns an array of values. With a single binding, it returns just the value.

Combining mix and grab

Use both helpers together for flexible components:
class FlexibleCard < Phlex::HTML
  def initialize(**options)
    @title = grab(options, title: nil)
    @attributes = options # Remaining options
  end

  def view_template(&block)
    div(**mix(base_attributes, @attributes)) do
      h3 { @title } if @title
      div(class: "card-body", &block)
    end
  end

  private

  def base_attributes
    {
      class: "card",
      data: { controller: "card" }
    }
  end
end

# Usage
render FlexibleCard.new(
  title: "My Card",
  class: "shadow",
  data: { card_id_value: 123 }
) do
  p { "Card content" }
end

Force Attribute Replacement

Use the ! suffix to force attribute replacement instead of merging:
mix(
  { class: "btn" },
  { class!: "completely-different" }
)
# => { class: "completely-different" }
The ! suffix removes the exclamation mark from the key name and replaces the value entirely instead of merging.

Nested Data Attributes

Combine nested data attributes elegantly:
class Dropdown < Phlex::HTML
  def initialize(**attributes)
    @attributes = attributes
  end

  def view_template(&block)
    div(**mix(base_attributes, @attributes), &block)
  end

  private

  def base_attributes
    {
      class: "dropdown",
      data: {
        controller: "dropdown",
        dropdown_open_class: "show"
      }
    }
  end
end

render Dropdown.new(
  class: "dropdown-right",
  data: {
    action: "click->dropdown#toggle",
    dropdown_close_on_click_away_value: true
  }
)

# Result:
# <div class="dropdown dropdown-right"
#      data-controller="dropdown"
#      data-dropdown-open-class="show"
#      data-action="click->dropdown#toggle"
#      data-dropdown-close-on-click-away-value="true">
Use mix liberally when building component libraries. It makes your components much more flexible while maintaining sensible defaults.

Type Compatibility Table

Type 1Type 2Result
StringStringStrings joined with space
StringArrayArray with string prepended
StringSetArray with string prepended
ArrayArrayArrays concatenated
ArraySetArray with set items appended
SetSetSets merged
HashHashHashes recursively merged
AnynilOriginal value preserved
nilAnyNew value used
When mixing incompatible types not in this table, the second value takes precedence.

Build docs developers (and LLMs) love