Building Component-Based Front Ends with Rails

Frederik Dohr, Lucas Dohmen

Our latest Ruby on Rails front-end project strongly emphasizes a component-based approach. In this post, we briefly explain how a tiny helper not only helped us render UI components, but also resulted in better components thanks to well-defined contracts and effortless composition.

These days, most front-end developers agree that UIs should be built in a component-oriented fashion. If you’re curious about that larger conversation, there are plenty of good resources on that out there: We’d highly recommend the excellent Style Guide Podcast — yes, all twelve episodes are well worth listening to — as well as Atomic Design to get a sense of why this matters.

Implementing a component-based approach can be a challenge. Generally speaking, it’s a good idea to maintain an application-independent style guide / component library, not least because it tends to result in more well-crafted components (e.g. regarding documentation, reusability and robustness). These days, we can choose from numerous tools to set up a library like that (e.g. Pattern Lab or Fractal).

Even so, you probably still want some sort of abstraction to generate components’ server-side markup[1] within your application, both for convenience and encapsulation:

<image-gallery>
    <ul>
        <li class="is-active">
            <img src="…" alt="…">
        </li>
        <li>
            <img src="…" alt="…">
        </li></ul>
</image-gallery>

(<image-gallery> here is a custom element, which unobtrusively takes care of client-side augmentation.)

Whenever we need to generate such complex HTML structures within our templates, we just want to invoke something like a function with the respective parameters:

component("image-gallery", images=[], selected_index=0)

Well, that’s pretty much exactly what we’ve done in our latest project — which happens to use Haml (“HTML Abstraction Markup Language”), though this approach should work just the same with ERB or whatever templating language you prefer[2]:

:ruby
  # parameters are passed into the component's template explicitly
  images = data.fetch(:images) # required
  selected_index = data.fetch(:selected_index, 0)

%image-gallery
  %ul
    - images.each_with_index do |image, index|
      %li{ class: index == selected_index ? "is-active" : nil }
        = image_tag image.src, alt: image.alt

Note that any parameters are passed in explicitly, which ensures that each component has a well-defined contract.

%main
  %h1 Portfolio
  %p Here's a selection of my favorite images:
  = component :image_gallery, data: { images: @images }

%footer
  %p © 2017 Unsigned Artist

Components may also support blocks to allow for composition:

= component :order_form, data: { logo: logo } do
  %footer
    %p All photos half price until solstice. Terms and conditions apply.
    = component :discount_voucher

Behind the scenes, component is just a tiny wrapper around the built-in render Rails helper:

module ComponentHelper
  def component(name, data: {}, &block)
    render_component("#{name}/#{name}", { data: data }, &block)
  end

  private

  def render_component(name, locals, &block)
    if block_given?
      # using `layout` is a trick to allow passing blocks to partials
      # (cf. http://stackoverflow.com/a/2952056)
      render layout: name, locals: locals, &block
    else
      render partial: name, locals: locals
    end
  end
end

Whenever component is invoked, it renders the respective markup partial from the corresponding directory in app/components[3] (e.g. app/components/image_gallery/_image_gallery.html.haml). That directory typically also contains whatever else the component might require, such as CSS and JavaScript assets as well as i18n translation files:

.
├── app
│   ├── components
│   │   ├── …
│   │   ├── image_gallery
│   │   │   ├── _image_gallery.html.haml
│   │   │   ├── _image_gallery.scss
│   │   │   ├── image_gallery.js
│   │   │   └── image_gallery.yml
│   │   └── …

We’d be happy to elaborate on that aspect some other time — let us know in the comments.

  1. Distinguishing between server-side base markup and augmented client-side DOM structures improves performance, robustness and generally makes us a good citizen of the web — but that’s a topic for another day.  ↩

  2. At least one of the authors is not particularly, err, partial to Haml.  ↩

  3. For this to work, we’ve extended ApplicationController to include append_view_path Rails.root.join("app", "components").  ↩

    Thumb staff member default

    Frederik Dohr is a senior consultant at innoQ. He started his career as a reluctant web developer hacking on TiddlyWiki in London. Back in Germany, he continues his vocal quest for simplicity, often by ranting about current trends in web development.

    More content

    Thumb smaller

    I’m a senior consultant at innoQ, programming in Ruby and JavaScript (and a lot of other languages in my free time ;)) and helping with technology decisions about different NoSQL solutions (and their adoption if we see a fit for them). Outside of work I’m doing a lot of open source and community work (the hacker and nerd calendar hacken.in, organising and teaching at our local CoderDojo and helped to organise the eurucamp conference) and record the podcast Nerdkunde (which is in German).

    More content

    Comments

    Please accept our cookie agreement to see full comments functionality. Read more