templates are functions too

programming ruby rails

this shows you how you can empower your rails templates to become components that style themselves, and how to get a grip on css by treating it as a dependency.

erb templates are functions. and not in some hand wavy way. when you write an erb template like this:

<div>
  Greetings, <%= name %>
</div>

it is compiled into a function:

def _app_views_pages__example_html_erb__1799925575419746472_70157429325540(local_assigns, output_buffer)
  _old_virtual_path, @virtual_path = @virtual_path, "pages/_example";_old_output_buffer = @output_buffer;name = name = local_assigns[:name];;@output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.safe_append='<div>
Greetings, '.freeze;@output_buffer.append=( name );@output_buffer.safe_append='
</div>
'.freeze;@output_buffer.to_s
ensure
  @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
end

this is not a download a car scenario. any qualities that make a function bad should sound the same alarms in a template.

they are also code in a more hand wavy kind of way: all the css your template uses is a dependency. just because rails’ autoloader takes the require statements out of your ruby doesn’t mean it doesn’t have a depency. every time you use a css class it’s another node on that dependency graph. and you don’t have the autoloader.

i was writing templates in react/jsx, and doing css in javascript when i realized this. you literally import all styles as dependencies.

regular erb templates are still in the css stone age, where you write classnames and think of it like some loose hooks for css (#ids and tags if we’re neanderthals.) we’re in the zen garden! it’s so elegant! admire how separate the content is from presentation.

like anything that hooks onto anything, you’re getting the ability to hook different things onto the thing, at the expense of the thing falling off or hooking onto something else by accident because hooks aren’t the most structurally sound thing to build with. this is a tradeoff i do not enjoy the benefits of: i hook my html to a single set of styles.

when your templates aren’t responsible for styles, your css has to do all the heavy lifting. you might end up using the cascade to style things, e.g. .navigation > li > a, or use long classnames as a namespacing crutch which is hard for everyone to be good at. want to style something? decision time! choose a meaningful name for the thing (oh god, this i dread) and it better be unique in a global scope, semantically appropriate, and follow the right bem/suitcss/smacss conventions or you’ll be sorry. have fun explaining the naming system to every person touching the codebase.

what i’m interested in is moving into the bronze age. i don’t how to get there but something i’ve seen that i want to replicate is to move complexity from css to your templates. to do this you have to:
a) call these empowered templates components
b) make them stand on their own.

these components need to have these qualities:

  1. reusable in different places without looking any different
  2. can be composed together
  3. can be extended upon

to get to these components we need to unwind some common ways of writing css.

first things first, give up the cascade, wherever possible. instead of css that looks like this:

.nav > li > a {  }
<nav class="nav">
  <li>
    <a>Homepage</a>
  </li>
</nav>

avoid the cascade and write it like this:

.nav-link {  }
<nav class="nav">
  <li>
    <a class="nav-link">Homepage</a>
  </li>
</nav>

the previous example uses a .nav-link class that decides how a navigation link should look. we want our components to be empowered, so the components need to describe how things look.

enter utility classes: a small class that does one thing and has a class name which describes the rules they apply. utility classes empower your templates to control how they look:

.bg-black { background: #111 }
.white { #fff }
.py2 { padding-top: 1rem; padding-bottom: 1rem }
.px1 { padding-left: .5rem; padding-right: .5rem }
<nav class="nav">
  <li>
    <a class="bg-black white py2 px1">Homepage</a>
  </li>
</nav>

there are some good libraries with these utility classes that you can install and treat as your template’s standard library. my favourite is basscss, and the new bootstrap v4 comes with a bunch of utility classes which i’m delighted to see. sadly all of bootstrap’s utility class rules have !important on them because bootstrap hasn’t given up the cascade (yet.) others i know of are suitcss, tachyons, and atomic.

using them with conventional templates as if they were regular classes sort of sucks: i might have 5 nav links, and i don’t want to change each one every time someone paints the bikeshed. so what you do is build a nav link component:

<%= tag.a { cass: "bg-black white py2 px1" } do %>
  <%= yield %>
<% end %>

and reference that instead:

<nav>
  <li>
    <%= render 'components/nav/link' do %>
      Homepage
    <% end %>
  </li>
  <li>
    <%= render 'components/nav/link' do %>
      About
    <% end %>
  </li>
</nav>

this is great until you need a nav link to look different than the others. if you wrote your css in BEM you’d add a modifier class and pray to the specicifity gods that your classes are ordered correctly.

with empowered components you need a way to set html classes both from where it’s called, and in the component itself. luckily your templates are a function, they can call other functions.

# combine_attr({ class: "a" }, { class: "b" }) 
# => { class: ["a", "b"] }
def combine_attr(caller_html, partial_html)
  caller_html.merge(partial_html) do |key, v1, v2|
    [v1, v2].flatten(1)
  end
end
<%= tag.a combine_attr(local_assigns, 
                       { cass: "bg-black white py2 px1" }) do %>
  <%= yield %>
<% end %>
<nav>
  <li>
    <%= render 'components/nav/link', href: root_path do %>
      Homepage
    <% end %>
  </li>
  <li>
    <%= render 'components/nav/link', href: about_path, 
               class: "active" do %>
      About
    <% end %>
  </li>
</nav>

combine_attr won’t be the answer for everything, but you have the entire ruby toolbox at your disposal to come up with something that solves your problem.

OK, but my component needs some styles that are outside of the standard library. what to do?

first off, this new style will need a class name. if someone was walking into this project, only saw the html with your class name, would they be able to predict what effect it would have?
no → pick a better name. consider breaking this down into multiple class names
yes → cool, continue on.

as for where to put this style: i don’t know what’s best. choose your own adventure:

a) a new stylesheet. declare it’s path as a dependency at the top of the component. uniq these stylesheets and <link> them in the head of your site through the magic of content_for. from what i understand about http/2, it’s just as fast to load 20 small stylesheets as one big one (update: sadly this is not the case), so keep ‘em small! aim to keep your standard library small, and keep your depency graph obvious. amazon cloudfront supports http/2 now and it’s easy to set up.

b) same as above, but instead of <link>ing to those stylesheets, plonk them right into your html with some asset pipeline magic:

Rails.application.assets['path/to/stylesheet'].to_s.html_safe

you won’t see the same http caching benefits, but you’ll save http requests. this might be appropriate for one-off styles that are specific to your component.

c) tack on css files to the end of your standard library. this is the most simple answer, but the worst: when you delete a component, you won’t know if there are styles to delete. they linger.

d) straight up write inline styles in style tags or html attributes, and dgaf about reuse. this is probably a better option than c) but people reading your code might think you’ve gone off your rocker. since templates are functions, and we’re treating css as a dependency, this is sort of the equivalent of writing a proc/lambda. but be weary of that global scope.

once you’ve killed the css cascade you could have fun with this. write a wrapper around every css classname that you use in a request:

<%= tag.p class: c("text-center bg-white") %>

split those styles on space and push them into a Set. use that set to generate a stylesheet specific to your page. voila, now we have the autoloader. work the component filename’s md5 hash in there and you’ve got a namespace. you don’t have to write javascript to come up with wacky css ideas!

so we want these to be composable. by this i mean, you should be able to use these components together without having to change them. the easiest way you can make this happen is by using yield to pass content instead of variables. we could change our nav interface to look like this:

<%= render 'components/nav', href: root_path do %>
  <%= render 'components/nav/item', class: "float-right" do %>
    <%= tag.img src: 'logo.svg' %>
  <% end %>

  <%= render 'components/nav/item_link', href: root_path do %>
    Homepage
  <% end %>

  <%= render 'components/nav/item_link', href: about_path,
             class: "active" do %>
    About
  <% end %>
<% end %>

now you have a bunch of small components that you use together. they start to get qualities that i enjoy in regular ruby functions. you could write these templates in straight ruby and it wouldn’t look much different than the erb.

this is what the individual components might look like:

<!-- nav -->
<%= tag.nav combine_attr(local_assigns, class: "…") do %>
  <%= yield %>
<% end %>

<!-- nav/item -->
<%= tag.div combine_attr(local_assigns, class: "…") do %>
  <%= yield %>
<% end %>

<!-- nav/link -->
<%= tag.a combine_attr(local_assigns, class: "…") do %>
  <%= yield %>
<% end %>

<!-- nav/item_link -->
<%= render 'components/nav/item', 
           local_assigns.except(:link_attributes) do %>
  <%= render 'components/nav/link', 
             local_assigns.slice(:link_attributes)) do %>
    <%= yield %>
  <% end %>
<% end %>

using yield will also give you the ability to extend components. e.g. you can make a totally different component, and bootstrap it off an existing one:

<!-- footer_nav -->
<%= render 'components/nav', 
           combine_attr(local_assigns, class: "…") do %>
  <%= yield %>
<% end %>

and that’s basically it. i like this idea and want to support it, and see what other people who write templates think of it. i hope to change my rails_components gem to provide a decent and common interface for stuff like combine_attr and the stylesheet helpers. maybe provide some handy generators for scaffolding. it’d be cool to build libraries of components for popular css frameworks like bootstrap, which might give people more of a reason to use it.

examples provided should work but you’ll need the latest version of rails for the new tag syntax, and for render to not explode when you pass it class.



home