Evil Front Part 2: Modern Frontend in Rails

Cover for Evil Front Part 2: Modern Frontend in Rails

Translations

If you’re interested in translating or adapting this post, please contact us first.

An opinionated guide to modern, modular, component-based approach to handling your presentation logic in Rails that does not depend on any frontend framework. Follow this three-part tutorial to learn the bare minimum of up-to-date frontend techniques by example and finally make sense of it all.

New! This article was updated in July 2019 to follow the latest developments in frontend and support most recent versions of Rails, Webpacker, and other libraries.

Previously, on Part 1…

By the end of the much-discussed Part 1 (read it here), we have managed to rewire our standard Rails application to cater for modern frontend practices. We are using Webpacker gem to build our assets with Webpack while PostCSS and postcss-preset-env are processing our styles. Babel, Autoprefixer, and Browserslist allow us not worry about cross-browser issues. Our code is automatically checked for syntax errors with Prettier, AirBnB Base Config, ESLint, and stylelint on each git commit.

We have a comprehensible folder structure that allows us to think in components and we are not tied to any particular frontend framework like React—we are still dealing with good old .erb partials. In development, we launch our server with hivemind (you can get it here) or foreman instead of usual rails s.

Our app is still lacking though, all we display for now is a “Hello world” message. Time to build a real thing. If you are following the tutorial and building an application along with us, brace yourself for a lot of cutting and pasting (also feel free to retype example code and modify it to your liking). But first, make sure you have completed Part 1.

Getting real

As a quick reminder, this is how we render our components:

<!-- app/views/pages/home.html.erb -->
<%= render "components/page/page" do %>
  <p>Hello from our first component!</p>
<% end %>

We can make our life a bit easier by introducing a helper that will allow us to render our components like so:

<%= c("page") do %>
  <%= c("auth-form") %>
<% end %>

That way, we can only mention our component’s name instead of typing the whole path. Our helper will also handle a case when we happen to have two partials in the same folder with a slightly different functionality (for instance _message-form.html.erb and _message-form_admin.html.erb). As a convention, we will use underscores to tell these “alternative” partials from each other.

Go to your application_helper.rb and add a method:

module ApplicationHelper
  def component(component_name, locals = {}, &block)
    name = component_name.split("_").first
    render("components/#{name}/#{component_name}", locals, &block)
  end

  alias c component
end

Now it’s time to think about our controllers. Currently, we have a single pages_controller.rb, the one we needed for a smoke test. You can safely get rid of it (and corresponding app/views/pages folder). Our chat application will have two controllers: an AuthController to handle authentication and a ChatController responsible for our chat window. We can generate them both:

$ bin/rails g controller auth
$ bin/rails g controller chat

Also, change your routes.rb:

Rails.application.routes.draw do
  root "chat#show"

  get "/login",  to: "auth#new"
  post "/login", to: "auth#create"
end

Let’s start with our “authentication” page:

# app/controllers/auth_controller.rb
class AuthController < ApplicationController
  before_action :only_for_anonymous # check if we know the user

  def new; end

  # Get username from params, save to session and redirect to chat window
  def create
    session[:username] = params[:username]
    redirect_to root_path
  end

  private

  # If a user had been to our chat before, send them straight to chat window
  def only_for_anonymous
    redirect_to root_path if session[:username]
  end
end

For the sake of example, our actions are fairly simple. A first-time user will be prompted a username, and we will store it in the session hash. A returning user will skip the authentication page. We only need one view, for our new action, so let’s create one. By design, our view templates should only contain render calls to component partials. Here we are embedding an auth component inside the page component that was created at the end of Part 1.

$ touch app/views/auth/new.html.erb
<!-- app/views/auth/new.html.erb -->
<%= c("page") do %>
  <%= c("auth-form") %>
<% end %>

Now, let’s create a component for our authentication form. We will name it more explicitly: auth-form.

$ mkdir -p frontend/components/auth-form
$ touch frontend/components/auth-form/{auth-form.pcss,auth-form.js,_auth-form.html.erb}

You will run these two commands pretty much every time you create a new component. Let’s start with the .erb partial. Here we are building a basic form, using standard Rails helpers.

<!-- frontend/components/auth-form/_auth-form.html.erb -->
<div class="auth-form">
  <%= form_tag login_path, method: :post do %>
   <%= text_field_tag :username, "", class: "auth-form--input", placeholder: "Choose your username...", autofocus: true, required: true %>
   <%= submit_tag "Identify me", class: "auth-form--submit" %>
  <% end %>
</div>

It also makes sense to agree on CSS naming from the very start.

By choosing a clear methodology, we can avoid nasty collisions in the common namespace and make our code self-documenting.

We are going to borrow from BEM’s handbook by adopting a “block/element” approach (where “block” is our component, and “element” is some logical part of it). We will choose this syntax convention: component-name--element-name. That way, text field and submit button need the following classes: auth-form--input (component auth-form, element input) and auth-form--submit (component auth-form, element submit). “M” in BEM stands for “modifier”, but we will not use modifiers in our simple example.

Of course, you are free to stick to any CSS naming you are used to, just make sure it is consistent across your components.

So, we have prepared the ground for our styles, but we have not added them yet. At this point, our authentication page looks like this (go to localhost:5000/login):

Bare authentication page

Bare authentication page

Let’s also take a second to enable postcss-nested plugin that will allow us to nest CSS classes. Type yarn add postcss-nested in the Terminal and add this line to the top of plugins section inside .postcssrc.yml: postcss-nested: {}.

Time to add some styles! Remember that they are pulled into Webpack by way of JavaScript, so we need to always import our component’s stylesheet file into its javascript file. We also need to “register” component’s JS inside our application.js entry point.

// frontend/packs/application.js
import "init";
import "components/page/page";
import "components/auth-form/auth-form";
// frontend/components/auth-form/auth-form.js
import "./auth-form.pcss";
/* frontend/components/auth-form/auth-form.pcss */
.auth-form {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;

  &--input {
    width: 100%;
    padding: 12px 0;
    border: 1px solid rgba(0, 0, 0, 0.1);
    font-size: 18px;
    text-align: center;
    outline: none;
    transition: border-color 150ms;
    box-sizing: border-box;

    &:hover,
    &:focus {
      border: 1px solid #3f94f9;
    }
  }

  &--submit {
    width: 100%;
    margin-top: 6px;
    padding: 12px 0;
    background: #3f94f9;
    border: 1px solid #3f94f9;
    color: white;
    font-size: 18px;
    outline: none;
    transition: opacity 150ms;
    cursor: pointer;

    &:hover,
    &:focus {
      opacity: 0.7;
    }
  }
}

You can see that our component-name--element-name convention makes it easy to write nested PostCSS with the help of the ampersand. & is simply replaced by a “parent” class name when PostCSS is processed into plain CSS, so .auth-form { &--input } becomes two separate classes: .auth-form and .auth-form--input. In our code, though, anything that has to do with the auth-form component is contained inside the auth-form class scope, so you don’t have to worry about class names clashing. The rule of thumb here is to have your parent CSS class to be named exactly as a component and its folder inside the project—not respecting that can lead to a spaghetti code in no time.

Now if you go back to your browser window (provided your server was already running), you will see that our login page has got some style: webpack-dev-server noticed changes in JS file and had refreshed the page in the background.

Styled authentication page

Styled authentication page

See how easy it has become to tweak CSS to your liking? If we need to change a button’s color, we can just open a browser and a code editor side by side, and your browser will reflect changes immediately—on each file save. That speeds up working with styles a lot.

Note: If you happened to submit this form and can not reopen the auth page due to controller logic (once the username is in session you can not go back), clear your browser’s cookies.

Don’t shoot the messenger

Our authentication page needs to lead somewhere, but all we have for now is some routes and an empty ChatController. We are going to be dealing with messages, so we need a basic Message model. Let’s create one:

$ bin/rails g model message author:string text:text
$ bin/rails db:create
$ bin/rails db:migrate

Our messages will be created from ActionCable, so our controller just needs a way to display them. We will display last 20 messages on initial page load.

# app/controllers/chat_controller.rb
class ChatController < ApplicationController
  before_action :authenticate!

  # dispay last 20 messages
  def show
    @messages = Message.order(created_at: :asc).last(20)
  end

  private

  # redirect user to /login if he hadn't picked a username yet
  def authenticate!
    redirect_to login_path unless session[:username]
  end
end

Again, we need only one view, this time it’s show.html.erb:

