Pickable Collections - DATA AND META-DATA - Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

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

PART 3: DATA AND META-DATA

Chapter 11: Pickable Collections

Adding support for picking multiple models is needed for “select all” and “deselect all” Scenarios. Knowing which models have been picked allows action to be taken on the them, or prevented if some criteria is not met for an action. A user interface can enable and disable various actions based on zero, one or multiple items having been picked.

Multi-Pick Collections

To get rolling, add a MultiPickCollection object to Backbone.Picky. This one is be responsible for tracking models that have been picked and handling pick all / unpick all functionality.

The basic object definition looks similar to that of the PickableModel - a constructor function and an extension of the prototype to provide the methods needed.

1 // Constructor function to create MultiPickCollections

2 Backbone.Picky.MultiPickCollection = function(collection){

3 this.collection = collection;

4 this.picked = {};

5 };

In addition to the storage of the collection from which models will be selected, the instance has a picked attribute to hold the list of picked models. This is an object literal attribute, used like a hash - a key/value pair - to make addition and removal of items easier.

The methods on the MultiPickCollection can be assigned to the prototype. Methods don’t need to be assigned to each instance because they work with the state of the instance they are called from. With prototypal inheritance and the way that JavaScript manages the context in which methods are called (the dreaded this operator), calling a prototype method on an instance will in the method working with the instance’ data.

1 _.extend(Backbone.Picky.MultiPickCollection.prototype, {

2

3 pickAll: function(){},

4

5 unpickAll: function(){},

6

7 togglePickAll: function(){}

8

9 });

Each of these method names and parameter lists should provide insight in to what the method does with the collection. Once again, these method names are following the theme of “picking” models with methods for picking all, none or toggling all models in the collection.

So, why not add picked to the list of methods and attributes that are extended on to the prototype? Why add it in the constructor, instead of on the prototype?

Adding picked to the prototype would cause unexpected and unwanted behavior due to the way _.extend works. When an object is extended, the methods and attributes from the extension are copied on to the target. That is, the name of the attribute is assigned to the value of the same attribute, but the assignment is done on the target.

Since the value of each attribute is assigned to the target, assigning the picked attribute to the prototype would result in every instance sharing the same list. In this case, the list shouldn’t be shared. Each instance should have its own list of models. Assigning the attribute in the constructor function instead of on the prototype means every instance gets its own list.

Listening For Model Picks

Picking models can happen one of two ways - through the PickableModel, or through the MultiPickCollection. In either case, the MultiPickCollection needs to be notified so it can keep track of the models that have been picked. The easiest way to handle this is to listen to the “picked” event on the Collection instance. This works because every time a model triggers an event the collection it belongs to also triggers the same event. The collection’s version of the event also includes the model, which means the MultiPickCollection will be able to track the picked models, easily.

In the constructor function for the MultiPickCollection, then, you will need to add an event handler for the collection’s “picked” event. The MultiPickCollection doesn’t know how to listen to events, though. To fix this, use _.extend to add Backbone.Events in to the MultiPickCollection, first.

1 _.extend(Backbone.Picky.MultiPickCollection.prototype, Backbone.Events, {

2 // ... existing methods here

3

4 });

Backbone.Events is a simple object literal in the Backbone code base. It is meant to be used as a mix-in in this manner. By extending the prototype of MultiPickCollection, all instances will be able to the event handler and event triggering methods, as needed.

With that in place, set up the collection’s “picked” event handler to store a reference to the picked model.

1 Backbone.Picky.MultiPickCollection = function(collection){

2 this.collection = collection;

3 this.picked = {};

4

5 // When a model is picked, it triggers a "picked" event.

6 // The owner collection forwards model events, so listen

7 // to the collection's events and capture all picked models

8 this.listenTo(this.collection, "picked", function(model){

9 this.picked[model.cid] = model;

10 });

11 };

The models are stored in the picked list by cid - the “client id” of the model. This id is guaranteed to be unique amongst all model instances. Unfortunately, though, it will be different between two model instances that have the same data - including the same “id” field. A more robust storage mechanism may want to account for this, but this simple “hash” usage of an object literal is good enough for now.

An Object Is Not A Hash

There are some limitations to using an object as a hash. Guillermo Rausch has written about why an object is not a hash and makes some compelling arguments. You should take the time to read this article and understand the limitations that you may run in to, and decide whether or not this technique is right for your application.

Similarly, when a model is “unpicked”, it should be removed from the picked list. Add this event handler directly underneath of the “picked” handler.

1 this.listenTo(this.collection, "unpicked", function(model){

2 delete this.picked[model.cid];

3 });

This will remove the model from the picked list by deleting the reference. Note that this only deletes the reference, and does not do any kind of “delete” API call or anything like that.

The Dangers Of “delete”

You should be aware that using the “delete” keyword tends to put browsers in to “slow mode” for objects. That is, the browser will treat the object differently than normal, as it has to re-examine the ever-changing landscape of the object that is having attributes added and deleted.

In all honesty, though, I wouldn’t worry about this too much. I have never seen this have a noticeable affect on performance, personally. You should, however, measure the performance difference between deleting an attribute and just setting it to null or undefined, in your own applications.

Cleaning Up Event Listeners

In the first part of this book, you wrote View code that used the listenTo method to listen to events. The event handlers in the MultiPickCollection constructor also use this method of listening. Unlike views, though, the MultiPickCollection does not automatically clean up the event handlers for you.

Backbone.View has a remove method that calls stopListening and unbinds the events. To get the same memory management and event cleanup in the MultiPickCollection, then, you’ll want to add a similar method. And once again, naming becomes important.

It’s common among JavaScript frameworks and objects to use destroy or close for these end-of-life method names. The choice often comes down to personal preference and past experience. Developers coming from a .NET and C# background, for example, tend to choose close. Other developers - especially those grounded in JavaScript patterns and idioms - tend to choose destroy. The choice is easier than looking at your own background or preferences, though. Use the semantics and common patterns to pick.

A method name of “destroy” already exists on Backbone.Model. It has the behavioral and semantic meaning of “destroy this model from the data store”. Choosing this name would imply the same behavior and semantics for the MultiPickCollection. This is bad since this object and method will not destroy any data in any backing data store. The choice is clear, then: close.

Add a close method to the MultiPickCollection’s prototype, along side the other instance methods. In this method, call the stopListening method of the instance. This will clean up all of the event listeners that were set up using the listenTo method.

1 _.extend(Backbone.Picky.MultiPickCollection, {

2 // … existing methods

3

4 close: function(){

5 this.stopListening();

6 this.trigger("close");

7 }

8

9 });

In addition to the stopListening call, the close method is triggering a “close” event. This one extra line of code offers a lot of flexibility for applications that are using a MultiPickCollection. Knowing when an object is being closed can often make life easier for memory management, app shut down, and other needs.

listenTo vs on

It should be noted that the listenTo method is not a panacea of solutions for memory management related to events. There are times when it makes more sense to use the on method instead. For more information on when to use which method, see Appendix A: Managing Events As Relationships.

The “pickAll” and “unpickAll” Methods

The pickAll and unpickAll methods loop through the list of models in the collection, and trigger a “picked” or “unpicked” event respectively.

1 pickAll: function(){

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

3 model.trigger("picked");

4 });

5 },

6

7 unpickAll: function(){

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

9 model.trigger("unpicked");

10 });

11 },

Each of these methods iterates the collection and picks or unpicked the model as needed. But wait… notice that these methods trigger the “picked” and “unpicked” events for the models - the same as the PickableModel from Chapter 8. Why duplicate this? Why not use a PickableModel here and not have to duplicate code?

Necessary Duplication… For Now

Let’s say you want to have the MultiPickCollection call the “pick” method for each model instead. How are you going to do that? PickableModel is the object that contains this method, and you don’t have a PickableModel for these models. Sure, you could create one for each of the models as you’re iterating over them. But where would you store them? Or would you just toss them out after they’ve done their job? Or, you could try to find the existing PickableModel instance for each of the models… but where would this come from? You would need some sort of global registry or locator for them. That would likely be more code and trouble than it’s worth.

This is a real problem, honestly. You don’t want to duplicate the code of triggering these events in both the PickableModel and MultiPickCollection. But you also don’t want to have to write crazy-glue-code to make things work properly.

For now, the best answer is to to duplicate the events, unfortunately. Having this duplication (at least for the time being) allows the PickableModel and MultiPickCollection to work independently, but still allows the MultiPickCollection to respond to PickableModel triggered events.

You can create a MultiPickCollection without worrying about whether or not the models in the collection have a PickableModel associated.

1 var myCollection = new MyCollection([/* ... model data ... */]);

2

3 var mpc = new MultiPickCollection(myCollection);

4 mpc.pickAll();

The MultiPickCollection works just fine on its own. If you happen to have a PickableModel for a given model, though, and this PickableModel has its .pick() method called, then the MultiPickCollection instance will still receive the “picked” event and still capture the model as being picked.

1 var m1 = new MyModel();

2 var pm1 = new PickableModel(m1);

3 // ...

4 var mN = new MyModel();

5 var pmN = new PickableModel(mN);

6

7 var myCollection = new MyCollection([m1, /* ... */, mN]);

8 var mpc = new MultiPickCollection(myCollection);

9

10 m1.pick();

In this example, the PickableModel and MultiPickCollection are interacting through the use of the events.

Still, this isn’t the greatest use of code in that it is causing a couple of lines of duplication. In the next chapter, though, methods of integrating the PickableModel and MultiPickCollection will be discussed. This will offer at least one possible solution to this problem - though the cost vs benefit ratio may not be entirely stable.

The “togglePickAll” Method

The togglePickAll method uses a combination of selectAll and deselectAll, based on the current count of picked models, according to these rules:

1. If no models are picked, pick all of them

2. If at least one or more, and less than all models are picked, pick all of them

3. If all models are picked, unpick all of them

The first two rules could easily be combined in to a single rule, but leaving them separated makes the behavior a little more explicit. The code, however, does not have to be as explicit as long as the documentation and/or behavioral specification (i.e. “unit tests”) for this method is explicit.

1 togglePickAll: function(){

2 var pickedLength = _.size(this.picked);

3 var collectionLength = this.collection.length;

4

5 if (pickedLength === collectionLength){

6 this.unpickAll();

7 } else {

8 this.pickAll();

9 }

10 }

Underscore’s size method is used to get the number of picked items stored in the picked attribute. This is the object literal used as a key/value store, and object literals not provide a length attribute. Underscore compensates for this by counting the number of attributes on the objects.

Once the length of picked models is determined, it is checked against the length of the collection. If the two lengths are the same, then that means all models are currently picked - so, unpick all of them. If the two lengths are not the same, then there are unpicked models and they need to be picked.

Handling Removed Models

There’s a problem in the MultiPickCollection, exposed by the togglePickAll method. If a collection has all models picked and one those models is removed from the collection, the list of picked models will still contain the removed model. This will cause the count to be inaccurate, potentially breaking the togglePickAll behavior.

To fix this, the model remove must be accounted for. Add an event handler for the collection’s “remove” event to do this. The event handler should be set up in the constructor, along with the other events.

1 Backbone.Picky.MultiPickCollection = function(collection){

2 // ... existing code

3

4 this.listenTo(this.collection, "remove", function(model){

5 delete this.picked[model.cid];

6 });

7 };

Yes, this is code duplication from the “unpicked” event - good catch. And, fortunately, this is the kind of duplication that can easily be rectified. Add a “_remove” method to the MultiPickCollection and have it delete the model from the picked list. And while you’re at it, add an “_add” method to add a model to the list.

1 _.extend(Backbone.Picky.MultiPickCollection, {

2 // ... existing methods

3

4 _add: function(model){

5 this.picked[model.cid] = model;

6 },

7

8 _remove: function(model){

9 delete this.picked[model.cid];

10 }

11 });

Now update the constructor function to call these methods instead of having the inline callback functions.

1 Backbone.Picky.MultiPickCollection = function(collection){

2 this.collection = collection;

3 this.picked = {};

4

5 this.listenTo(this.collection, "picked", this._add);

6 this.listenTo(this.collection, "unpicked", this._remove);

7 this.listenTo(this.collection, "remove", this._remove);

8 };

The use of an “_” in front of the method name is not anything technical or syntax specific to JavaScript or Backbone applications. This is common notation within JavaScript idioms, though, signifying a “private” method or attribute. That is, when you see a method or attribute that starts with an “_” name, you should treat it like it is private within the object. Methods and attributes named like this are subject to change without notice, because it is expected that you will not touch them directly… ever.

Using A MultiPickCollection

With the MultiPickCollection implemented, an application can easily provide the ability to pick and unpick models in a list. Similar to the PickableView in Chapter 8, a MultiPickView can be created

1 var MyItemView = BBPlug.ItemView.extend({/* … */});

2

