How Rails Components Work

How To Use Rails Components

To recap, so far we've covered a lot about Rails-Components.com

It's time to show you how to use Rails Components and the underlying simplicity of the implementation.

Stay Close to the Framework

Indirection is at the heart of complexity when it comes to tools and frameworks.

/ Begin Diatribe /

I think one of the most prevelant cases of this in web development is React. In the end, React is building web applications, composed of HTML, CSS, and Javascript. Yet, the HTML you're writing in React isn't HTML, it's JSX. You can't actually write HTML, there's no mechanism to drop down to interact with the medium upon which you build. Try using attaching an event listener to the ready event of the document object. Nope, you can't interact with the document's native lifecycle. Look, I'm not saying this is bad, I'm just saying it's indirect and complex. It is complex to understand how React, given this indirection, is actually building you a web application composed of HTML, CSS, and Javascript when you are so rarely actually interacting with those tools. The rub is that it's entirely possible to know how to build a React application and have no idea how to build a web application. Given how absolutely beautiful I think the native web medium is, I think that indirection is tragic.

/ End Diatribe /

But coming back to Rails Components and how to use them and how they work, my general philosophy in design was to stay as close to Rails as possible. What this means is using as few dependencies as possible and using the framework's tools and conventions.

To answer a question I've gotten a lot, this philosophy is why I did not implement any of these components in the wonderful [ViewComponent or Phlex libraries.

General Architecture

I'm mostly using the built in ActionView helpers, ERB, HTML, and wrapping all of that in helper methods. A component tends to look like:

app/helpers/components/<component>_helper.rb
app/views/components/_<component>.html.erb

Pretty simple. The next convention is that there is a single helper method responsible for the end result of the markup. Those helper methods are always named render_<component>. Why? Because it is declarative, it's literally what the method does. Plus, you could get some conflicts if you named something <%= button %> or <%= input %>. I felt that the method prefix added a nice sort of prefix to the concept. It also means that you can say render_input across different Rails Component libraries. That is to say, the API stays consistent as the underlying theme can change.

Slots, capture(), content_for and yield

It turns out a lot of rubyists figured out the power of this pattern in making structured components without much need for external libraries. I tend to see that confluence as both the elegance of Ruby and Rails and the correctness of the approach. Basically, nice_partials and Chris at GoRails (worth subscribing too but not just for this video but because the entire library is awesome) discovered the almost identical manner in which I implemented the components.

The point of the component is to abstract away the underlying markup or div soup that goes into making complex interfaces. You have the divs, the nesting, the classes and html attributes, it's a lot of code to look at that is either devoid of logic (it should be at least), or is a really write once and forget it kind of situation.

However, because of the nesting and the requirement to position content in certain areas of the markup's tree, you need to a way of saying "this content goes here" and "that title goes there." Plus you need to be able to say "this is a piece of content composed of this other complex piece of markup content." Take a Dropdown Menu for example:

<% render_dropdown_menu do %>
  <%= dropdown_menu_trigger do %>
  <% end %>

  <%= dropdown_menu_content do %>
    <%= dropdown_menu_label %>

    <%= dropdown_menu_item %>
    <%= dropdown_menu_item do %>
    <% end %>
    <%= dropdown_menu_item %>
  <% end %>
<% end %>

That is concealing a ton of markup. Well let me just show you. Okay, I was going to but it's absurd, just take my word for it. And parts have to go in different parts. Luckily, Rails gives us a powerful way to break partials into content areas using content_for.

This is the menu's underlying partial:

<%= render_popover do %>
  <%= popover_trigger do %>
    <%= content_for(:dropdown_menu_trigger) %>
  <% end %>
  <%= popover_content class: "p-1 w-56" do %>
    <% if content_for?(:dropdown_menu_label) %><div class="px-2 py-1.5 text-sm font-semibold"><%= content_for(:dropdown_menu_label) %></div><% end %>
    <%= content_for(:dropdown_menu_content) %>
  <% end %>
<% end %>

What's cool is that the dropdown menu is itself just composed of a popover. But you can see how it partitions it into discrete content_for blocks.

The ones that are seemingly missing are the ones for the individual dropdown_menu_items. They get globbed up into the content_for the dropdown_menu_content block via capture.

  def dropdown_menu_item(label = nil, **options, &block)
    content = (label || capture(&block))
    render "components/ui/shared/menu_item", content: content
  end

Each menu item is basically independently rendered in it's own context and returned to the area of the dropdown_menu_content. It's as if the menu items were hardcoded as HTML inside that block by the time the dropdown_menu_content is rendered.

Now you can see the entire architecture of the component's helper.

module Components::DropdownMenuHelper
  def render_dropdown_menu(**options, &block)
    content = capture(&block) if block
    render "components/ui/dropdown_menu", content: content, **options
  end

  def dropdown_menu_trigger(&block)
    content_for :dropdown_menu_trigger, capture(&block), flush: true
  end

  def dropdown_menu_label(label = nil, &block)
    content_for :dropdown_menu_label, (label || capture(&block)), flush: true
  end

  def dropdown_menu_content(&block)
    content_for :dropdown_menu_content, capture(&block), flush: true
  end

  def dropdown_menu_item(label = nil, **options, &block)
    content = (label || capture(&block))
    render "components/ui/shared/menu_item", content: content
  end
end

Each slot is essentially a combination of content_for and capture allowing you to create a complex markup structure in very neat ruby without any serious magic.

More importantly, between the simplicity of the partial's markup and the helper's ruby, I believe these components are easy to maintain and take ownership of. The code is readable and declarative allowing you to safely make edits because you understand, especially after reading this, how they work.

Did you find this article valuable?

Support code.avi.nyc by becoming a sponsor. Any amount is appreciated!