Maintainable front-end code in Ruby on Rails applications

by George Brocklehurst from Reevoo

Presented at RailsConfUa

Ruby is only half the battle

As Ruby developers, most of us spend our time building websites, and whether we like it or not we have to write and maintain a lot of HTML, CSS and JavaScript.

Rails tells us a lot about how to structure our back-end code, but very little about how to structure our front-end code. Beyond providing a few simple helper methods it's more or less silent on the subject.

Just because Rails doesn't make these decisions for us doesn't mean they don't need to be made.

JavaScript & Rails

Hooray for Rails 3!

JavaScript helpers are now unobtrusive and framework agnostic.

Unobtrusive
JavaScript lives in .js files, not in .html files and the application still works if the .js files aren't loaded.
Framework agnostic
We don't have to use Prototype, but can replace it with the best framework for this particular team or project.

Rails 2

link_to_remote 'home', :url => root_path returns <a href="#" onclick="new Ajax.Request('/', {asynchronous:true, evalScripts:true, parameters:'authenticity_token=' + encodeURIComponent('1/GQG8AZN55Dpa…=')}); return false;">home</a>

Rails 3

link_to 'home', root_path, :remote => true returns <a href="/" data-remote="true">home</a>

Eventually, the helpers aren't enough

Although the helpers are now very good, they still only provide very basic functionality. As an app grows in size and complexity we are likely to want more JavaScript functionality than the Rails helpers can provide. Usually we would create a new file in public/javascripts and just start hacking, but there are better ways.

So how should we structure our JavaScript?

Structured JavaScript

MyApp.imageGallery = (function() {

  var imagesURL = 'http://example.org/images/';
  var loadMoreImages = function() { … };

  return {
    showLightbox: function() { … }
  };

}());

Initialising the right scripts

Ask the HTML what it needs

<body class='with-js-image-gallery'>

Common JavaScript API
MyApp.imageGallery = (function() {
  …

  return {
    globalInit:   function() { … },
    init:         function() { … }
  };

}());
A little bit of glue…
var key, className;

for(key in MyApp) {
  if(MyApp.hasOwnProperty(key)) {

    if(typeof MyApp[key].globalInit === "function") {
      MyApp[key].globalInit();
    }

    if(typeof MyApp[key].init === "function" &&
      jQuery('body').hasClass(classNameFromKey(key))) {
      MyApp[key].init();
    }
  }
}
var classNameFromKey = function(key) {
  return 'with-js-' + key.replace(/[a-z][A-Z]/g, function(str) {
    return str[0] + '-' + str[1].toLowerCase();
  });
};
What about AJAX?
Reading HTTP headers in JavaScript

xhr.getResponseHeader("X-With-JavaScript");

Calling init twice?
<div class='gallery'>
  …
  <a href='…'>More</a>
  <a href='…'>More</a>
</div>
Defensive init functions
return {
  init: function() {
    jQuery('.gallery:not(.enhanced)')
      .addClass('enhanced')
      .each(function() {
        var moreLink = jQuery('<a></a>')
                         .text('More')
                         .click(loadMoreImages);

        jQuery(this).append(moreLink);
      });
  }
};
Rails to the rescue
Requesting JavaScript
def use_javascript_for(feature)
  @js_features ||= []
  @js_features << feature.to_s.downcase
  if request and request.xhr?
    header = @js_features.uniq.join(' ')
    response.headers['X-With-JavaScript'] = header
  end
end

For example:

<div class='gallery'> … </div>
<% use_javascript_for 'image-gallery' %>
Adding the classes to the body
def javascript_feature_classes
  @js_features ||= []
  @js_features.uniq.map{ |f| "with-js-#{f}" }.join(" ")
end

For example: <body class='<%= javascript_feature_classes %>'>

CSS & Rails

Call things what they are

Good class names: <div class='product with-images'>

Bad class names: <div class='rounded-corners'>

Split things up

  1. reset.css
  2. elements.css
  3. layout.css

Serving front-end code from Rails

The future's bright

public is for designers, those messy kids, app is for the high-level programmer stuff that we care about. That's not really a good way of going about it … and we've been forgetting about stylesheets and JavaScript and ways we can optimise that for a long time. Let's not do that anymore.

David Heinemeier-Hansson speaking about Rails 3.1 at RailsConf

Benefits of putting JavaScript and CSS in the app

ERB in JavaScript

jQuery('<a></a>').text('More'); could be rewritten as jQuery('<a></a>').text(<%= I18n.t('more').to_json %>);

The future is now!

Rails CSS views

github.com/rhulse/rails-css-views

Sprockets

getsprockets.org & github.com/sstephenson/sprockets

JavaScript features

github.com/georgebrock/javascript-features

Any questions?

George Brocklehurst

“georgebrock” on Twitter, GitHub, etc.

These slides with notes and links:
georgebrock.com/conferences/rubyconfua2010