Chapter 3: Views

So we’ve gotten the big guys — Application and Module — taken care of, so now we’ll get into the bits you can see: views. Backbone already has views, but they really don’t do very much for you. Marionette fills in the feature gaps, so you can skip over the vast amounts of boilerplate code, and avoid the pitfalls you could run into if you don’t know what to look out for. So let’s take a gander at what Marionette provides.

Event Binding

Up until recently, Backbone views were often mishandled, causing a horrible problem known as zombie views. The issue 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 didn’t stop listening to the events on the model, which meant 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, even though it wouldn’t render 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 fixed this problem themselves (finally!) when they released Backbone 1.0, by adding the listenTo and stopListening methods to Events, which Backbone’s View class inherits some methods 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’s base View class gives us a couple properties to use when extending Marionette’s views: modelEvents and collectionEvents. Simply pass in an object hash 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(s) to call when that event is triggered. Look at this simple example:

Marionette.View.extend({ // We don't normally directly extend this view
    modelEvents: {
        'change:attribute': 'attributeChanged render', // call 2 functions
        '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.

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). Second, 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:

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

This isn’t a good way of doing this, but the option is there if you absolutely need it. Also, instead of simply assigning an object literal to modelEvents or collectionEvents, you may also assign a function. The function will need to return an object hash that has the events and callbacks.

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

    modelDestroyed: function(){}
});

There are very few reasons that you would need to use modelEvents like this, but it will sure come in handy when that occasion arises.

As often as possible, the modelEvents and collectionEvents feature follows the pattern that Backbone and Marionette use: 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 a couple chapters, 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 — assuming they were set up using listenTo. 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, in addition to calling Backbone’s remove function for you.

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 triggered.

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 needing to set up listeners for these 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 (they will be covered in detail in chapter 7), 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, 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. (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.) 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 developer’s to-do list, and I’ll work with him to fix it if we can. 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 when the view is rendered or shown. Keep this in mind if you need to use this functionality. This will be essentially the same in most circumstances, but it may cause the DOM refresh event to fire twice if a view is shown again (perhaps in a different region).

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.) because some widgets depend on the DOM element they are working with to already exist in the actual DOM. Often, when a view is rendered, it won’t be appended to the DOM until after the rendering has finished. This means that you can’t initialize the widget 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 chapter 7). 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 listening for this event can respond to it. Suppose you have code that looks like this:

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:

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 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
}

As with many of the Marionette features, there isn’t a surplus of use cases for this, but in the few situations where this applies, it makes things much simpler.

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.

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');
    }
});

Summary

We’ve already seen many 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 extend 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. The next chapter is about ItemView and then the next chapters will cover CollectionView and CompositeView.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset