Wejetset City Notes Interactive: Notes from Sammy.js in Production
Last week I had the pleasure of launching a big new feature set into production for one of my favorite clients, Wejetset. The excitement was greatly enhanced by the fact that City Notes Interactive makes full use of my own special framework, Sammy.js.
City Notes and the other features deployed were an almost ideal project. The design and direction was an awesome back-and-forth collaboration between myself, Tom Ran of The Scout fame, and the face and brains behind Wejetset, Taj Reid. I could probably talk forever about how much fun it is to work with those guys, but in this post I’d rather dive deeper into the challenges and takeaways in the actual code.
I knew right from the beginning that Sammy would be a good fit for this. The question was how. Sammy let me think about the construction of the app in terms of routes and instantly gave me a way to create perma-links to specific notes and even specific filters.
Templating
One of the first decisions I made was to go with server-side templating. As much as I am and have been a proponent of pushing everything client-side, for this specific app it made much less sense. Specifically, there are other places throughout the site where I’m using the same templates and the benefits of re-using them were just too strong. Also, this gives the extra bonus of making the site a little more google friendly. In fact, without JavaScript the most recent notes still display and can link to the right destinations.
Forms
One thing that I wanted to streamline as much as possible is handling forms – posting them to the server and handling the responses without leaving the page. I came up with a useful method – postFormDirectly()
– that handles the post route and submits it directly to the server, handling the response and sending it back to a callback.
Heres an example call, for positing the form to create city notes.
this.postFormDirectly('/city_notes', function(response) {
if (response.status == 'success') {
this.redirect('#/');
}
});
Instantly, you can tell that a lot is abstracted and changed from a traditional route. Here we’re routing on an actual URI as opposed to just an anchor. This has the added benefit of allowing the form to work without JS. (As an aside, I’ve personally found a key to making degradable and unit testable sites is to first make the process work (albeit without any flair) without JS and then add the code to push the interactions and the single page-ness.)
To give a little more insight, heres the implementation:
this.postFormDirectly = function(path, callback) {
var app = this;
app.post(path, function(context) {
var context = this,
post_path = this.path,
wrapped_callback = function(response) {
WJS.handleResponse(response, context.params, function(r) {
if ($.isFunction(callback)) {
callback.apply(context, [r]);
}
});
};
if (!post_path.match(/js$/)) { post_path = post_path + '?format=js'; }
this.params['$form'].find(':input').trigger('clear-errors');
this.params['$form'].ajaxSubmit({
url: post_path,
dataType: 'json',
success: wrapped_callback,
error: wrapped_callback
}).trigger('show-loading');
});
};
Lets take a little walk through. First and formost, we’re just adding a post
route to our Sammy app. The contents of this route allow us to use jquery.form’s nifty ajaxSubmit
to send the form to the server without reloading the page. We’re creating a response callback for the ajax request called wrapped_callback
which takes the response from the server, passes it to handleResponse
which then in turn calls our passed callback if it exists. handleResponse
does a lot, too, but I’m not going to post the contents here (though I might make it into a plugin …). All you need to know is that the response from the server is JSON and looks something like:
{"status": "success", "message": "You're note was posted successfuly."}
handleResponse
displays the little flash message and also highlights the form if there are errors. I use this pattern on a number of projects and it seems to work pretty well.
One neat Sammy tidbit you might not have known about, in post & put routes, there is always a ‘$form’ param which is a jQuery object containing the HTML form that submitted to the route. Very useful for cases like above.
Modals
If you play around with the app, you’ll notice that we make pretty good use of modal windows for actions (submit/share/email to friend). The cool thing about using Sammy for this is they’re just routes, so refreshing the page or linking jumps directly to them. I also wrote a little jQuery extension that I found extremely useful for them:
$.fn.findOrAppend = function(selector, element) {
var $this = $(this),
$el = $this.find(selector);
if ($el.length > 0) return $el;
// el doesnt exist so lets make it
if (typeof element == 'undefined') {
// figure out how to build the element from the selector
var parts = selector.split(' ');
$el = $("<div>");
if (parts.length == 1) {
// single selector so just added
$el.addSelector(selector);
} else {
// selector with multiple depth so we just want the last selector
$el.addSelector(parts.pop());
// find this down the parts
$this = $this.findOrAppend(parts.join(' '));
}
} else {
// we provided a specific element
$el = $(element);
}
// append it
$this.append($el);
// return it
return $el;
};
findOrAppend()
takes a selector (I used it mostly with #ids) and creates a series of nested divs (if needed) that matches that selector.
For example, given HTML:
<div id="container">
<div id="main"></div>
</div>
And we wanted to replace the contents of an element #submit_modal inside of it:
var $modal = $('body').findOrAppend('#main #submit_modal');
The first time its called, it actually creates the neccesary #submit_modal div and returns it. However, if it exists already, it will just find it using $.fn.find()
and return it.
For modals, this allows me to ensure that the container exists first, then replace it with the contents of the modal that I’m fetching from the server.
this.helpers({
// …
showModal: function(path, selector, callback) {
var context = this;
this.showLoading();
this.loadNotesInBackground();
this.partial(path, function(form) {
var $el = context.$element()
.findOrAppend(selector);
var $close = $('<img src="/images/button_remove.gif" alt="close" class="close_modal" />');
$el.hide().html(form);
$close.prependTo($el).click(function() {
$el.slideUp(200);
context.redirect('#', context.conditionsToURL());
});
if (callback) { callback.apply(context, [$el]); }
// bind error handlers
context.bindFormErrorHandlers();
context.hideLoading();
$el.slideDown();
$('#main').slideTo();
});
}
//…
});
Here I have a helper that fetches the view/template from the server using partial()
and then situates its content using findOrAppend()
. It also does me the favor of adding the little close icon in the upper right corner. A typical use is for the login screen:
this.get('#/login', function(context) {
this.showModal('/account/login', '#main #login_signup.modal')
});
Analytics
It has to be said as well that I’m really excited about the burgeoning Sammy.js community. I’m lucky enough to have some other really smart individuals working on Sammy projects and sharing some of their code through plugins. Thanks to Brit Gardner and his Sammy.GoogleAnalytics plugin, adding analytics to the app was as easy as dropping in the file and adding one line to my app. That felt really really good. I hope more people will start sharing some plugins soon.
A lot more
There were a lot more discoveries over the months building this app then I can really share in a little blog post but needless to say, I’m going to try to bring the usefull and reusable stuff back into Sammy and plugins for everyone to use. If you want to hear more about it or have specific questions, dont hesitate to ping me on IRC or twitter or email the Sammy mailing list.