await.js

I built a JavaScript utility for asynchronous “promise”-style programming called await.js. It’s usable in browsers and also in node.js. I even published it to npm and wrote some tests in mocha, so it was quite a fun learning experience all around.

Backbone.js best practice #4

Backbone.js best practice #4: Loose coupling with views

When you’re coding a Backbone view, you’re contemplating two worlds: the world of instantiation and the world of the instantiated. The world of instantiation is everything outside the view. As far as your view is concerned, it’s a big, unpredictable ocean of state. The world of the instantiated is the view itself. As far as the rest of your app is concerned, it’s a self-contained bubble of state.

These worlds should be kept separate and independent. Instantiation code should never make assumptions about, or tamper with, the internal state of a view. The same is true of the view, with regard to the state of world into which it’s being instantiated. When your view needs to communicate with the rest of the world, simply use the API:

1. Instantiation code calls a view’s methods

The most common example of this is that when you instantiate a view, you would call the view’s constructor and render() method. However, a view can expose other methods if and when it makes sense, e.g. addItem().

2. Views trigger events

Suppose the user clicks the ‘delete’ button on your item subview. The item subview concerns itself with just that one item. Therefore, it shouldn’t attempt to remove itself from the containing list, because that would mean the view is altering the state of the world outside itself. The view should instead just do this.trigger('delete'), announcing to anyone who cares that the item just got deleted. Let the containing list view worry about handling that event. Note that this is easy to do since all views automatically extend Backbone.Events.

Example in code

Here’s an example regarding the el property of a view.

// BAD: view shouldn't know or care
// whether #content exists in the DOM
var FooView = Backbone.View.extend({ el: '#content', … })
var fooView = new FooView({ … })
fooView.render()
// GOOD: let the person who instantiates the view
// worry about whether #content exists in the DOM
var FooView = Backbone.View.extend({ … })
var fooView = new FooView({ el: '#content' })
fooView.render()

Here’s an example of a terms & conditions view being shown in a modal dialog.

// BAD: the terms & conditions view creates a dependency
// on being shown in a modal, and tries to operate on the
// world external to itself
var Terms = Backbone.View.extend({
    events: { 'click button.accept': 'accept' },
    accept: function(){ $('#modal').remove() },
    render: function(){ … }
})

...elsewhere...

var terms = new Terms()
terms.render()
showModalDialog(terms.el)
// GOOD: the view simply announces to anyone who may be
// interested that the terms & conditions have been
// accepted. Let whoever instantiates the Terms view
// worry about how to adjust the DOM when that happens.
var Terms = Backbone.View.extend({
    events: { 'click button.accept': 'accept' },
    accept: function(){ this.trigger('accept') },
    render: function(){ … }
})

...elsewhere...

var terms = new Terms()
terms.render()
var modal = showModalDialog(terms.el)
terms.on('accept', modal.close)

Backbone best practice #3

Backbone best practice #3: Delegate items to subviews

List views should defer all item-specific functionality to a subview, which gets one instance for each item in the list. The list view should be instantiated on a collection, the item view should be instantiated on each model in the collection.

Reason 1: Separation of concerns

Let the collection view concern itself with the concept of multiple things, like paging. Let the item subview concern itself with the concept of a single thing, like editing an item. This conceptual separation translates directly into a well-organized codebase.

Reason 2: Avoids littering the DOM with hints

Suppose you have a list of tweets, each with a retweet button. If you use a monolithic list view, your click handler only sees this.collection, forcing you to embed hints in the DOM so you have a way to retrieve a reference to the correct model. For example, <li data-id="a6f2364d7cb0e289">. If your click handler was deferred to an item subview, this.model would have been available, avoiding the need to embed anything in the DOM.

Reason 3: Code reuse

With an item subview, you have the option to instantiate just that subview in other places in your app.

Item subview example

