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 ist Senior Consultant bei innoQ und hat sich nach langjähriger Erfahrung mit JavaScript-Anwendungen, unter anderem als Co-Maintainer von TiddlyWiki, auf die Grundlagen des Web besonnen. Dabei vertritt er eine Philosophie der radikalen Einfachheit und Eleganz.

Weitere Inhalte

Thumb smaller

Lucas ist Senior Consultant bei innoQ und programmiert in Ruby und JavaScript (und vielen anderen Sprachen in seiner Freizeit ;)) und hilft bei der Entscheidung für verschiedene NoSQL Lösungen (und bei deren Einführung, wenn sie passen). Außerhalb seiner Arbeit beschäftigt er sich mit Open Source und Community Arbeit (wie den Hacker und Nerd Kalender hacken.in und Organisieren und Lehren beim lokalen CoderDojo) und ist Teil des Podcasts Nerdkunde.

Weitere Inhalte

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen