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.
-
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. ↩
-
At least one of the authors is not particularly, err, partial to Haml. ↩
-
For this to work, we've extended
ApplicationController
to includeappend_view_path Rails.root.join("app", "components")
. ↩