Modular front-end code in Rails

23-08-2012
Tagged ruby, rails, css, sass, programming, web development

Maintainable stylesheets of non-trivial size need a modular design — so much has been established by now. How can you incorporate this style of front-end coding in a typical Rails application?

Modular CSS

With ‘Modular CSS’ I mean a range of new ideas in front-end web development aiming to solve some of the issues with giant catch-all .css-files, rife with duplication and specificity conflicts. These approaches typically revolve around defining “objects”, “modules” or “components” in your stylesheets, in order to reduce the cost of change by:

  • increasing style re-use and, by extension, consistency;
  • creating a shared language among team members;
  • decoupling markup from styles.

To learn more about these ideas, check out Nicole Sullivan’s Object-Oriented CSS, Jonathan Snook’s SMACSS, Yandex’ BEM and keep an eye out for Roy Tomeij’s book Modular Frontend.

Modular front-end code in Rails apps

Historically, Rails has “forced” many best practices on developers by providing conventions for trivial matters (see Yehuda Katz’s RailsConf 2012 talk for examples and background). But to properly modularize our front-end code, we have to fight Rails a little bit and rid ourselves of the “one controller, one stylesheet, one script” default file layout. At the same time, Rails does provide us with some nice tools to make our jobs easier: the asset pipeline, Sass included by default, and RecordTagHelper.

Note: in this post I’m ignoring the Javascript aspect of front-end code on purpose. That’s a story for another time.

Marking up objects

The modularized front-end revolves around identifying the building blocks of your web pages, like a post, a footer or a horizontal list. Rails’ content_tag_for is a great help to mark these objects up:

<%= content_tag_for(:article, @posts) do |post| %>
  <h2><%= post.title %></h2>
  <div class="content">
    <%= markdown post.body %>
  </div>
<% end %>

This might generate the following output:

<article id="post_1" class="post">
  <h2>My example post</h2>
  <div class="content">
    <p>Foo bar</p>
  </div>
</article>
<article id="post_2" class="post">
  <h2>Another example post</h2>
  <div class="content">
    <p>baz qux</p>
  </div>
</article>

Rails gives us a convention for naming and identifying objects. You would typically provide styles for the .post class, and attach javascript behaviour to individual posts using the #post_1 and #post_2 IDs.

Note: There is also a div_for tag, which does basically the same as content_tag_for but omits the first argument and always gives you a div. For more information, see the documentation on ActionView::Helpers::RecordTagHelper.

If Haml is more your cup of tea, you could write:

%div[post]
  %h2= post.title

If you are not dealing with instances of ActiveRecord::Base, you can still use these helpers by defining some extra methods on your objects, as these classes and IDs are generated as "#{object.param_key}_#{object.to_key}". Take a look at ActiveModel::Naming and ActionController::RecordIdentifier for details.

Object inheritence

You will usually find yourself needing to go one step further and apply object inheritence: a post can take many different forms on your site, ranging from full articles, excerpts in an index or links in a sidebar. Rails can still help us:

<%- content_tag_for(:article, @posts, :excerpt) do |post| %>
  ...
<% end %>

Will give us:

<article id="excerpt_post_123" class="excerpt_post">
  ...
</article>

Although I would have preferred dashes insteads of underscores, Rails still makes a trivial decision on how to name stuff for us, so let’s stick with it.

Partials, paths and STI

It is a natural fit to use a partial for every object you define. So, to render blog post excerpts on your front page, you will end up with a posts/_excerpt_post partial and a modules/posts/excerpt_post.css.scss stylesheet. It is now trivial to render an excerpt post object anywere in your application:

<%= render partial: 'posts/excerpt_post', collection: @posts %>

Should you create custom Ruby objects — such as decorated models or presenters that group multiple persistence objects into a single business object — you may want to define to_partial_path on it to tell Rails how to render them:

require 'delegate'
class ArticlePresenter < DelegateClass(Post)
  def to_partial_path
    'posts/article_post'
  end
end

Which would allow you to render an instance of ArticlePresenter like so:

<%= render article_presenter %>

…and have it use the posts/_article_post.html.erb template.

Finally, Rails’ Single Table Inheritance can get in your way. Consider you have set up your models so that both Post and Page inherit from a generic Node model. Using content_tag_for will give you .post and .page classes. If you want to use the same styles for all Node subtypes, you could apply the same styles to both classes in your stylesheet, or you could use ActiveRecord::Base#becomes:

<%= content_tag_for(:div, @post.becomes(Node)) do %>
  <h2><%= @post.title %></h2>
<% end %>

