(For more resources related to this topic, see here.)
Marionette.View and Marionette.ItemView
The Marionete.View extends the Backbone.View, and it's important to remember this, because all the knowledge that we already have on creating a view will be useful while working with these new set of views of Marionette.
Each of them aims to provide a specific out of the box functionality so that you spend less time focusing on the glue code needed to make things work, and more time on things that are related to the needs of your application. This allows you to focus all your attention on the specific logic of your application.
We will start by describing the Marionette.View part of Marionette, as all of the other views extend from it; the reason we do this is because this view provides a very useful functionality. But it's important to notice that this view is not intended to be used directly. As it is the base view from which all the other views inherit from, it is an excellent place to contain some of the glue code that we just talked about.
A good example of that functionality is the close method, which will be responsible for removing .el from DOM. This method will also take care of calling unbind to all your events, thus avoiding the problem called Zombie views. This an issue that you can have if you don't do this carefully in a regular Backbone view, where new instantiations of previously closed fire events are present. These events remain bound to the HTML elements used in the view. These are now present again in the DOM now that the view has been rerendered, and during the recreation of the view, new event listeners are attached to these HTML elements.
From the documentation of the Marionette.View, we exactly know what the close method does.
It calls an onBeforeClose event on the view, if one is provided
It calls an onClose event on the view, if one is provided
It unbinds all custom view events
It unbinds all DOM events
It removes this.el from the DOM
It unbinds all listenTo events
The link to the official documentation of the Marionette.View object is https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md.
It's important to mention that the third point, unbind all custom view events, will unbind events created using the modelEvents hash, those created on the events hash, and events created via this.listenTo.
As the close method is already provided and implemented, you don't need to perform the unbind and remove previously listed tasks. While most of the time this would be enough, at times, one of your views will need you to perform extra work in order to properly close it; in this case, two events will be fired at the same time to close a view.
The event onBeforeClose, as the name indicates, will be fired just before the close method. It will call a function of the same name, onBeforeClose, where we can add the code that needs to be executed at this point.
function : onBeforeClose () {
// code to be run before closing the view
}
The second event will be onClose, which will be fired after the close method so that the .el of the view won't be present anymore and all the unbind tasks will have been performed.
function : onClose () {
// code to be run after closing the view
}
One of the core ideas behind Marionette is to reduce the boilerplate code that you have to write when building apps with Backbone. A perfect example of which is the render method that you have to implement in every Backbone view, and the code there is pretty much the same in each of your views. Load the template with the underscore _.template function and then pass the model converted to JSON to the template.
The following is an example of repetitive code needed to render a view in Backbone:
render : function () {
var template = $( '#mytemplate' ).html();
var templateFunction = _.template( template );
var modelToJSON = this.model.toJSON();
var result = templateFunction(modelToJSON);
var myElement = $( '#MyElement' );
myElement.html( result );
}
As Marionette defining a render function is no longer required, just like the close method, the preceding code will be called for you behind the scenes. In order to render a view, we just need to declare it with a template property set.
var SampleView = Backbone.Marionette.ItemView.extend({
template : '#sample-template'
});
Next, we just create a Backbone model, and we pass it to the ItemView constructor.
var SampleModel = Backbone.Model.extend({
defaults : {
value1 : "A random Value",
value2 : "Another Random Value"
}
})
var sampleModel = new SampleModel();
var sampleView = new SampleView({model:sampleModel);
And then the only thing left is to call the render function.
sampleView.render();
If you want to see it running, please go through this JSFiddle that illustrates the previous code:
http://jsfiddle.net/rayweb_on/VS9hA/
One thing to note is that we just needed one line to specify the template, and Marionette did the rest by rendering our view with the specified template. Notice that in this example, we used the ItemView constructor; we should not use Marionette.View directly, as it does not have many functionalities of its own. It just serves as the base for other views.
So some of the following examples of the functionalities provided by Marionette.View will be demonstrated using ItemView, as this view inherits all of these functionalities through extension.
As we saw in the previous example, ItemView works perfectly for rendering a single model using a template, but what about rendering a collection of models?
If you just need to render, for example, a list of books or categories, you still can use ItemView. To accomplish this, the template that you would assign to ItemView must know how to handle the creation of the DOM to properly display that list of items.
Let's render a list of books.
The Backbone model will have two properties: the book name and the book ID. We just want to create a list of links using the book name as the value to be displayed; the ID of the book will be used to create a link to see the specific book.
First, let's create the book Backbone model for this example and its collection:
var BookModel = Backbone.Model.extend({
defaults : {
id : "1",
name : "First",
}
});
var BookCollection = Backbone.Collection.extend({
model : BookModel
});
Now let's instantiate the collection and add three models to it:
var bookModel = new BookModel();
var bookModel2 = new BookModel({id:"2",name:"second"});
var bookModel3 = new BookModel({id:"3",name:"third"});
var bookCollection = new BookCollection();
bookCollection.add(bookModel);
bookCollection.add(bookModel2);
bookCollection.add(bookModel3);
In our HTML, let's create the template to be used in this view; the template should look like the following:
<script id="books-template" type="text/html">
<ul>
<% _.each(items, function(item){ %>
<li><a href="book/'+<%= item.id %> +"><%= item.name %> </li>
<% }); %>
</ul>
</script>
Now we could render the book list using the following code snippet:
var BookListView = Marionette.ItemView.extend({
template: "#books-template"
});
var view = new BookListView ({
collection: bookCollection
});
view.Render();
If you want to see it in action, go to the working code in JSFiddle at http://jsfiddle.net/rayweb_on/8QAgQ/.
The previous code would produce an unordered list of books with links to the specific book. Again, we gained the benefit of writing very little code once again, as we didn't need to specify the Render function, which could be misleading, because the ItemView is perfectly capable of rendering a model or a collection. Whether to use CollectionView or ItemView will depend on what we are trying to accomplish. If we need a set of individual views with its own functionality, CollectionView is the right choice, as we will see when we get to the point of reviewing it. But if we just need to render the values of a collection, ItemView would be the perfect choice.
Handling events in the views
To keep track of model events or collection events, we must write the following code snippet on a regular Backbone view:
this.listenTo(this.model, "change:title", this.titleChanged);
this.listenTo(this.collection, "add", this.collectionChanged);
To start these events, we use the following handler functions:
titleChanged: function(model, value){alert("changed");},
collectionChanged: function(model, value){alert("added");},
This still works fine in Marionette, but we can accomplish the same thing by declaring these events using the following configuration hash:
modelEvents: {
"change:title": "titleChanged"
},
collectionEvents: {
"add": "collectionChanged"
},
This will give us exactly the same result, but the configuration hash is very convenient as we can keep adding events to our model or collection, and the code is cleaner and very easy to follow.
The modelEvents and collectionEvents are not the only configuration hash sets that we have available in each one of the Marionette views; the UI configuration hash is also available. It may be the case that one of the DOM elements on your view will be used many times to read its value, and doing this using jQuery can not be optimal in terms of performance. Also, we would have the jQuery reference in several places, repeating ourselves and making our code less DRY.
Inside a Backbone view, we can define a set of events that will be fired once an action is taken in the DOM; for instance, we pass the function that we want to handle in this event at the click of a button.
events : {
"click #button2" : "updateValue"
},
This will invoke the updateValue function once we click on button2. This works fine, but what about calling a method that is not inside the view?
To accomplish this, Marionette provides the triggers functionality that will fire events which can be listened to outside of your view. To declare a trigger, we can use the same syntax used in the events object as follows:
triggers : { "click #button1": "trigger:alert"},
And then, we can listen to that event somewhere else using the following code:
sampleView.on("trigger:alert", function(args){
alert(args.model.get("value2"));
});
In the previous code, we used the model to alert and display the value of the property, value2.
The args parameter received by the function will contain objects that you can use:
The view that fired the trigger
The Backbone model or collection of that view
UI and templates
While working with a view, you will need a reference to a particular HTML element through jQuery in more than one place in your view. This means you will make a reference to a button during initialization and in few other methods of the view. To avoid having the jQuery selector duplicated on each of these methods, you can map that UI element in a hash so that the selector is preserved. If you need to change it, the change will be done in a single place.
To create this mapping of UI elements, we need to add the following declaration:
ui: {
quantity: "#quantity"
saveButton : "#Save"
},
And to make use of these mapper UI elements, we just need to refer them inside any function by the name given in the configuration.
validateQuantity: function() {
if (this.ui.quantity.val() > 0 {
this.ui.saveButton.addClass('active');
}
}
There will be times when you need to pass a different template to your view. To do this in Marionette, we remove the template declaration and instead add a function called getTemplate.
The following code snippet would illustrate the use of this function:
getTemplate: function(){
if (this.model.get("foo"){
return "#sample-template";
}else {
return "#a-different-template";
}
},
In this case, we check the existence of the property foo; if it's not present, we use a different template and that will be it. You don't need to specify the render function because it will work the same way as declaring a template variable as seen in one of the previous examples.
If you want to learn more about all the concepts that we have discussed so far, please refer to the jsFiddle link: http://jsfiddle.net/rayweb_on/NaHQS/.
If you find yourself needing to make calculations involving a complicated process while rendering a value, you can make use of templeteHelpers that are functions contained in an object called templateHelpers. Let's look at an example that will illustrate its use better.
Suppose we need to show the value of a book but are offering a discount that we need to calculate, use the following code:
var PriceView = Backbone.Marionette.ItemView.extend({
template: "#price-template",
templateHelpers: {
calculatePrice: function(){
// logic to calculate the price goes here
return price;
}
}
});
As you can see the in the previous code, we declared an object literal that will contain functions that can be called from the templates.
<script id="my-template" type="text/html">
Take this book with you for just : <%= calculatePrice () %>
</script>
Marionette.CollectionView
Rendering a list of things like books inside one view is possible, but we want to be able to interact with each item. The solution for this will be to create a view one-by-one with the help of a loop. But Marionette solves this in a very elegant way by introducing the concept of CollectionView that will render a child view for each of the elements that we have in the collection we want to display.
A good example to put into practice could be to list the books by category and create a Collection view. This is incredible easy.
First, you need to define how each of your items should be displayed; this means how each item will be transformed in a view.
For our categories example, we want each item to be a list <li> element and part of our collection; the <ul> list will contain each category view.
We first declare ItemView as follows:
var CategoryView = Backbone.Marionette.ItemView.extend({
tagName : 'li',
template: "#categoryTemplate",
});
Then we declare CollectionView, which specifies the view item to use.
var CategoriesView = Backbone.Marionette.CollectionView.extend({
tagName : 'ul',
className : 'unstyled',
itemView: CategoryView
});
A good thing to notice is that even when we are using Marionette views, we are still able to use the standard properties that Backbone views offer, such as tagName and ClassName.
Finally, we create a collection and we instantiate CollectionView by passing the collection as a parameter.
var categoriesView = new CategoriesView({collection:categories);
categoriesView.render();
And that's it. Simple huh?
The advantage of using this view is that it will render a view for each item, and it can have a lot of functionality; we can control all those views in the CollectionView that serves as a container.
You can see it in action at http://jsfiddle.net/rayweb_on/7usdJ/.
Marionette.CompositeView
The Marionette.Composite view offers the possibility of not only rendering a model or collection models but, also the possibility of rendering both a model and a collection. That's why this view fits perfectly in our BookStore website. We will be adding single items to the shopping cart, books in this case, and we will be storing these books in a collection. But we need to calculate the subtotal of the order, show the calculated tax, and an order total; all of these properties will be part of our totals model that we will be displaying along with the ordered books.
But there is a problem. What should we display in the order region when there are no items added? Well, in the CompositeView and the CollectionView, we can set an emptyView property, which will be a view to show in case there are no models in the collection. Once we add a model, we can then render the item and the totals model.
Perhaps at this point, you may think that you lost control over your render functionality, and there will be cases where you need to do things to modify your HTML. Well, in that scenario, you should use the onRender() function, which is a very helpful method that will allow you to manipulate the DOM just after your render method was called.
Finally, we would like to set a template with some headers. These headers are not part of an ItemView, so how can we display it?
Let's have a look at part of the code snippet that explains how each part solves our needs.
var OrderListView = Backbone.Marionette.CompositeView.extend({
tagName: "table",
template: "#orderGrid",
itemView: CartApp.OrderItemView,
emptyView: CartApp.EmptyOrderView,
className: "table table-hover table-condensed",
appendHtml: function (collectionView, itemView) {
collectionView.$("tbody").append(itemView.el);
},
So far we defined the view and set the template; the Itemview and EmptyView properties will be used to render our view.
The onBeforeRender is a function that will be called, as the name indicates, before the render method; this function will allow us to calculate the totals that will be displayed in the total model.
onBeforeRender: function () {
var subtotal = this.collection.getTotal();
var tax = subtotal * .08;
var total = subtotal + tax;
this.model.set({ subtotal: subtotal });
this.model.set({ tax: tax });
this.model.set({ total: total });
},
The onRender method is used here to check whether there are no models in the collection (that is, the user hasn't added a book to the shopping cart). If not, we should not display the header and footer regions of the view.
onRender: function () {
if (this.collection.length > 0) {
this.$('thead').removeClass('hide');
this.$('tfoot').removeClass('hide');
}
},
As we can see, Marionette does a great job offering functions that can remove a lot of boilerplate code and also give us full control over what is being rendered.
Summary
This article covered the introduction and usage of view types that Marionette has. Now you must be quite familiar with the Marionette.View and Marionette.ItemView view types of Marionette.
Resources for Article:
Further resources on this subject:
Mobile Devices [Article]
Puppet: Integrating External Tools [Article]
Understanding Backbone [Article]
Read more