How To Use Rails Components
To recap, so far we've covered a lot about Rails-Components.com
- What's Rails-Components.com?
- Why Ruby on Rails Needs Components
- Rails Components: More Installer Over Library
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 /
document object. Nope, you can't interact with the
/ 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.
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:
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.
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
The ones that are seemingly missing are the ones for the individual
dropdown_menu_items. They get globbed up into the
dropdown_menu_content block via
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
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.