$ touch app/views/chat/show.html.erb
<!-- app/views/chat/show.html.erb -->
<%= c("page") do %>
  <%= c("chat", messages: @messages) %>
<% end %>

As our components are just plain ERB partials rendered with a helper that uses a render method, we pass our locals the usual way. You already know the drill to create yet another component:

$ mkdir -p frontend/components/chat
$ touch frontend/components/chat/{chat.pcss,chat.js,_chat.html.erb}

Here, we are going to see a deeper component nesting. Our chat component is a way to refer to the contents of the page as a whole. Our page will have a dynamically updating list of messages and a form to submit a new message, so that can be broken into two components: messages and message-form. And where there are messages, there’s a message, so we need a message component too! More Terminal:

$ mkdir -p frontend/components/message
$ touch frontend/components/message/{message.pcss,message.js,_message.html.erb}

$ mkdir -p frontend/components/messages
$ touch frontend/components/messages/{messages.pcss,messages.js,_messages.html.erb}

$ mkdir -p frontend/components/message-form
$ touch frontend/components/message-form/{message-form.pcss,message-form.js,_message-form.html.erb}

The final structure after you have created all folders and files should look like this:

frontend/components
   ├── auth-form
   │   ├── _auth-form.html.erb
   │   ├── auth-form.pcss
   │   └── auth-form.js
   ├── chat
   │   ├── _chat.html.erb
   │   ├── chat.pcss
   │   └── chat.js
   ├── message
   │   ├── _message.html.erb
   │   ├── message.pcss
   │   └── message.js
   ├── message-form
   │   ├── _message-form.html.erb
   │   ├── message-form.pcss
   │   └── message-form.js
   ├── messages
   │   ├── _messages.html.erb
   │   ├── messages.pcss
   │   └── messages.js
   └── page
       ├── _page.html.erb
       ├── page.pcss
       └── page.js

We are going to start filling in the blanks with our parent component: chat.

<!-- frontend/components/chat/_chat.html.erb -->
<div class="chat">
  <div class="chat--messages">
    <%= c("messages", messages: messages) %>
  </div>
  <div class="chat--form">
    <%= c("message-form") %>
  </div>
</div>

You can see now that our component will render its sub-components, but we don’t want to put all of them in our entry point individually, this may quickly get out of hand. We’ll adopt a new rule of thumb: if a component has any children, children should be imported in a component’s .js file. This way, in our application.js we only register components located at the top of the hierarchy. Let’s do it right away, so we don’t forget later:

// Our updated frontend/packs/application.js
import "init";
import "components/page/page";
import "components/auth-form/auth-form";
import "components/chat/chat";

Now we import JS files for all components nested inside chat in the chat.js:

// frontend/components/chat/chat.js
import "components/messages/messages";
import "components/message-form/message-form";
import "./chat.pcss";

And, finally, for CSS:

/* frontend/components/chat/chat.pcss */
.chat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  height: 100%;
  overflow: hidden;

  &--messages {
    width: 100%;
    flex: 1 0 0;
  }

  &--form {
    width: 100%;
    background: white;
    flex: 0 0 50px;
  }
}

One component done, three more to go!

For our message-form:

<!-- frontend/components/message-form/_message-form.html.erb -->
<div class="message-form js-message-form">
  <textarea class="message-form--input js-message-form--input" autofocus></textarea>
  <button class="message-form--submit js-message-form--submit">Send</button>
</div>

Note that we are not using a <form> tag here, as we are going to submit the contents of our <textarea> with JavaScript to make use of ActionCable.

Perhaps you are wondering why we repeat our class names twice: message-form and js-message-form. This convention ensures that if someone gets carried away while redesigning and chooses to change a class name, your JS selectors will not be affected. Thus you have two parallel ways to name things: one for CSS and one for JavaScript. You are not required to adopt this practice in your code; it is perfectly fine just to use a single selector. But you have to be extra-vigilant and modify your JS for DOM-manipulation every time you modify your CSS class names, so a redesign does not lead to broken logic.

