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.
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.
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