Building A Better CollectionView - BACKBONE VIEWS - Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

PART 1: BACKBONE VIEWS

Chapter 4: Building A Better CollectionView

Splitting apart the concepts of a CollectionView and ModelView opens a number of opportunities for improvements. While there may be some improvements that can be made to the ModelView, the CollectionView stands to gain the most ground by recognizing some of the common use cases and patterns for collections.

Rendering A Collection Of Views

One of the most common scenarios that a CollectionView needs to handle, is the ability to render a list of models in to individual views. This allows for an individual view per model, with each view instance to reference a single model. From this perspective, each view is a ModelView. This means the view definition for the individual models can act as a ModelView and focus it’s code entirely on the single model.

It is not difficult to get this capability in a CollectionView, as is.

1 var MyModelView = BBPlus.ModelView.extend({

2 template: "#some-model-template",

3

4 events: {

5 "click #foo": "fooClicked"

6 },

7

8 fooClicked: function(e){

9 // do stuff with this.model, here

10 }

11 });

12

13 var CollectionOfModelsView = CollectionView.extend({

14 render: function(){

15 var html = [];

16

17 // render a model view for each model

18 // and push the results

19 this.collection.each(function(item){

20 var view = new MyModelView({model: item});

21 html.push(view.render().$el);

22 });

23

24 // populate the collection view

25 // with the rendered results

26 this.$el.html(html);

27

28 return this;

29 }

30 });

This very simple view will loop through every model in it’s collection and render a new instance of MyModelView for that model. Each instance of MyModelView only have a reference to the individual model that it renders. The model views never have to worry about dealing with the collection.

As simple as it is to do this, though, there are a number of problems that this creates. The views are rendered, but what happens when you need to close the parent view? The children need to be closed too, or you’ll end up with zombies. And what happens when you add a new model to the collection, or remove a model from the collection? These should be handled by the CollectionView automatically, to reduce boilerplate code again. There should also be an easy way to specify the model view to use, instead of having it hard coded in to the render method. And lastly, with the list of models being iterated and rendered, it isn’t really necessary to have an implementation of a serializeData method anymore.

API-First Design Of The New CollectionView

Designing the API and usage of the new CollectionView is fairly easy - there isn’t much that needs to be configured. You will want to specify a view type to use for each model, and that’s about it. Anything else will be customization that can come later, as you need it.

1 var MyModelView = BBPlug.ModelView.extend({

2 template: "#some-model-template"

3 });

4

5 var MyCollectionView = BBPlug.CollectionView.extend({

6 modelView: MyModelView

7 });

8

9 var myColView = new MyCollectionView({

10 collection: myCollectionOfStuff

11 });

This usage is simple and clean. It is obvious what you are intending, and the amount of code that you will reduce in view definitions is significant.

information

Collection Views And Templates

Note that there is no need to specify a template for a collection view, since the models each render their own template. Allowing a template on the collection view would add a lot of complexity, too. Where do you render the items, in the template? You have to specify a way to figure that out. And what if the collection view simply doesn’t need a template? Are you forced to specify a template and have it be empty? All these questions, and more, led MarionetteJS to separate the CollectionView from what it calls a CompositeView - named after the composite design pattern, which is used in creating nested and hierarchical structures.

This minimalist API set for a collection view is not without power, though. Since this view type ultimately inherits from Backbone.View, it will still retain all of the capabilities of this base view, and the base BBPlug.View that it directly extends from. This means you will be able to specify the tagName and other attributes of the collection view.

Declaring The ModelView First

There is one very important point to make in the above example, that has nothing to do with the CollectionView API design. You must declare and define the ModelView type before the CollectionView type that uses it. This is due to the way JavaScript performs “variable hoisting” - the process by which JavaScript parsers will move all variable declarations to the top of the function scope, but leave the variable assignment at the location specified in the code.

What this means, in practice, is this code will fail:

1 var MyCollectionView = BBPlug.CollectionView.extend({

2 modelView: MyModelView

3 });

4

5 var MyModelView = BBPLug.ModelView.extend({

6 // ...

7 });

You won’t get a Javascript parsing error with this code, though. You won’t know that there is a problem until runtime, in fact. JavaScript parsers see the MyModelView and MyCollectionView variables on the first pass of parsing, declaring them and making them available from anywhere in the containing function. But the assignment does not happen until the line of code that makes the assignment runs. In this case, MyModelView will be undefined when it is assigned to the modelView attribute. Setting MyModelView to a view definition later, will not cause the CollectionView’s definition to be updated, and you will have an undefined modelView on your hands.

information

Variable Hoisting? Function Scope?

These are some of the most confusing aspects of JavaScript, if you ask me. They are simple rules that are not at all obvious, and can cause a lot of bugs, easily. For more information about these aspects of JavaScript, see my Variable Scope In JavaScript screencast.