becomes will give you a new Node object with all the attributes of the @post object, so you will end up with the proper .node class on your object. You can also use this trick to render child class using the partial that would have been used for the parent class:

<%= render @posts.map { |p| p.becomes(Node) } %>

See the documentation on #becomes and read Henrik Nyh’s notes on using becomes with form helpers.

Styling objects

Now you’ve got your .post object and various sub types, such as .excerpt_post set up. These are now trivial to style using Sass and the @extend directive:

.post {
  h2 {
    color: #f00;
    margin: 20px 0;
  }
}

.excerpt_post {
    @extend .post;
  h2 {
    margin: 0;
  }
}

Which would result in something like:

.post h2, .excerpt_post h2 {
  color: #f00;
  margin: 20px 0;
}
.excerpt_post h2 {
  margin: 0;
}

With Sass 3.2, you might even keep the base .post class from the final style sheet — since you will most probably never use it on its own. Just use a placeholder selector:

%post {
  color: #333;
}
.excerpt_post {
  @extend %post;
  font-size: 12px;
}
.article_post {
  @extend %post;
  font-size: 14px;
}

This leads to the base class not being output, only the extensions:

.excerpt_post, .article_post {
  color: #333;
}
.excerpt_post {
  font-size: 12px;
}
.article_post {
  font-size: 14px;
}

Catches

Much like object-oriented programming, we can define generic styles for all post objects, and add special styles to special instances. Also much like object-oriented programming, note that that you ‘child’-objects should not differ too much from your ‘parent’-object; even though you are technically displaying a post object, it may be a better idea to classify your sidebar link item as a “link” rather than a “post”. Here, Rails leaves you on your own and you will have to write your markup yourself:

<li id="link_<%= post.id -%>" class="link">
  <%= link_to post.title, post %>
</li>

This is a nuisance, but it would be trivial to write a helper method for it. I haven’t come across it much in practice.

Don’t forget about mixins

Regardless, it is clear it makes sense to think about your object hierarchy before your build it. Also, as Ruby itself shows us with its modules, not everything is modelled best with a hieriarchical structure. Consider mixins to share styles across object types:

@mixin sidebar_link {
  font-weight: bold;
}
.link_post {
  @include sidebar_link;
}
.link_comment {
  @include sidebar_link;
}

Organising your files

Using the aforementioned techniques, you will quickly end up with a bunch of styled objects. It is probably best to abandon Rails’ convention of one-stylesheet-per-controller and keep each in a separate file using the module pattern:

app/
|--assets/
|  |--stylesheets/
|  |  |--application.css
|  |  |--layout.css.scss
|  |  |--elements.css.scss
|  |  |--modules/
|  |  |  |--post.css.scss
|  |  |  |--excerpt_post.css.scss
|  |  |  |--link_post.css.scss

Tip: When using Rails generators, you can provide the --no-assets or --no-stylesheets flag (along with --no-helper) to not generate the regular asset files you are not going to use anyway.

Your modules directory quickly clutters up so you had better group them in subdirectories:

app/
|--assets/
|  |--stylesheets/
|  |  |--application.css
|  |  |--layout.css.scss
|  |  |--elements.css.scss
|  |  |--modules/
|  |  |  |--posts/
|  |  |  |  |--post.css.scss
|  |  |  |  |--excerpt_post.css.scss
|  |  |  |  |--link_post.css.scss
|  |  |  |--comments/

Concatenating assets

With so many files lying around, it would of course be great if you could include them all in one fell swoop, rather than importing each by hand in the application.css manifest file. Sprockets can do that for you:

/*
 * in app/assets/stylesheets/application.css
 * require_tree .
 */

Done! But Sprockets, alas, will first compile every file and then concatenate them together. Now Sass will no longer be able to use mixins and variables from one file in another. This is a disappointment, so you will have to bypass Sprockets for concatenation and let Sass handle it for you. Rename application.css to application.css.scss and let the sass-rails gem handle the importing for your:

// in app/assets/stylesheets/application.css.scss
@import "modules/**/*"

This approach imports your modules sorted alphabetically, which is a great way to foce yourself to write truly independent modules that do not depend on the order in which they are defined. In practice, this might not work out, in which case you will have to revert to importing every module by hand in your manifest stylesheet.

Conclusion

There’s a little bit of friction when adopting these methods in your Rails application, but for the most part it is easy to get started and Rails has some nice convenience methods for us to use. With a little bit of discipline you can set up a very neat and organised front-end codebase — just make sure your approach is documented so all your team members are on the same page with you.

If you have any other tips, let me know on Twitter!