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:
- A Thorough Introduction To Backbone.Marionette (Part 1)
- A Thorough Introduction To Backbone.Marionette (Part 2)
- Backbone.js Tips And Patterns
- An Introduction To Full-Stack JavaScript
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’smodel
property, if it has one.collection
A reference to the view’scollection
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)