Fortitude expresses your view code as Ruby itself, using a simple DSL patterned after HTML. One big benefit of this approach is its consistency: views, helpers, and partials are all written in the exact same language, Ruby.
Ruby is a great deal nicer to work with than either an HTML templating language or the string interpolation of traditional helper methods. We’ll see the difference this can make in the following example.
The following ERb code was extracted from a real-world application, and is a great, simple example of where Fortitude can help the most.
...
<a href='<%= conditional_refresh_url(:user => @user) %>'
class="button icon refresh" onclick="javascript:handleRefreshClick();">
<div class="button_text">
<p>Refresh this page if:</p>
<ul>
<li>Content has changed</li>
<li>Local data is <%= @out_of_date_condition %></li>
</ul>
</div>
</a>
...
Combined with appropriate CSS, this code creates an “icon button” — a small icon, with a tooltip available:
Unsurprisingly, this button gets used in many places throughout the application. Using ERb, let’s see what we can do to factor out this common pattern.
Using traditional templating engines, we have two choices:
render :partial => ....
In either case, we’ll need to make sure we allow for several variations in our desired output:
href parameter) is, of course, different in almost every case.
onclick, data-*, etc.) to the a element, and sometimes not.
Given these constraints, let’s see how each of these looks.
Here’s what our new helper method looks like:
def icon_button(icon_name, target, tooltip_html, additional_attributes_string = "")
"<a href="#{target}" class="button icon #{icon_name}" #{additional_attributes_string}><div class="button_text">#{tooltip_html}</div></a>"
end
And here’s how we’d call it for the example above:
...
<%= icon_button(:refresh, conditional_refresh_url(:user => @user), "<p>Refresh this page if:</p><ul><li>Content has changed</li><li>Local data is #{h(@out_of_date_condition)}</li></ul>", "onclick="javascript:handleRefreshClick();"")
What’s good and what’s bad about this?
additional_attributes_string is escaped before being passed. Similarly, we need to remember to call h() on the variable being interpolated into the tooltip_html string before passing it, or else we’ll be vulnerable to XSS attacks again.
Yikes. We’ve factored out this helper method, which cleans up our calling code, but at a rather significant expense. Perhaps we can do better by creating an actual partial, instead?
Here’s what our new partial looks like:
<a href="<%= target %>" class="button icon <%= icon_name %>"
<%= (defined?(additional_attributes) ? additional_attributes || '') %>">
<div class="button_text">
<%= tooltip_html %>
</div>
</a>
And here’s how we’d call it for the example above:
<%= render :partial => '/shared/buttons/icon_button', :locals => {
:target => conditional_refresh_url(:user => @user),
:icon_name => 'refresh',
:additional_attributes => 'onclick="javascript:handleRefreshClick();"'.html_safe,
:tooltip_html => %{<p>Refresh this page if:</p>
<ul>
<li>Content has changed</li>
<li>Local data is #{h(@out_of_date_condition)}</li>
</ul>}.html_safe
} %>
Again, what’s good and what’s bad about this?
ERb, not a Ruby string. On the other hand, we still have to use a Ruby string for the HTML we’re passing in to it.
_icon_button.html.erbpartial, how can you easily tell which variables you need to pass in, and which of those are optional? You can’t — except by reading through the entire text of the partial and thinking about each use carefully, which is really painful. (In this, the helper method was certainly a lot cleaner, too.)
tooltip_html string is correctly escaped using h() before being passed in. (We also need to make sure we call #html_safe on the value we pass to additional_attributes, too; this is less dangerous, but also very easy to forget, and will cause corrupt HTML if we forget about it.)
Both cases above are far from perfect. Helper methods are more succinct to call, yet also a lot messier to both write and call when passing around HTML, and have serious XSS risks. Partials mitigate some of those problems, but introduce others, and are really verbose and messy to call.
Ironically, some of the issues above may feel a little unfamiliar, and it’s probably because of this: nobody does this — because both these approaches have such serious tradeoffs, most teams, developers, or designers just leave well enough alone for small(ish) examples like this, and repeat the original HTML everywhere, over and over. And when it comes time to change the HTML structure of this “icon button” element that’s been repeated all over the site, we either just bite the bullet and do a really painful global search and repeated manual modificaftion, or give up, hack on CSS to make it do sort of what we want it to do, and then leave that mess in place.
We imagine that this is just the way views are. In fact, these problems are all because we generally don’t have the right tools to do a better job. Views don’t need to be like this, any more than any code needs to be like this.
To see what Fortitude brings to this example, let’s start by looking at Fortitude code for the original example, before we try to refactor it:
...
a(:href => conditional_refresh_url(:user => @user),
:class => 'button icon refresh') {
div(:class => 'button_text') {
p "Refresh this page if:"
ul {
li "Content has changed"
li "Local data is #{@out_of_date_condition}"
}
}
}
...
We won’t delve more deeply into Fortitude’s syntax immediately, but it should be clear that Fortitude expresses HTML using a very simple Ruby DSL that’s easy to grasp.
Using this syntax, we can now factor out this shared code as a “helper method” that we can easily make available on any view that needs it.
(We put “helper method” in quotes because this isn’t a traditional helper that we’d put into something like app/helpers/application_helper.rb and write using Ruby string interpolation. Rather, it’s a method we define in a module, and mix into any view class that needs it — Fortitude views are actually just normal Ruby classes — or into our base view class to make it magically available everywhere.)
Using this strategy, we can create the following “helper method”:
def icon_button(icon_name, target, additional_attributes = { })
a(additional_attributes.merge(
:href => target, :class => "button icon #{icon_name}")) {
div(:class => :button_text) {
yield
}
}
end
And the calling code for the example above now looks like this:
...
icon_button('refresh', conditional_refresh_url(:user => @user),
:onclick => 'javascript:handleRefreshClick();') {
p "Refresh this page if:"
ul {
li "Content has changed"
li "Local data is #{@out_of_date_condition}"
}
}
...
Let’s take a look at how much cleaner this is. A few of the most obvious improvements:
However, one of the biggest improvements we’ve made is slightly more subtle: by creating this new icon_button method, we’ve started slowly building up a customized view language that is specific to our needs and our application. As we proceed through further examples, we’ll see how powerful this can be in creating clean, concise, maintainable views that are a joy to use.