Rendering Models With A modelView Setting

Allowing the use of a modelView setting in a view configuration is the most simple of the needed changes. Instead of hard coding the view type in the render method, create a method called getModelView and have that method retrieve the view type to create, for each model. Call that method from the render method, inside of the loop for rendering each model.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2

3 // a method to get the type of view for

4 // each model. this method can be overridden

5 // to return a different view type based on

6 // attributes of the model passed in

7 getModelView: function(model){

8 return this.modelView;

9 },

10

11 render: function(){

12 var html = [];

13

14 // render a model view for each model

15 // and push the results

16 this.collection.each(function(item){

17

18 // get the view type to use

19 var ViewType = this.getModelView(item);

20

21 var view = new ViewType({model: item});

22 view.render();

23 html.push(view.$el);

24 }, this);

25

26 // populate the collection view

27 // with the rendered results

28 this.$el.html(html);

29

30 return this;

31 }

32

33 });

The getModelView method is a little odd, at this point. It takes in a parameter of model but it never uses that parameter. This is done because there will be a day when you want to change the type of view that is used, based on some data found within the model. For example, you may be doing something with social media, and you might want to have models with data that come from Twitter, Facebook, MySpace, or whatever out-dated and irrelevant service was popular at the time this book was written. Having the ability to pick which view type is used for rendering means you can handle each of these networks differently.

The one drawback to allowing the getModelView function to change the view type based on the model, is that you have to call this function for every model in the collection. This adds a little overhead in comparison to calling it once and only once, per render. There is little chance that this will have a noticeable impact on your application’s performance, though. Unless you’re rendering thousands of items, which would be a performance problem in so many other ways, you are likely not going to have any issues with this.

Render A New View On Model Add

When a model is added to the underlying collection, a new view instance should be rendered and added to the DOM. A simple default implementation of this would just append the new view instance to the end of the existing list.

Listen To The Collection “add” Event

Add a constructor function in the CollectionView definition, and have it apply the base view’s constructor to itself. This ensures the inheritance chain is set up properly. Now add a handler for the “add” event of the view’s collection, to the constructor. This handler should call a modelAddedmethod.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2

3 constructor: function(options){

4 BBPlug.BaseView.call(this, options);

5

6 this.listenTo(this.collection, "add", this.modelAdded);

7 },

8

9 // ...

10

11 });

The modelAdded method needs to do a few things: get the view type to use, create an instance of it, render the view, and populate the HTML of the CollectionView with the child view’s HTML. If this sounds familiar, it should. This is exactly what the render method currently does. Only, therender method does it for all models in the collection and you only want it to happen for the new model, in this case.

Extracting renderModel And Calling It From modelAdded

There’s a redundant code problem at this point. Both the render method and modelAdded are doing the same basic rendering process. To combat that, extract the code that works on the individual models from the render method and put it in to its own method. Then you can call this new method from both render and modelAdded.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2

3 constructor: function(options){

4 BBPlug.BaseView.call(this, options);

5

6 this.listenTo(this.collection, "add", this.modelAdded);

7 },

8

9 getModelView: function(model){

10 return this.modelView;

11 },

12

13 // event handler for model added to collection

14 modelAdded: function(model){

15 var view = this.renderModel(model);

16 this.$el.append(view.$el);

17 },

18

19 // render a single model

20 renderModel: function(model){

21 var ViewType = this.getModelView(item);

22 var view = new ViewType({model: item});

23 view.render();

24 return view;

25 },

26

27 // render the entire collection

28 render: function(){

29 var html = [];

30

31 // render a model view for each model

32 // and push the results

33 this.collection.each(function(model){

34 var view = this.renderModel(model);

35 html.push(view.$el);

36 }, this);

37

38 // populate the collection view

39 // with the rendered results

40 this.$el.html(html);

41

42 return this;

43 }

44 });

Now when you .add a model to the collection that this view is holding, it will be rendered and show up in the DOM.

Remove A View On Model Remove

A model can be added to the collection, and one can also be removed from the collection. When a model is removed from the collection, the view that holds it and renders it should also be removed. It may be tempting to put a “remove” event handler in a model view and have that view close itself. This creates code duplication, though, as every model view type would have to implement this feature. That means every developer implementing a model view for a collection view would have to know about this requirement. This is boilerplate code that shouldn’t have to be written more than once, though. The best place to put it, then, is in the CollectionView.

Holding View References

In order to remove a view for a given model, you will need to hold a reference to view for each model in the collection. Modify the renderModel method to handle this, adding the view instance to an object that acts as storage, using the model’s cid (client-side id) as the key. Be sure to create a new instance of this storage property in the constructor of the CollectionView, so that each instance has it’s own.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2