3 var MultiPickCollection = BBPlug.CollectionView.extend({

4 itemView: MyItemView,

5

6 initialize: function(){

7 // build the MultiPickCollection

8 this.multiPick = new Backbone.Picky.MultiPickCollection(this.collection);

9 },

10

11 // DOM event handlers

12

13 pickNoneClicked: function(e){

14 e.preventDefault();

15 this.multiPick.unpickAll();

16 },

17

18 pickAllClicked: function(e){

19 e.preventDefault();

20 this.multiPick.pickAll();

21 },

22

23 togglePickAllClicked: function(e){

24 e.preventDefault();

25 this.multiPick.togglePickAll();

26 }

27 });

Since the calls to pickAll, unpickAll and togglePickAll each trigger the correct event, the event handlers for these will be fired at the correct time. The view will be updated with the correct buttons being enabled and disabled, or however the view should handle this situation. This makes for a great reduction in the amount of code that an individual view has to write, to facilitate the “select all” and “select none” functionality that it needs.

Homework: “all”, “none” and “some” Events

You may find yourself wanting to receive events from the MultiPickCollection, telling you when all, some or no models have been picked. As homework for this chapter, work on adding events that will tell you this. Trigger a “picked:all”, “picked:none” or “picked:some” event at the appropriate time, and have the MultiPickView listen to these events and update the UI accordingly.

Lessons Learned

Creating a pickable collection introduces a number of interesting challenges. The behavior and semantics introduced with the PickableModel need to be maintained, but the API and implementation are very different at the same time. Working with the “pickable” theme and examining existing methods names on the underlying objects provided a guide to building this extension.

Adding Events To Your Object

Backbone.Events is a simple object literal that is meant to be used as a mixin for other objects. All of Backbone’s objects mix this in, and your objects can as well. Any time you need the ability to trigger or listen to events in an object, just mix the Backbone.Events in to your object.

Listening To Events

Backbone.Events provides two mechanisms for listening to events: the .on and the .listenTo methods. Understanding when to use which can help lead to better memory management and fewer zombie problems. There is no rule or absolute for which to use when, though. Each situation must be evaluated for the needs of the objects that are involved and the lifecycle of those objects.

Extending A Prototype vs Augmenting An Instance

The use of _.extend to add methods to prototypes is powerful. It allows types to quickly and easily be augmented. But this isn’t always what you want. There are times when each object instance needs to be augmented with data or instance-specific configuration. Adding default and empty values to an instance can be done in a constructor function, ensuring the object instance has the basic configuration that it needs in order to work correctly.

Sometimes Duplication Is OK

There will be times when the desire to keep your code DRY - “Don’t Repeat Yourself” - will cause more problems than it’s worth. The DRY principle is important and duplicating code does tend to lead to problems. But this is not a hard and fast rule. Like any other principle, it is more of a guiding light - a direction that needs to be examined in order to determine its appropriateness.

In the case of duplicating the “picked” and “unpicked” events between PickableModel and MultiPickCollection, the DRY principle was out-weighed by the complexity that would have been added to keep it DRY. Adding layers of complexity and obscurity in API or object requirements is a cost that should be paid in this case.

Iterate For Edge Cases

No matter how hard you try to determine the exact functionality of code before we sit down and write it, you will never get it 100% complete until you start writing it. Business logic and application workflow are often subtle in nature, with edge cases hanging around places that are very unexpected. As code is implemented, these edge cases may present themselves. When this happens, iterate - take the time to evaluate the edge case and determine if it needs to be handled.

In the case of togglePickAll, the edge case of a MultiPickModel having a picked size that is inconsistent with the number of models in the collection came up. This wasn’t noticed until the code in the togglePickAll method was written, though. Once the potential problem was identified, though, additional code was written to account for this. The end result is a more robust MultiPickCollection, capable of handling additional scenarios.

Use Callback Functions, Not Inline Functions

While it is easy and tempting to use nested, inline functions for event handlers, this tends to lead to very bad code. It causes duplication and leads toward the dreaded arrow-of-doom code with nesting so deep, you can’t see how the code flow anymore. Instead of writing inline and nested event handler functions, then, use function reference. Call out to another named function and keep the code flat and well organized.

In the case of the MultiPickCollection, a _remove method handles both “unpicked” and “remove” events of the collection. This reduced both the complexity in the constructor function, and removed code duplication between the event handlers.

Use _methodNames For “Private” Members

JavaScript does not have access modifiers built in to the language at this point. It is possible to create privacy by using function scope to hide variables, but this isn’t always reasonable to do. Often times a method that is attached to an object needs to be considered “private” even though it cannot have an access modifier that enforces this. In this situation, use an “_” as the first character of the method name. This is a common practice in JavaScript to say, “This method should be considered private. Do not call it directly.”