Using Sammy for changing page state

Max Williams

This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.


In case you haven’t seen Sammy.js yet, it is a really neat Javascript framework, loosely based on the very lightweight Ruby web framework, Sinatra.

Sammy allows you to specify ‘routes’ in your JS which have associated behaviour attached to them. This means that it is possible to create single-page applications on the client-side, that can easily switch between different states depending on what the user is doing. Your Rails application can therefore serve up json in response to certain requests, and the view state will be handled on the client.

It took me a while to figure out whether I thought this was useful or not, but I have now come down firmly on the side of yes (some caveats aside). This is because it allows you to declaratively specify different states, and quickly jump between them by using only the window’s location (specifically the anchor bit).

The simplest way of demonstrating how it can be used is a tab-based layout. This means that given the following html (assuming that Sammy and JQuery have been loaded up elsewhere):

...
<p>
  <a href="#/tab_1">Tab 1</a>
  <a href="#/tab_2">Tab 2</a>
</p>

<div>
  <div class="section" id="tab_1_section">
    <p>hello from tab 1</p>
  </div>

  <div class="section" id="tab_2_section">
    <p>hello from tab 2</p>
  </div>
</div>
...

and the following JS:

...
;(function($) {
  var app = new Sammy.Application(function() {
    with(this) {
      get('#/tab_1', function() { with(this) {
        $('.section').hide();
        $('#tab_1_section').show();
      }});

      get('#/tab_2', function() { with(this) {
        $('.section').hide();
        $('#tab_2_section').show();
      }});
    }
  });
  $(function() {
    app.run('#/tab_1')
  });
})(jQuery);
...

you will get a couple of tabs, with minimal effort.

Looking at the Sammy routes it is trivial to work out what all the states on the page are, and you don’t get them spattered around in views, and buried in Javascript files. To change state, you write simple links with anchors, and there is no need to bind complex event handlers to the page.

This simple app could be made more interesting if we changed the second tab to be a list of a user’s tweets. You wouldn’t necessarily want to load them up when the page first loaded, as they could be stale by the time someone switched tabs (hypothetically speaking).

You can therefore change the route to:

get('#/tab_2', function() { with(this) {
    $('.section').hide();
    // method declared somewhere else which pulls the tweets down and inserts them into the html of the page
    get_and_display_latest_tweets_on_tab_2();
    $('#tab_2_section').show();
}});

These are pretty lame examples, where the use of Sammy is rather overkill. However, in a real application which is more complex, it is easier to see the benefits.

Adding modal dialogs with Sammy

I recently encountered a use case where I combined Sammy with a JQuery plugin called Block UI.

What I wanted to do was:

  • present a user with a list of the projects in the system
  • allow them to click a link to add a new project
  • allow them to enter the new project details into a modal dialog
  • close the dialog, insert the new project into the list and display a message to them

Sammy makes it easy to break these different actions into their component parts and organise them in a meaningful way.

To accomplish this, we first define some routes:

  • #/
  • #/new\_project
  • #/create\_project

These are the various states of the page we will be using. Additionally, you will notice that create_project is defined as a ‘post’ request. This means that you can easily capture the data from a form in your page without sending people elsewhere when they hit “submit” (by setting the action to #/create\_project).

The html is simple, and consists of:

  • a link for the new project dialog, with the href of our route,
  • a list of projects
  • a form in a div called js_modal_dialog, which we will initially hide, but then inject into our dialog

When the page loads up we set the default state:

...
get('#/', function() { with(this) {
  $('#js_modal_dialog').hide();
  hide_modal_dialog();
  flash_message(null);
}});
...

This is also the state we return to after completing any actions, and therefore also serves to reset any changes in state we may have made (eg. showing flash messages).

When the user clicks the add project link we show them the dialog box

...
get('#/new_project', function() { with(this) {
  show_modal_dialog();
}});
...

In this dialog is a button to close (simply linking back to the #/ route), and the form.

When the form is submitted to #/create_project, we capture this post and show a flash message saying “loading” inside the dialog. We then take the submitted parameters and send them off via ajax (to a static file containing json in this case, which is why the project added to the list will always be the same, no matter what you type as the name).

We then follow a pattern which feels very familiar if you are used to Rails.

If the response is a success, we show a global flash message saying so, add the project to the list, and redirect to #/.

If the response is not successful, we display a flash message to the user inside the modal dialog, and do not redirect. This means that the user can correct the mistakes in the form, and re-submit it.

As with all technologies, there are pros and cons to their usage. In the example I encountered, this was a neat solution, however this may not always be the case. One of my main concerns about pure client-side interfaces is accessibility. In particular, Sammy lets you render partials via ajax into the page, and I could have injected the form in that way. However, by including the forms in the page, and hiding them with JS, they are still available if JS is not available.

Hopefully this can become a powerful tool in our toolboxes, and used in the right way I believe it can make complicated state-changing pages more maintainable and understandable.