3 constructor: function(options){

4 BBPlug.BaseView.call(this, options);

5

6 // set up storage for views

7 this.children = {};

8

9 // listen to collection events

10 this.listenTo(this.collection, "add", this.modelAdded);

11 },

12

13 // ...

14

15 // render a single model

16 renderModel: function(model){

17 var ViewType = this.getModelView(item);

18 var view = new ViewType({model: item});

19

20 // store the child view for this model

21 this.children[model.cid] = view;

22

23 view.render();

24 return view;

25 },

26

27 // ...

28

29 });

Now you can access the view instance for any given model instance, by looking up the view from the children container.

Removing The View On Model Remove

Add another event handler to the constructor function, this time handling the “remove” event. The method that handles the event should look up the view instance and remove that view from the DOM, ensuring it is closed appropriately.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2

3 constructor: function(options){

4 BBPlug.BaseView.call(this, options);

5

6 // set up storage for views

7 this.children = {};

8

9 // listen to collection events

10 this.listenTo(this.collection, "add", this.modelAdded);

11 this.listenTo(this.collection, "remove", this.modelRemoved);

12 },

13

14 // ...

15

16 // handle removing an individual model

17 modelRemoved: function(model){

18 // guard clause to make sure we have a model

19 if (!model){ return; }

20

21 // guard clause to make sure we have a view

22 var view = this.children[model.cid];

23 if (!view){ return; }

24

25 // remove the view, if the method is there

26 if (_.isFunction(view.remove)){

27 view.remove();

28 }

29

30 // remove it from the children

31 this.children[model.cid] = undefined;

32 },

33

34 // ...

35 });

The modelRemoved method has two guard clauses in it. The first one checks to see if the method received a model. If it didn’t, there is no point in trying to do anything so, so exit. The second guard checks to see if a view was found for the model’s cid. If a view was not found, there is no point in trying to do anything else, so exit. Finally, the view object that is found must have a remove method on it. This method is built in to Backbone.View, so every view instance that is used will have this method available. Objects that are not views, though, or views that have been modified and have this method missing, can’t be removed properly.

When a model is removed from the collection, the modelRemoved method will fire and the view for that model will shut down and be removed from the DOM.

Close All The Children With The Parent

Closing a single view is good, but the entire collection of views also needs to be closed when the parent view is closed. Fortunately you’ve already set up most of the logic to do this.

Adding A closeChildren Method

You’ll need a method to iterate and close all existing views. This method will loop over all the views stored in the children reference. This method can either close the child view directly, or remove the model from the collection, allowing the “remove” event on the collection to be handled.

As tempting as it may be to re-use the existing logic of removing a model, this would be a very bad idea. Closing the collection view is not the same as removing a model from a collection. If there are other parts of the application that are using that same collection, removing the model from the collection would potentially break those other areas.

Instead, this method should just close the views, ensuring they are torn down properly. Most of the code you need for this is already found in the modelRemoved event handler, though.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2

3 // ...

4

5 // handle removing an individual model

6 modelRemoved: function(model){

7 // guard clause to make sure we have a model

8 if (!model){ return; }

9

10 // guard clause to make sure we have a view

11 var view = this.children[model.cid];

12 this.closeChildView(view);

13 },

14

15 // a method to close an individual view

16 closeChildView: function(view){

17 if (!view){ return; }

18

19 // remove the view, if the method is there

20 if (_.isFunction(view.remove)){

21 view.remove();

22 }

23

24 // remove it from the children

25 this.children[model.cid] = undefined;

26 },

27

28 // close and remove all children

29 closeChildren: function(){

30 var children = this.children || {};

31 _.each(children, function(child){

32 this.closeChildView(child);

33 }, this);

34 }

35

36 });

Both the closeChildren and modelRemoved methods take advantage of the new closeChildView method, now. This keeps the code consistent and re-usable, making it easy to see what’s going on as well.

Override remove To Close The Children

With the ability to close all of the children, now, the parent CollectionView should do this when it is being closed and removed from the DOM.

Override the remove method on the CollectionView, and have it call the closeChildren method.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2 // ...

3

4 // override remove and have it

5 // close all the children

6 remove: function(){

7 BBPlug.BaseView.prototype.remove.call(this);

8 this.closeChildren();

9 }

10 });

Pay attention to the order in which things are removed from the DOM, in this case. The CollectionView itself is removed first, and then the children are removed. This has a net effect of the entire collection of views being removed from the DOM all at once. When the CollectionView is removed, it will take all of it’s children with it, from the DOM. But you still need to call .remove on all of the children anyways - not the clean up the DOM, but to clean up all of the event handlers and other code that may have been added to the child views’ remove method.

The reason for this order is performance. If you were to remove all of the individual views, first, there’s a high likelihood of causing too many changes in the DOM, slowing down the perceived performance of the app. By removing the collection view first, all of the views are gone all at once, and then they can be cleaned up out of memory, which is a much faster thing to do.

Re-Render The Entire List On Reset

The last feature that the CollectionView needs, is the ability to wipe out the entire list of child views, and re-render the whole thing. And there are two scenarios where this needs to happen:

1. When some code call .render directly on the CollectionView instance

2. When the collection is .reset with a new set of models

The first scenario doesn’t need much to make it work. At the beginning of the render method, all of the existing children can be closed using the .closeChildren method.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2 // ...

3

4 render: function(){

5 this.closeChildren();

6

7 // ...

8 },

9

10 // ...

11 });

The closeChildren method already handles for a scenario where there are no views in the children list. Calling the render method for the first time, then, will simply skip right past most of the code to close child views because there aren’t any to close.

The second scenario of covering a reset event is equally as simple a change. You only need to listen for the “reset” event on the collection instance, and call render when it happens.

1 BBPlug.CollectionView = BBPlug.BaseView.extend({

2 // ...

3

4 constructor: function(options){

5 // ...

6

7 this.listenTo(this.collection, "reset", this.render);

8 },

9

10 // ...

11 });

Since the render function already handles closing any existing children and then re-rendering the list entirely, there isn’t any additional code needed for the “reset” event. If you do find some scenarios that need additional code for handling the reset event, though, you can add a separate event handler method for it. Just be sure to call .render() when the time comes to re-render the new list.

warning

Didn’t We Just Avoid Remove All View Individually?!

Yes, unfortunately, this code to close all views at the beginning of the render method or in the reset event handler may run in to the very same performance problem that was avoided in the remove method, of having too many DOM changes happening. There may be ways around this, but that would be a performance optimization outside of this scope. For more information on performance optimizations, I recommend Nicholas Zakas’ book on High Performance JavaScript.

With that, the core of the new CollectionView type is done. You can now handle rendering a Backbone.Collection with a new view instance for every model. It handles adding, removing, re-rendering and resetting the underlying collection.

information

A Better Example Use

For a great use of this style of collection, check out the chapter on rendering a filtered collection at the end of this book. It walks you through the core possibilities for handling what looks like two simple tasks, showing you how these two things combined can create a very unexpected journey.

View-Per-Model vs Iterating Models In A Template

This new version of the CollectionView is more flexible and more capable than the original version. However, this comes at a cost. There is more code that has to be run and more maintenance for that code. There are also times when the view-per-model pattern that is introduced here will fall apart.

Take the example of a <select> list in HTML. If you want to generate a <select> from a CollectionView now, you will run in to trouble. Sure, you can set the tagName of the CollectionView to select, but what about the children? Assuming you want each model to represent an <option> tag in the select, you will need to provide a value on the option tag for this to be useful. You can set the tagName of the model view to option, but getting the value attribute on the view’s $el will prove somewhat challenging. It would be easier, in this scenario, to iterate through the list of models in the template for a single view. The iteration of models could easily use an HTML template to build the correct option tag with the right value attribute.

But neither this new version of the CollectionView, nor the old version of the CollectionView is the right way to do things all the time. Both of these patterns are important and you should use the one that fits the scenario at hand instead of blindly applying one or the other, trying to force it to fit when it won’t.

information

Get The Model For The Clicked Element

For more information about the two variations of rendering a collection, see my blog post on how to get the model for the clicked DOM element.

Lessons Learned

There are often multiple solutions to the same problem, and understanding which solution fits the current situation is critical. Gaining that understanding is no trivial task, though. It often involves experimenting and becoming familiar with the failures of each solution. This means you will need to set out with the goal of not only failing, but being able to understand why the failure occurred, and under which circumstances these specific failures could be avoided.

Cleaning Up Children

Handling child views seems like a generally simple task at first. But without proper cleanup of the children, a lot of problems can be introduced. Memory leaks and zombie event handlers can run rampant, and cause all kinds of bugs. Keeping a list of child views and having them closed when the parent view is closed provides a simple way of cleaning up the children and preventing these types of problems.

Order Of Variable Declaration Matters

JavaScript can be an odd language at times, and the idea of “variable hoisting” is certainly one of those times. Declaring the variables and setting the value of those variables in the right order becomes important in JavaScript, because of this.

You don’t want to declare the CollectionView and assign the modelView attribute before the model view type is declared. Doing this will cause the modelView to be undefined because the variable will have been declared, due to hoisting, but not be assigned a value yet. Always define the model view before the collection view. The same can be said for any JavaScript objects where one needs to reference the other.

Not All Collections Need A View-Per-Model

There are times when a model needs it’s own view and there are times when the entire collection should be rendered in to a single view. Having options is important so that you can make the choice for any given situation.