A Tale of two Phlexes

There are two types of components you can write in Phlex. The standard component exposes a “builder” style API. The second type is a component that uses DeferredRender, which fully consumes the render block before its own template is rendered.

Let’s take a look at both types by building two different Table components.

The first component will use a builder style API.

class Table < Phlex::HTML
  def template
    table do
      yield
    end
  end

  def head
    thead do
      tr do
        yield
      end
    end
  end

  def column_heading
    th { yield }
  end

  def body
    tbody { yield }
  end

  def row
    tr { yield }
  end

  def cell
    td { yield }
  end
end

And we can imagine how we might use it:

render Table.new do |t|
  t.head do
    t.column_heading { "Name" }
    t.column_heading { "Age" }
  end

  t.body do
    t.row do
      t.cell { "Alice" }
      t.cell { "28" }
    end

    t.row do
      t.cell { "Bob" }
      t.cell { "32" }
    end
  end
end

In this simplistic example of a table, it may seem like the component isn’t doing a lot for us. But imagine if the component was applying predefined classes to each row, and cell, and column heading. It could considerably clean up our invocations of the table by abstracting away the specific DOM details of the table. It does not abstract away the structure of the table however.

Now lets take a look at a component that uses DeferredRender.

class Table < Phlex::HTML
  include Phlex::DeferredRender

  def initialize(rows:)
    @data = rows
    @columns = []
  end

  def template
    table do
      thead do
        tr do
          @columns.each do |column|
            th { column[:heading] }
          end
        end
      end

      tbody do
        @data.each do |row|
          tr do
            @columns.each do |column|
              td { column[:content][row] }
            end
          end
        end
      end
    end
  end

  def column(heading, &block)
    @columns << { heading: heading, content: block }
  end
end

We would use this component differently:

render Table.new(rows: users) do |t|
  t.column("Name") { |user| user.name }
  t.column("Age") { |user| user.age }
end

This component not only abstracts away the details of the DOM, but also the structure of the final table. With this component, the renderer of the Table has no say in how the final markup is put together, just the content.

Both of these styles have their utility. The builder style component retains the most flexibility for the renderer at the cost of some verbosity. The DeferredRender style component is able to provide a terser API but can’t offer as much in the way of flexibility.

I have found a pattern where I will actually have both types of components for the same “thing”. An example from my own application is that I have a Table component that uses the builder style API, and I have a DataTable component that uses DeferredRender. DataTable uses Table internally. For most usecases I can use DataTable to render out my tabular data, but if I need a table that is more complex, maybe it needs to have sortable columns, or selectable rows, I can fall back to Table and maintain the styling of the table.

I think of the two styles existing on a spectrum between imperative and declarative. The builder style API is more imperative, and the DeferredRender style is more declarative.

To answer the question of “Why isn’t the DeferredRender style the default?”, I think it’s because the builder style is more intuitive, and makes your components work the way normal HTML elements work inside of Phlex.

In the block of a DeferredRender component’s render call, all you have available to you is the component’s own API. Any additional markup or content you try to add outside of it’s API will be ignored.

render Table.new(rows: users) do |t|
  t.column("Name") { |user| user.name }
  t.column("Age") { |user| user.age }

  h1 { "This will not be rendered" }
end

When to use one over the other #

As a general rule of thumb, I try to use a builder style API as much as I can, because it affords me the most flexibility with the component as I continue using it. I very often don’t know all the different ways I will want to use my component when I am first creating it.

When I am using a builder style component in many places and all the invocations are all very similar, I will try extracting out a more declarative terser component that will usually need to use DeferredRender.

A case where DeferredRender is my first choice is when I’m building a component that needs to repeat the provided “structure” the renderer will give us. Take this PageHeader component for example.

TailwindUI's Page Header

It has a set of actions that are rendered as buttons when the screen is large enough. But as the viewport shrinks the actions get tucked into a dropdown menu. The final markup will need to include these actions multiple times: once as buttons, and again as menu items within the dropdown.

dropdown.png

Using a DeferredRender component, we can abstract away the details of the DOM, and the structure of the final markup, and only take concern with the content.

class PageHeader
  include Phlex::DeferredRender

  def initialize
    @actions = []
  end

  def template
    div do
      # ...

      div(class: "action-buttons hidden lg:block") do
        @actions.each do |action|
          render Button.new(**action[:attributes]) do
            render action[:content]
          end
        end
      end

      div(class: "dropdown-menu lg:hidden") do
        @actions.each do |action|
          a(**action[:attributes]) { render action[:content] }
        end
      end
    end
  end

  def action(**attributes, &content)
    @actions << { attributes: attributes, content: content }
  end
end

And then (ignoring the other parts of the page header for simplicity here), we can use that page header component like this:

render PageHeader.new do |h|
  h.action(href: edit_listing_path(@listing)) { "Edit" }
  h.action(href: listing_path(@listing)) { "View" }
end
 
35
Kudos
 
35
Kudos

Now read this

An Exploration of Bitcoin Transactions, the Blockchain, and Miners

What will follow is a walk-through of bitcoin’s circular lifecycle as it gets transacted from person to person. Because of the circular nature, there’s not an ideal starting point, because you must always know what came before to truly... Continue →