Refactoring to Decouple HTML, CSS, and JS

After reading Tips for how to decouple your HTML, CSS, and JavaScript, I was struck by how stupid I've been marking up my HTML for CSS and JS events. I started rewriting my latest project using these principles and they've really helped create a separation of concerns.

To illustrate how this has helped, I was coding up a quick administrative interface to CRUD some service model classes in my project and I started to really hate the UI/UX I chose. I decided to switch to a table since the data is more tabular in nature and I was quite please to see that none of the JavaScript functionality broke.

Note: this is using Rails 4.0 with AJAX events

This is the HTML before the change.

<!-- /app/views/admin/services/index.html.erb -->
<div class='row'>
  <div class='span8'>
    <div class='row js-service-list'>
      <% @services.each do |service| %>
        <%= render service %>
      <% end %>
    </div>
  </div>
</div>

A partial is used here because it is also called by the JavaScript returned by the create action. It is listed below.

<!-- /app/views/admin/services/_service.html.erb -->
<div class='span8 box-container'>
  <div class='holder'>
    <div class='prop-info'>
      <%= link_to admin_service_url(service) do %>
        <h3 class='prop-title'><%= service.name %></h3>
        <ul class='more-info clearfix'>
          <li class='info-label clearfix'>
            <span class='pull-left'>Created</span>
            <span class='qty pull-right'><%= service.created_at %></span>
          </li>
          <li class='info-label clearfix'>
            <span class='pull-left'>Last Updated</span>
            <span class='qty pull-right'><%= service.updated_at %></span>
          </li>
        </ul>
      <% end %>
    </div>
  </div>
</div>

As you can see, there are a lot of tags that create panels to show each service record. The view template (index.html.erb) shows the JavaScript hooks (prefixed with js- classes). Those hooks are the only things that are required when I make the UI changes.

Here is the new view template.

<div class='row'>
  <div class='span8'>
    <div class='row'>
      <div class='span8 box-container'>
        <table class='table'>
          <thead>
            <tr>
              <th>Name</th>
              <th>Created</th>
              <th>Last Updated</th>
              <th>&nbsp;</th>
            </tr>
          </thead>
          <tbody class='js-service-list'>
            <% @services.each do |service| %>
              <%= render service %>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

And here is the new partial.

<!-- /app/views/admin/services/_service.html.erb -->
<tr>
  <td><%= link_to service.name, admin_service_url(service) %></td>
  <td><%= service.created_at %></td>
  <td><%= service.updated_at %></td>
</tr>

I made the changes, refreshed my browser and tested out the AJAX functionality again. Everything worked the first time! It all happened because the JavaScript response from the server doesn't assume any knowledge of the HTML structure, it simply hooks into exact tags that it is looking for.

Colour me impressed.

If it helps, here is the JavaScript returned by the create action (whether successful or not). Most of it won't make sense without context but rest assured that everything works before and after the HTML was changed.

// /app/views/admin/services/create.js.erb
<% if @service.errors.any? %>
  $(".js-form-input-name-container").removeClass("error").addClass("success");

  $('.js-form-input-name-error').html('').append("<span class='help-inline'><i class='icon-ok-circle'></i></span>");

  <% @service.errors.messages.each do |m| %>
    $('.js-form-input-<%= m.first.to_s %>-container').removeClass('success').addClass('error');
    $('.js-form-input-<%= m.first.to_s %>-error').html('');

    <% m.second.each do |i| %>
      $('.js-form-input-<%= m.first.to_s %>-error').append("<span class='help-inline'><i class='icon-exclamation-sign'></i> <%=j i %>");
    <% end %>
  <% end %>
<% else %>
  $(".js-new-service-form")[0].reset();
  $("<%=j render(@service) %>").addClass("new").hide().prependTo(".js-service-list").fadeIn();

  $(".js-form-input-name-container").removeClass("error").removeClass("success");
  $(".js-form-input-name-error").html("");

  totalCount = $('.js-total-count');
  totalCount.html(parseInt(totalCount.html())+1);

  $('.js-last-added').html('<%=j time_ago_in_words(@service.created_at) %> ago');
  $('.js-link-to-newest').html('<%=j link_to @service.name, admin_service_url(@service) %>');
<% end %>