Building applications entirely from UI components can be a great way to manage complexity in non-trivial application views, especially when using CSS frameworks like TailwindCSS. It’s a technique that’s been used with great success, by many communities, like JavaScript and Elixir.
What if you could rapidly build Rails applications like this?
class PostsController < ApplicationController
resources :posts, from: :current_user
class Form < ApplicationForm
def template
field :title
field :publish_at
field :content, rows: 6
submit
end
end
class New < ApplicationView
attr_accessor :current_user, :blog, :post
def title = "Create a new post"
def subtitle = show(@blog, :title)
def template(&)
render Form.new(@post)
end
end
end
That emit a HTML user interfaces like this?
Try it yourself by exploring the demo, dig into the source code, and continue reading for a tour of a Rails application built entirely from Phlex components.
Phlex, a pure Ruby framework for building HTML components, and some integration code with Rails, opens up the possibility of building applications in Rails entirely from components.
Sure, there’s Rails scaffolding and other ways of generating code, but generating lots of scaffolding views puts files in your project that all have to be changed when its time to switch from Rails scaffolding markup to whatever gets deployed to production. How good are you at grep?
With Phlex components, iterating on the UI becomes an exercise of extending and refining Ruby classes, so there’s a lot less files to deal with meaning you can ship a higher quality product to production much faster.
A tour of a working Rails application built entirely from Phlex components
First it’s important to understand the basics of Phlex. A basic HTML component has an initializer and a template
method. The temple method is what’s called when the component is rendered. Within the template method you’ll notice blocks that generate HTML tags, in this case <p/>
and <h1/>
.
class BasicComponent < Phlex::HTML
def initialize(name:)
@name = name
end
def template(&)
h1 { "Hello #{@name.capitalize}" }
p { "I hope you're doing incredibly well!" }
end
end
To render it in Rails, we call ActionController#render
.
render BasicComponent.new(name: "Brad")
That’s it for the basics! It might not seem like much, but this simplicity and consistency is what makes it possible to compose complex HTML views in Rails that are easier to reason through than a bunch of templates, partials, and helper methods.
Now buckle up, we’re going to continue our tour with a demo RESTful Blog app that answers the question, “what happens if I build a Rails app entirely out of Phlex components?”
Views in controllers
There’s some magic that I wrote about in Hacking Rails Implicit Rendering for View Components & Fun on how I map class names to Rails action names, but to keep this tour running on-time, just know that the show
action for a resource maps to the Show
class, edit
to Edit
, and so on.
This is what the PostsController
looks like with inline Phlex views:
class PostsController < ApplicationController
resources :posts, from: :current_user
class Form < ApplicationForm
def template
field :title
field :publish_at
field :content, rows: 6
submit
end
end
class Index < ApplicationView
attr_writer :posts, :current_user
def title = "#{@current_user.name}'s Posts"
def template
render TableComponent.new(items: @posts) do |table|
table.column("Title") { show(_1, :title) }
table.column do |column|
# Titles might not always be text, so we need to handle rendering
# Phlex markup within.
column.title do
link_to(user_blogs_path(@current_user)) { "Blogs" }
end
column.item { show(_1.blog, :title) }
end
end
end
end
class Show < ApplicationView
attr_writer :post
def title = @post.title
def subtitle = show(@post.blog, :title)
def template
table do
tbody do
tr do
th { "Publish at" }
td { @post.publish_at&.to_formatted_s(:long) }
end
tr do
th { "Status" }
td { @post.status }
end
tr do
th { "Content" }
td do
article { @post.content }
end
end
end
end
nav do
edit(@post, role: "button")
delete(@post)
end
end
end
class Edit < ApplicationView
attr_writer :post
def title = @post.title
def subtitle = show(@post.blog, :title)
def template
render Form.new(@post)
end
end
private
def destroyed_url
@post.blog
end
end
Why inline views?
I’ve found over the years that I really enjoy prototyping my applications in frameworks like Sinatra where views, controllers, and routing are all closer together; however, when the application grows complex, I find myself wishing I’d built it in Rails.
With inline Phlex views in my controller, I can have the best of both worlds—build out views in the controller, then when it’s time to re-use them somewhere else, I can move the views into their own files.
Extract inline views into view files
For example, I could move the PostsController::Edit
view into its own file at ./app/views/posts/edit.rb
, namespace it to Posts::Edit
,
class Posts::Edit < ApplicationView
attr_writer :post
def title = @post.title
def subtitle = show(@post.blog, :title)
def template
render Form.new(@post)
end
end
Then change the call from the PostsController
to what you’d expect:
class PostsController < ApplicationController
# ...
def edit
render Posts::Edit.new(post: @post)
end
# ...
end
In practice though, I’m finding I don’t really need to do this if I’m heavily refactoring view classes to be highly compentized since the views end up being pretty small.
Can my views co-exist with my existing Erb templates?
Yep! That’s what’s magical about this approach, you can have classes, explicit rendering, and implicit rendering in the same controller.
class PostsController < ApplicationController
# Your fancy new Edit code
class Edit < ApplicationView
attr_writer :post
def title = @post.title
def subtitle = show(@post.blog, :title)
def template
render Form.new(@post)
end
end
# Your existing Erb code
def show
respond_to do |format|
format.html { render "blog/posts/show" }
format.txt { render plain: @post.to_s }
format.json
end
end
That means you could retrofit existing Rails applications with inline class views, meaning if you wanted to convert your application entirely to components, you could do so action-by-action.
Application layout component
Phlex’s application layout emits the same markup as Rails’ default application layout file.
class ApplicationLayout < ApplicationComponent
include Phlex::Rails::Layout
def initialize(title:)
@title = title
end
def template(&)
doctype
html do
head do
title(&@title)
meta name: "viewport", content: "width=device-width,initial-scale=1"
csp_meta_tag
csrf_meta_tags
stylesheet_link_tag "application", data_turbo_track: "reload"
javascript_importmap_tags
end
body(&)
end
end
end
We hook the ApplicationLayout
into the ApplicationView
with the around_template
callback that Phlex gives us to wrap any of our views with layouts.
class ApplicationView < ApplicationComponent
# ...
def title = nil
def subtitle = nil
def around_template(&)
render PageLayout.new(title: proc { title }, subtitle: proc { subtitle }) do
super(&)
end
end
# ...
end
How “slots” work
There’s a few interesting things going on here. You’ll notice the title
and subtitle
methods are set to nil in the ApplicationView
class. You’ll also notice we call these methods from within a proc, and we pass the proc to the PageLayout
class.
The PageLayout
class calls the h1
method and h2
method respectively with the @subtitle
and @title
blocks. These blocks are the conceptual equivalent of custom HTML element “slots”.
class PageLayout < ApplicationLayout
def initialize(title: nil, subtitle: nil)
@title = title
@subtitle = subtitle
end
def template(&)
super do
header(class: "container") do
if @title and @subtitle
hgroup do
h1(&@title)
h2(&@subtitle)
end
else
h1 { @title }
end
end
main(class: "container", &)
end
end
end
The super
method in template
calls ApplicationLayout#template
, which calls the title(&@title)
tag inside head
to set the title of the HTML document.
Let’s put that all together with a New
view that displays a form to create a blog post.
class PostsController < ApplicationController
# ...
class New < ApplicationView
attr_accessor :current_user, :blog, :post
def title = "Create a new post"
def subtitle = show(@blog, :title)
def template(&)
render Form.new(@post)
end
end
# ...
end
When we request /blogs/:id/posts/new
, we see our layout that shows the <title/>
and <h1/>
tags set to @title
and the <h2/>
tag set to @subtitle
.
In the world of custom HTML template, we’d call these blocks “slots”. Slots are how blocks of markup can be passed into layouts, components, etc.
A form component that automatically permits strong parameters
One of the most aggravating experiences for me as a Rails developer is when I build a form, add a field, test it out in my browser and wonder why it’s not persisting to the database. 99% of the time its because I forgot to permit the Action Controller param. Arg!
I’m so lazy that I’ll spend 20 hours to save 10 minutes of work, so I built a Phlex form component that tracks which form fields I’m rendering and passes them to the controller to permit the attributes.
Consider the blog post form with the :title
, :publish_at
, and :content
fields.
class PostsController < ApplicationController
resources :posts, from: :current_user
class Form < ApplicationForm
def template
field :title
field :publish_at
field :content, rows: 6
submit
end
end
# ...
end
If you dig deep enough into the form abstraction, which is still a work in progress, you’ll see code that looks like this:
def input_field(field:, value: nil, type: nil, **attributes)
@fields << field # This tracks the fields that the form uses
input(
name: field_name(field),
type: type,
value: value || model_value(field),
**attributes
)
end
Which gets called from the higher level field
method:
input_field(:title, type: "text")
The magic happens when I append the :title
field name symbol, and all the other field names I call from my form, to the @fields
array.
I have another method in the ApplicationForm
that permits controller’s params.
def permit(params)
params.require(@model.model_name.param_key).permit(*@fields)
end
Finally, from my controller code I put it all together by creating an instance of the form, and passing the controllers params into the permit
method to permit the keys.
class PostsController
# ...
def permitted_params
Views::Posts::Form.new.permit(params)
end
# ...
end
The form view component is capable of figuring out which fields I’ve permitted from the view and eliminates the problem of “forgetting to permit a param that you’ve already entered into your forms”. 🙌
I automated creating form instances via some lightweight meta-programming, which is a work in progress, but it’s easy to see how it’s possible to get back to building Rails applications without worrying about whether or not you forgot to permit a param.
This is just one of many examples Phlex will enable for better Rails forms. Oh, and it will be possible to embed these into Erb files too in case you want to keep using Erb.
<h1>Edit Post</h1>
<%= render Views::Posts::Form.new(@post) %%>
Table component
HTML tables are always an interesting exercise in building components. Here’s what a basic table component looks like at the time of this writing.
class PostsController < ApplicationController
resources :posts, from: :current_user
class Index < ApplicationView
attr_writer :posts, :current_user
def title = "#{@current_user.name}'s Posts"
def template
render TableComponent.new(items: @posts) do |table|
table.column("Title") { show(_1, :title) }
table.column do |column|
# Titles might not always be text, so we need to handle rendering
# Phlex markup within.
column.title do
link_to(user_blogs_path(@current_user)) { "Blogs" }
end
column.item { show(_1.blog, :title) }
end
end
end
end
end
I want this component to accept a collection of objects, in this case an Active Record association, that I can loop through to emit table rows.
TableComponent.new(items: @posts)
Then the component configures columns that can accept text for the column title and a link to the blog post. The _1
is a short cut for getting the first item, in our case a Post
instance, and the show
method renders a link to the post that uses Post.title
for the link text.
table.column("Title") { show(_1, :title) }
Or for a more complex use case, we could include a link in the column.title
block so the user can get back to their blogs and display a link to the post.title
in each column.item
.
table.column do |column|
# Column title is markup, so we have to pass Phlex into
# the column.title to generate the link.
column.title do
link_to(user_blogs_path(@current_user)) { "Blogs" }
end
# Then we link to each posts blog and print the title.
column.item { show(_1.blog, :title) }
end
What’s remarkable about Phlex is how easy it is to mix helpers and markup together, essentially building your own markup language unique to your app that emits HTML in the browser.
Why build an application from components?
There’s so many reasons, and there’s a lot of lists that use buzzwords like TDD, composability, collaboration. I don’t really like buzzwords, so lets try to break down the benefits in terms of how it will help you.
Components are easier to change
As your application UI becomes more complex, it becomes harder to change, especially if there’s a lot of HTML that’s been copy and pasted all over templates, partials, and view helpers.
Rails partials can make this slightly more manageable for simple chunks of UI, but it starts to get complicated when you need partials to deal with stateful UI components like tabs, filtered lists, or layouts.
Components keep all of this complexity in one place, making it easier to reason through when it needs to be changed.
Direct access to view state makes it more palatable to build sophisticated UIs
Complex views usually have state unique to the view that is helpful to manage. For forms we can track what fields the view is displaying so we can automatically permit its params. For tables we could manage the order its sorted. For a bulk selector we might need to show checkboxes next to selected items. Its awkward managing this state in an Erb file, but it feels natural when this data is part of the view component itself.
Components can be directly tested
Have you ever tried to write tests that test one Rails partial? Probably not, because there’s no way to directly pass them different variables. The only way of testing partials is to call them from a Rails view test. You could make a pretty good argument that its acceptable to implicitly test a component through a view, but if you’re trying to build a component library that’s an unsatisfactory answer.
Components can be shared with other people inside your team or to a wider community
Medium size teams usually have engineers and designers working together to ship product—its helpful when they can collaborate together on a component together that’s part of the design system, then implement it throughout the Rails application.
If you’ve ever wondered why it’s so awkward to work with Rails plugins that generate UI in your app, it’s because they usually generate Erb templates with markup that doesn’t match your style of markup. If we can get to a world of standard UI components, we might get to a place where Rails plugin UIs work better out of the box.
Why not build an application from components?
First, this approach isn’t for everybody—some folks loath abstractions and want to write markup. If that’s you then keep writing Erb templates, partials, and layouts in Rails.
There’s also certain applications where component-base UI’s don’t make a ton of sense, like websites that are primarily managing unstructured content. Heck even I prefer using Markdown and Erb files for content-heavy sites with Sitepress.
Building an HTML UI entirely from Ruby Phlex components seems like a terrible idea initially, much like how the concept of building an application from TailwindCSS components initially seems like a terrible idea. It’s worth pushing past this initial instinct before making final judgement because it could end up feeling great.
More components
I could write several articles about all sorts of different components that Phlex promises to make a little easier to implement. Maybe I’ll write about them in the future, but for now I’ll keep it as a list.
- Tables - Tables could include helpers that make columns sortable in ascending or descending order, filters, lazy loading, and even syncing to an Turbo stream.
- RESTful link helpers - Rails
link_to
helpers can get awkward, so I created RESTful link helpers likedelete(@post, role: "button") { "Delete Post" }
to replace long-winded calls likelink_to "Delete Post", @post, role: "button", data: { "turbo-method": :delete }
. - Bulk object selection - Display checkboxes next to each item in a collection of Active Record objects that your user can select and do something with—Phlex components and thoughtful use of Action classes could make bulk selection a breeze.
- Navigation - Display a sidebar or top menu navigation bar that shows a page is “active” if a person is on that part of the website. Describe the structure of your menu once and designate what you want to display on a mobile drop-down vs a desktop nav bar.
- Async query rendering - **** Components can be built that manage the loading behavior of a page, like adding a turbo frame to a Phlex component that is populated when an Active Record
#loan_async
query completes. - Hotwire integration - Build out a view framework that automatically keeps in sync with the server, similar to how Elixir LiveView works.
The best part? These could be built, packaged up into gems, and shared to the wider community. When a community of developers build up a standard library of view components, more powerful abstractions can be built on top of them making everybody even more productive.