Component driven development on Rails with Phlex

Bee's assembling an application entirely from components.
Image by Annie Ruygt

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?

Screenshot of a blog post generated by Phlex components

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?”

Fly.io ❤️ Rails

Fly.io is a great way to run your Rails app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Rails app today!

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.

Screenshot of a blog post generated by Phlex components

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 like delete(@post, role: "button") { "Delete Post" } to replace long-winded calls like link_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.