// frontend/components/message-form/message-form.js
import "./message-form.pcss";
/* frontend/components/message-form/message-form.pcss */
.message-form {
  display: flex;
  width: 100%;
  height: 100%;

  &--input {
    flex: 1 1 auto;
    padding: 12px;
    border: 1px solid rgba(0, 0, 0, 0.1);
    font-size: 18px;
    outline: none;
    transition: border-color 150ms;
    box-sizing: border-box;
    resize: none;

    &:hover,
    &:focus {
      border: 1px solid #3f94f9;
    }
  }

  &--submit {
    flex: 0 1 auto;
    height: 100%;
    padding: 12px 48px;
    background: #3f94f9;
    border: 1px solid #3f94f9;
    color: white;
    font-size: 18px;
    outline: none;
    transition: opacity 150ms;
    cursor: pointer;

    &:hover,
    &:focus {
      opacity: 0.7;
    }

    &:active {
      transform: translateY(2px);
    }
  }
}

Note that at any point of this process we can go to localhost:5000 and log in: our chat window will be displayed. Just make sure to comment out c render calls for components that are not ready yet.

Moving on. We have a parent component and a form; now we need a place to display our messages and a template for each message. We follow the same pattern: ERB, then JS, then CSS.

<!-- frontend/components/messages/_messages.html.erb -->
<div class="messages js-messages">
  <div class="messages--content js-messages--content">
    <% messages.each do |message| %>
      <%= c("message", message: message) %>
    <% end %>
  </div>
</div>
// frontend/components/messages/messages.js
import "components/message/message"; // message is nested, so we import it here
import "./messages.pcss";
/* frontend/components/messages/messages.pcss */
.messages {
  position: relative;
  width: 100%;
  height: 100%;
  background: white;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-bottom: 0;
  box-sizing: border-box;

  &--content {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow-x: hidden;
    overflow-y: auto;
  }
}

And, finally, for the individual message:

<!-- frontend/components/message/_message.html.erb -->
<div class="message">
  <div class="message--header">
    <span class="message--author">
      <%= message.author %>
    </span>
    <span class="message--time">
      <% if message.created_at > Time.now - 24.hours %>
        <%= l(message.created_at, format: :short) %>
      <% else %>
        <%= l(message.created_at, format: :long) %>
      <% end %>
    </span>
  </div>
  <div class="message--text">
    <% message.text.lines.each do |line| %>
      <p><%= line %></p>
    <% end %>
  </div>
</div>
// frontend/components/message/message.js
import "./message.pcss";
/* frontend/components/message/message.pcss */
.message {
  margin: 12px 6px;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }

  &--author {
    font-weight: bold;
  }

  &--time {
    color: rgba(0, 0, 0, 0.5);
    font-size: 12px;
  }

  &--text p {
    margin: 0;
  }
}

Time to test that all went well. We are not yet able to create our messages from the form, but we can create some Message instances from rails console and see that if they are in fact displayed correctly:

# In rails console...
> Message.create(author: "Evil Martian", text: "Surrender!")

Now make sure you run your server and refresh the browser. That is what you should see after following all the steps above:

A chat window

A chat window

One more thing…

If you are tired of creating components’ folders and files by hand, here is a simple Rails generator you can use and adapt to your needs. Create a folder named generators inside your lib and put a file named component_generator.rb inside:

$ mkdir lib/generators
$ touch lib/generators/component_generator.rb
# lib/generators/component_generator.rb
class ComponentGenerator < Rails::Generators::Base
  argument :name, required: true, desc: "Component name, e.g: button"

  def create_view_file
    create_file "#{component_path}/_#{component_name}.html.erb"
  end

  def create_css_file
    create_file "#{component_path}/#{component_name}.pcss"
  end

  def create_js_file
    create_file "#{component_path}/#{component_name}.js" do
      # require component's CSS inside JS automatically
      "import \"./#{component_name}.pcss\";\n"
    end
  end

  protected

  def component_name
    @component_name ||= name.underscore.dasherize
  end

  def component_path
    "frontend/components/#{component_name}"
  end
end

Now you can generate your components from the command line:

$ bin/rails g component NAME

Congratulations! You have completed Part 2 of the tutorial. Check your code against this this branch of the official project repository if you had any trouble. Thank you for reading and jump to Part 3 where we will make our application interactive with ActionCable, put some finishing touches and happily deploy it to Heroku. We will also discuss what can go wrong in a “sprocketless” Rails application.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.

In the same orbit

How can we help you?

Martians at a glance
17
years in business

We transform growth-stage startups into unicorns, build developer tools, and create open source products.

If you prefer email, write to us at surrender@evilmartians.com