/**
Display a list of tweets.
*/
var TweetList = Backbone.View.extend({
    events: {
        'click a[href].prev':'prev',
        'click a[href].next':'next'
    },
    prev:function(){ ... },
    next:function(){ ... },
    render: function(){

        /* populate this view's DOM */
        this.$el.html(template.render('tweet-list'));

        /* grab a reference to this freshly-created element */
        var $container = this.$('#container');

        /* Instantiate item subviews */
        this.collection.each(function(model){
            var view = new TweetItem({model:model});
            view.render();
            $container.append(view.el);
        });
    }
})
/**
Display a single tweet.
*/
var TweetItem = Backbone.View.extend({
    events: {
        'click button.retweet':'retweet'
    },
    retweet: function(){ this.model.retweet(); }
    render: function(){ ... }
})

Finally, while this post mentions lists and items specifically, this concept can be abstracted to more than just lists. It often makes sense to delegate responsibility of a specific subsection of the UI to a subview.

Backbone best practice #2

Backbone best practice #2: Instantiating views on models and collections

You can instantiate views on any data, but the idea here is to only instantiate them on models and collections.

Reason 1: Easy to remember

It’s easy to remember how to instantiate views that follow this pattern. This makes makes development, maintenance, and refactoring easier by avoiding the need to consult the source code every time you want to use a view.

Reason 2: Code reuse

Models and collections are reused all over your app. When you add a method to one, it becomes available everywhere that model or collection is used. Learning to use these APIs may be construed as work, but the knowledge gained is super-useful because it applies throughout the codebase.

Reason 3: Convention and community

The Backbone way is to instantiate views on models and collections. Not only do you get this.model and this.collection for free within your view, but you get the benefit of doing things the same way as lots of other people who use Backbone. When you follow this pattern, community and documentation become more effective resources for you.

Examples in code

// bad
new FooView({
    name: model.get('name'),
    value: model.get('value')
})

// good
new FooView({ model: model })

Backbone best practice #1

Backbone best practice #1: Aligning with native DOM functionality

Aligning with native DOM functionality means leveraging the things browsers natively know how to do. For example, <span> isn’t natively-interactive, and <a> is only interactive with the href attribute. <button> and other form controls are interactive, without the disabled attribute.

Instead of treating these capabilities as cruft from the era of static websites, your code should go with the flow. You could use JavaScript and CSS to make a <span> into a button, but it’s better to just use <button>. Here’s some of the reasoning behind this.

Reason 1: Get things for free

  • UI elements can receive focus.
  • Ability to tab or arrow through UI elements.
  • Ability to press ENTER to activate a UI element.
  • Appropriate default CSS styling.
  • Fails to violate accessibility rules.

Reason 2: The codebase is less noisy

By keeping markup and selectors POSH, code is more meaningful and contains fewer surprises. Toggling the disabled property on a form control, for example, does the expected thing.

Examples in code

<!-- bad: no keyboard nav :( -->
<span class="delete">Delete this item</span>

<!-- good: keyboard nav-able :) -->
<button class="delete">Delete this item</button>
// bad: what if user hits enter from within a text field?
events: { 'click input[type=submit]': 'save' }

// good: aligns with native browser functionality
events { 'submit form': 'save' }
/* bad: non-links will get underlines */
a:hover { text-decoration:underline; }

/* good: only links get underlines */
a[href]:hover { text-decoration:underline; }
/* bad: non-links will trigger page next event */
events: { 'click a.next': 'next' }

/* good: only links trigger page next event */
events: { 'click a[href].next': 'next' }

Case study

Here’s an approach for building simple paging controls, to show the idea in action. Note how the absence of the href attribute, by itself, fully grays-out and disables the link. Both in terms of your js and css, and browser treating it as a non-interactive element.

<!-- handlebars template -->
<a class="page prev" {{#has_prev}} href="#" {{/has_prev}}>prev</a>
<a class="page next" {{#has_next}} href="#" {{/has_next}}>next</a>

/* CSS */
a.page { color: grey; }
a[href].page { color: blue; text-decoration: none; }
a[href].page:hover { text-decoration: underline; }

// Backbone view
events: {
  'click a[href].prev': 'prev',
  'click a[href].next': 'next'
}