← All Articles

Hotwire, ViewComponents and TailwindCSS: The Ultimate Rails Stack

Khash SajadiKhash Sajadi
Feb 23rd 21Updated Jul 5th 21
Ruby on Rails

Hotwire, ViewComponents and TailwindCSS: The Ultimate Rails Stack

An example Rails app for this post can be found here: https://github.com/cloud66-samples/tickerizer
It is a Stock ticker viewer, built with Rails, Hotwire, ViewComponents and TailwindCSS with a grand total of 7 lines of Java script!

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

Okay! Plenty of buzz words and new tech in the title so I guess I must be talking about another fad in web development. But I really think these three are the perfect match. Like peanut butter and jelly and ..., well some other thing that goes really well with peanut butter and jelly.

First, let me tell you a little bit about each one of these and then why I think they are amazing together.

First, TailwindCSS:

TailwindCSS bills itself as a "Utility First CSS framework". I'm not quite sure what that means, but I really like what it actually is. In a nutshell, Tailwind lets you build nicely styled HTML documents without messing with CSS directly. When you look at Tailwind the first time, it feels like bringing back inline style into your HTML and you'll get a bit of a sick feeling as a result. But once you try it you will realize it is far from that and is actually very good. By moving style in a nicely packaged way into HTML you are avoiding a lot of styling issues that come from the use of central CSS documents. No more !important, no more dilemmas about style groups or dependencies. Things make sense in a single document (the markup) as you read them. Not only Tailwind is much easier to use than building your own entire CSS stack, but it is way easier to understand and work with than frameworks like Bootstrap.

DHH likes Tailwind so much that he built a little gem just so it can be wrapped in a Rails app quickly.

Here is an example, to make a "user card" like this:

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

The big downside? Sprinkling style classes in your markup could lead to inconsistencies across your application and making changes tedious and potentially dangerous.

ViewComponents can take care of that for you!

Next, ViewComponents:

ViewComponents for Rails were initially developed and used by Github and now are gaining more popularity. You can think of them as tightly wrapped partial views with their own little controllers next to them. There is much more they can do with Slots (an experimental feature).  Even in their most basic form, they offer encapsulation for UI components with strongly defined interfaces.  So you can use and reuse them across your projects.

Let's say you want to show a User card, like the one above, many times in your project. This is how it would look like using ViewComponents:

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

And now on your page, you can just use them like this:

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

Nice right?! Well, you might say what's the big deal? I can do this with partials already and that would be true. In our example, we are not doing much in card_component.rb in terms of logic so that would be a fair point. At this level, the main benefit is the named parameter user: as a strongly defined interface that is helpful but not groundbreaking. However, as you add more logic to your component controller card_component.rb(like adjusting the size of the card based on input but with some defaults), things start to get interesting.

One main point here is how ViewComponents not only wrapped the view and the logic behind it but also the styling. When used with Tailwind, ViewComponents help a lot with controlling how style is used across the application by wrapping them in components.

This gets even better when you add Hotwire (or Stimulus to be more precise) to the mix.

The third ingredient, Stimulus:

Stimulus sells itself as a "Modest Javascript framework for the HTML you already have". It is minimalist, opinionated, and does little, but does them well.

Using Stimulus alongside ViewComponents lets you put behavior next to your components. Take the example of a dropdown menu. You need a controller to accept and validate the input (the menu items), a markup file for the UI, and some JS for the behavior. Click on the chevron will open the menu, and clicking on it closes it.

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

In a Rails application, our JS is either application-based (one JS file with multiple namespaces, classes, and functions), which is not a good idea (hard to maintain), page-based or action-based(ie update.js.erb). Mixing ViewComponents with Stimulus makes your JS, component-based.

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

Putting it all together

By mixing TailwindCSS, ViewComponents, and Stimulus, we can create independent visual components with both frontend and backend logic and behavior. As well as constant styling that's deeply integrated with our application. ViewComponent is part of your Rails stack and yet isolated enough to be reused and understandable.

Let's try a simple example, building a dropdown menu:

<!-- app/components/dropdown/component.html.erb -->
<div class="relative inline-block text-left" data-controller="dropdown--component">
  <div>
    <button type="button"
            class="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500"
            data-action="click->dropdown--component#toggleMenu"
            aria-haspopup="true"
            aria-expanded="true">
      <%= @title %>
      <!-- Heroicon name: solid/chevron-down -->
      <svg class="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
      </svg>
    </button>
  </div>
  <div class="hidden origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50"
       data-dropdown--component-target="menu">
    <div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
      <% @items.each do |item| %>
        <%= link_to item.title,
                    item.url,
                    role: "menuitem",
                    method: item.method,
                    class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 #{item.selected ? 'font-semibold' : ''}" %>
      <% end %>
    </div>
  </div>
</div>

As you can see, all three files controlling the markup component.html.erb. The backend behavior component.rb and the frontend behavior component_controller.js are in the same folder, next to each other and completely reusable. Let's see how you can use it now:

hotwire-viewcomponents-and-tailwindcss-the-ultimate-rails-stack

But what about Hotwire?

Hotwire is mostly about sending HTML over the wire down the client (see my other post on that) so we don't need it here but, in many cases, you will benefit from using Hotwire in your components for a simple reason: State management.

Traditionally, all frontend JS frameworks like React or Vue are about sending JSON down the wire and manipulating the DOM based on that. This brings a whole subset of issues with it around managing the state: how to persist and synchronize the state of your frontend with the backend is a major headache for all these components and they have taken plenty of different approaches for solving it.

Hotwire, works around this issue but taking a completely different approach: send full HTML from the server and let Hotwire change the UI based on that. This way you can send the state down the wire as well and not have to deal with it altogether.

I can imagine some of the components you might want to build will need a state which Hotwire can help you with, but that integration is a topic for another post!

Integrating ViewComponets, TailwindCSS and Stimulus into your Rails application

In this section, I will show you how to add those components to your Rails stack so you can build and use these components.

First, let's add TailwindCSS

TailwindCSS uses PostCSS to build the CSS and has Webpack hooks. While you can import it into your application manually following guides like this, you can simply use the tailwindcss-rails gem.

Next Hotwire

Hotwire comes with Stimulus so by adding Hotwire, you will get Stimulus as well and more. If you prefer to only have Stimulus, add it to your stack with the stimulus-rails gem. Otherwise, there is the hotwire-rails gem.

Finally ViewComponents

ViewComponents are the easiest part to add, just use the view_component gem.

One thing to note when adding the ViewComponents gem. Make sure to include it like this in your gemfile, if you want the ERB views to automatically reload during development:

gem "view_component", require: "view_component/engine"

Making Stimulus work with ViewComponents

Apart from adding the gems and following their installation instructions, there is one part you'd need to do manually so you can move your Stimulus controllers next to your ViewComponents:

In your webpacker.yml file add or modify additional_paths to include app/components :

default: &default
  # ...
  additional_paths: ["app/components"]
  # ...

You will also need to add this to your javascript/controllers/index.js

const componentContext = require.context("../../components/", true, /(.*)\/.+\.js$/);
application.load(definitionsFromContext(componentContext));

This will tell Webpacker to look in your components folder for controller JS files. On top of that you'd need to follow naming conventions like this:

Naming Conventions

In your component folder, call your markup component.html.erb and your component ruby file component.rb and make sure your JS file (if you need one) is called component_controller.js

This means creating a folder for each component under the app/components folder and following the normal Rails file naming conventions (app/components/dropdown/component.rb should define DropdownComponent)

Stimulus also has strict naming conventions for its data attributes in the markup. Normally, to connect an element to its controller you'd use the name of the controller like this: data-controller="dropdown, assuming your controller is called dropdown_controller.js. However, when used inside of components you'd need to change this slightly.

For our example above, the controller data attribute would be data-controller="dropdown--component" Similarly, your other Stimulus data attributes will reflect this change:

data-action="click->dropdown--component#toggleMenu"
data-dropdown--component-target="menu"

Note the double - that reflects the ruby module.

Apart from that, you're all good to go now!


Try Cloud 66 for Free, No credit card required