About The Author

Joseph Zimmerman is a God-fearing and loving family man, Web Developer, JavaScript Blogger, teacher, and amateur gamer. More about Joseph

A Thorough Introduction To Backbone.Marionette (Part 3)

Quick Summary

In this series on Backbone.Marionette, we’ve already discussed Application and Module. This time, we’ll be taking a gander at how Marionette helps make views better in Backbone. Marionette extends the base View class from Backbone to give us more built-in functionality, to eliminate most of the boilerplate code and to convert all of the common code down to configuration.

Table of Contents
Membership counter

Members support Smashing

Wonderful, friendly people who keep this lil' site alive — and get smarter every day.

Are you smashing, too? →

To help you tap the full potential of Marionette, we’ve prepared an entire eBook full of useful hands-on examples which is also available in the Smashing Library. — Ed.

In this series on Backbone.Marionette, we’ve already discussed Application and Module. This time, we’ll be taking a gander at how Marionette helps make views better in Backbone. Marionette extends the base View class from Backbone to give us more built-in functionality, to eliminate most of the boilerplate code and to convert all of the common code down to configuration.

Further Reading on SmashingMag:

I highly recommend that you go back and read the articles about Application and Module first, if you haven’t already. Some things may be mentioned in this article that refer to the previous articles, and this is part of a series about Marionette, so if you wish to learn about Marionette, you should read the whole series.

Event Binding

Up until recently, Backbone views were often mishandled, causing a horrible problem known as “zombie views.” The problem was caused by the views listening to events on the model, which in itself is completely harmless. The problem was that when the views were no longer needed and were “discarded,” they never stopped listening to the events on the model, which means that the model still had a reference to the view, keeping it from being garbage-collected. This caused the amount of memory used by the application to constantly grow, and the view would still be responding to events from the model, although it wouldn’t be rendering anything because it was removed from the DOM.

Many Backbone extensions and plugins — including Marionette — remedied this early on. I won’t go into any detail on that, though, because Backbone’s developers remedied this problem themselves (finally!) in the recently released Backbone 1.0 by adding the listenTo and stopListening methods to Events, which Backbone’s View “class” inherits from. Marionette’s developers have since removed their own implementation of this feature, but that doesn’t mean Marionette doesn’t help us out with some other things related to event binding.

To make binding to events on the view’s models and collections simpler, Marionette gives us a few properties to use when extending Marionette’s views: modelEvents and collectionEvents. Simply pass in an object where the keys are the name of the event we’re listening to on the model or collection, and the property is the name(s) of the function to call when that event is triggered. Look at this simple example:


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    modelEvents: {
        'change:attribute': 'attributeChanged render',
        'destroy': 'modelDestroyed'
    },

    render: function(){ … },
    attributeChanged: function(){ … },
    modelDestroyed: function(){ … }
});

This accomplishes the same thing as using listenTo, except it requires less code. Here’s the equivalent code using listenTo.


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    initialize: function() {
        this.listenTo(this.model, 'change:attribute', this.attributeChanged); 
        this.listenTo(this.model, 'change:attribute', this.render); 
        this.listenTo(this.model, 'destroy', this.modelDestroyed);
    },

    render: function(){ … },
    attributeChanged: function(){ … },
    modelDestroyed: function(){ … }
});

There are a couple key things to note. First, modelEvents is used to listen to the view’s model, and collectionEvents is used to listen to the view’s collection (this.model and this.collection, respectively). Secondly, you may have noticed that there are two callbacks for the change:attribute event. When you specify a string for the callbacks, you can have as many callback function names as you want, separated by spaces. All of these functions will be invoked when the event is triggered. Any function name that you specify in the string must be a method of the view.

There are alternative ways to specify modelEvents and collectionEvents, too. First, instead of using a string to specify the names of methods on the view, you can assign anonymous functions:


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    modelEvents: {
        'change': function() {
            …
        }
    }
});

This probably isn’t the best practice, but the option is there if you need it. Also, instead of simply assigning an object literal to modelEvents or collectionEvents, you can assign a function. The function will need to return an object that has the events and callbacks. This allows you to create the list of events and callbacks dynamically. I haven’t been able to think of any situations in which you would need to determine event bindings dynamically, but if you need it, this could be very handy.


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    modelEvents: function() {
        return {'destroy': 'modelDestroyed'};
    },

    modelDestroyed: function(){ … }
});

The modelEvents and collectionEvents feature follows the pattern that Backbone and Marionette use as often as possible: Relegate code to simple configuration. Backbone itself did this with the events hash, which enables you to easily set up DOM event listeners. Marionette’s modelEvents and collectionEvents are directly inspired by the original events configuration in Backbone. You’ll see this configuration concept show up a lot, especially in subsequent articles, when we get into ItemView, CollectionView and CompositeView.

Destroying A View

As I mentioned at the beginning of the previous section, sometimes a view needs to be discarded or removed because a model was destroyed or because we need to show a different view in its place. With stopListening, we have the power to clean up all of those event bindings. But what about destroying the rest of the view? Backbone has a remove function that calls stopListening for us and also removes the view from the DOM.

Generally, this would be all you need, but Marionette takes it a step further by adding the close function. When using Marionette’s views, you’ll want to call close instead of remove because it will clean up all of the things that Marionette’s views set up in the background.

Another benefit offered by Marionette’s close method is that it fires off some events. At the start of closing the view, it’ll fire off the before:close event, and then the close event when it’s finished. In addition to the events, you can specify methods on the view that will run just before these events are fired.


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    onBeforeClose: function() {
        // This will run just before the before:close event is fired
    },

    onClose: function(){
        // This will run just before the close event is fired
    }
});

If you want to run some code before the view disappears completely, you can use the onBeforeClose and onClose view methods to automatically have it run without your needing to listen to the events. Simply declare the methods, and Marionette will make sure they are invoked. Of course, other objects will still need to listen to the events on the view.

DOM Refresh

Back when we discussed Application, I mentioned Region a bit. I won’t get into this much here (once all of the articles about views are done, I’ll go into more detail), but know that a Region is an object that handles the showing and hiding or discarding of views in a particular part of the DOM. Look at the code below to see how to render a view in a Region.


var view = new FooView(); // Assume FooView has already been defined
region.show(view); // Assume the region was already instantiated. Just use "show" to render the view.

When you use show, it will render the view (all view classes that Marionette implements that are based on this base View class will also call the onRender function if you’ve defined it and will fire a render event when render is invoked), attach it to the DOM and then show the view, which simply means that a show event is fired so that components will know that the view was rendered via a Region. After a view has been rendered and then shown, if the view is rendered again, it will trigger a DOM refresh.

This actually isn’t true at the moment because of a bug, but it’s on the developers’ to-do list. Currently, when a view is rendered, it will set a flag saying that it was rendered. Then, when the view is shown, it will set a flag saying that it was shown. The moment when both of these flags have been activated, it will trigger a DOM refresh. Then, any time after that, the DOM refresh will be triggered any time the view is rendered or shown. Keep this in mind if you need to use this functionality.

When a DOM refresh is triggered, first, it will run the onDomRefresh method of the view (if you defined one) and then trigger the dom:refresh event on the view. This is mostly useful for UI plugins (such as jQuery UI, Kendo UI, etc.) with some widgets that depend on the DOM element they are working with being in the actual DOM. Often, when a view is rendered, it won’t be appended into the DOM until after the rendering has finished. This means that you can’t use the plugin during render or in your onRender function.

However, you can use it in onShow (which is invoked just before the show event is triggered) because a Region is supposed to be attached to an existing DOM node (as we’ll see in a future article). Now, since the view has been shown, you will know that the view is in the DOM; so, every time render is called, a DOM refresh will take place immediately after the rendering, and you can call the UI plugin’s functionality safely again.

DOM Triggers

Sometimes, when a user clicks a button, you want to respond to the event, but you don’t want the view to handle the work. Instead, you want the view to trigger an event so that other modules that are listening for this event can respond to it. Suppose you have code that looks like this:


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    events: {
        'click .awesomeButton': 'buttonClicked'
    },
    buttonClicked: function() {
        this.trigger('awesomeButton:clicked', this);
    }
});

The function for handling the click event just triggers an event on the view. Marionette has a feature that allows you to specify a hash of these events to simplify this code. By specifying the triggers property when extending a View, you can assign a hash very similar to the events property; but, instead of giving it the name of one of the view’s methods to invoke, you give it the name of an event to fire. So, we can convert the previous snippet to this:


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    triggers: {
        'click .awesomeButton': ' awesomeButton:clicked '
    }
});

And it’ll do nearly the same thing. There is one major difference between these two snippets: the arguments that are passed to the listening functions. In the first snippet, all we passed to the functions listening for the event was this, which was the view. Using triggers, Marionette will pass a single object with three properties as the argument to each of the functions. These three properties are the following:

  • view A reference to the view object that triggered the event.
  • model A reference to the view’s model property, if it has one.
  • collection A reference to the view’s collection property, if it has one.

So, if you were subscribing to the event from the previous snippet, it would look like this:


// 'view' refers to an instance of the previously defined View type
view.on('awesomeButton:clicked', function(arg) {
    arg.view; // The view instance
    arg.model; // The view's model
    arg.collection; // The view's collection
}

I know there isn’t a surplus of use cases for this, but in the few situations where this applies, it can save plenty of hassle.

DOM Element Caching

Often, this.$el isn’t the only element that you’ll need to directly manipulate. In such cases, many people will do something like this:


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    render: function() {
        this.list = this.$('ul');
        this.listItems = this.$('li');
        . . .
        // Now we use them and use them in other methods, too.
    }
});

Once again, Marionette makes this simpler by converting this all into a simple configuration. Just specify a ui property that contains a hash of names and their corresponding selectors:


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    ui: {
        list: 'ul',
        listItems: 'li'
    }
});

You can access these elements with this.ui.x, where x is the name specified in the hash, such as this.ui.list. This ui property is converted into the cached jQuery objects by the bindUIElements method. If you’re extending Marionette.View, instead of one of the other view types that Marionette offers, then you’ll need to call this method yourself; otherwise, the other view types will call it for you automatically.


Backbone.Marionette.View.extend({ // We don't normally directly extend this view
    ui: {
        list: 'ul',
        listItems: 'li'
        },
    render: function() {
        // render template or generate your HTML, then…
        this.bindUIElements();
        // now you can manipulate the elements
        this.ui.list.hide();
        this.ui.listItems.addClass('someCoolClass');
    }
});

Conclusion

We’ve already seen a plethora of features that Marionette brings to views that cut down on the complexity and amount of code required for common tasks, but we haven’t even touched on the most important part. Marionette.View doesn’t handle any of the rendering responsibilities for us, but Marionette has three other view types that do: ItemView, CollectionView and CompositeView.

These view types, which are what you’ll actually be extending in your code (note the “We don’t normally directly extend this view” comment in all of the code snippets), will take a few minor configuration details and then handle the rest of the rendering for you. We’ll see how this is all done in the next article. For now, ponder all of these features that you’ve been introduced to.

(Front page image credits: nyuhuhuu)

Smashing Editorial (al, il)