Pickable Models - 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

While views are often the first place that developers see boilerplate code, they are certainly not the last. Nearly every type that Backbone provides will have some form of boilerplate applied to it.

For example, many applications have a model or list of models that can be “selected” from a user interface. The app may need need to show a list of items - often in a table or grid layout - with a checkbox next to each item and have the app user select as many or as few of the items as needed, by checking the boxes. With at least one items selected, they can then perform some action on the selected items. This may be as simple as a bulk-delete, or may be more complex and specific to the application being created.

Part 2 will cover the creation of a plugin that allows models to be select, or “picked”. Collections will also be able to track the models that have been picked, as well. Finally, a method of mixing the functionality in to models and collections that doesn’t cause issues with method or attribute name collisions will be shown.

information

Theme And Variation

Naming things can be difficult and cause problems far beyond the actual name. Names often set a theme or set of expectations around the language used with the thing being named. In the first version of Part 2 of this book, a theme of “select” was chosen. This proved by be a bad idea for reasons described in Appendix E: Theme And Variation. In the end, the theme of “pick” was chosen to correct this set of problems.

Chapter 10: Pickable Models

A model should know whether or not it has been picked. The model should also provide methods for picking itself, and unpicking. Having this knowledge and these methods directly on the model reduces the amount of code in the views that are displaying the models. It also properly encapsulates the logic of picking / unpicking on to the objects that are being modified. Rather than keeping all of the data and logic surrounding the selection in the view instance, the view only needs to react to changes in the model’s state. The view can also call the appropriate method on the model based on user interaction with the checkbox on the view.

Options For Storing Picked State

There are a few options for storing the state of selection on a model: as part of the model’s attributes, as data attached directly to the model (outside of the attributes), or in a third party object with no data stores on the model directly.

The first option involves using the model’s get and set methods to store and retrieve the state of selection. The advantage of this is the existing storage mechanism for the selection state. Changing this state will also trigger “change” events that can be used by views or any other objects. The result is a reduction in the amount of code that needs to be written because the model is using existing functionality to facilitate the “picked” state. The downside to this is the data being stored with the model’s attributes. This might not be a problem if this state can be ignored by anything reading the data. However, the data is still there in the attributes. It will be sent to a back-end server when the model is saved, and it will be part of the model’s attributes for as long as the model is in memory.

The second option involves a little more code: store the state directly on the model and not in the attributes. The benefit is that the events can be customized to a higher degree, and the extra data is not found in the model’s attributes or .toJSON() call. The selection state is not sent to the server because its not part of the attributes. The model is not restricted to using “change” events, either. It can trigger “picked” and “unpicked” events, or any other event name desired. The downside is that there is more code to write. Methods to manage picking the models, triggering events, and storing the state will have to be added to the model.

The third option is a variation of the second, but requires more code, still. Creating a separate object to house the state of model selection can provide some benefit, though. It keeps the model clean of the selection state. It also allows any model to be selected or “picked”, whether or not it was originally designed for this purpose. The major downside is two-fold: the state is not stored in the model, and the methods to manipulate the state are not found on the model.

The choice between less code + attribute pollution, or more code without attribute pollution is not always clear. For this example, the idea of polluting the attributes seems to be the worse of the two, though. Having extra data hanging around the model’s attributes, is generally a bad idea. It pollutes the models with information specific to a single user experience or UI feature. The allure of being able to select any model - whether or not it was set up for this purpose - is great, as well. The creation of a plugin to manage the selection state is an option to reduce the overall boilerplate code for managing model selection. The problem of allowing a model to manage that state is solvable as well, and will be examined in chapter 10.

Backbone.Picky Namespace

To add the selection ability to a model or collection - or, in other words, to allow a user to pick which items they want - a new plugin will be created, and called “Backbone.Picky”. Naming the plugin with “Backbone.” is quite common. This convention gives any others developers using the plugin a clear understanding that this plugin is meant to be used with Backbone.

To get started, a namespace will be created for the new plugin.

1 Backbone.Picky = {};

This will serve as the namespace that all of the selectable model and collection objects and functionality will hang from.

The assumption is Backbone being an available object is fairly safe at this point. It does add a requirement of Backbone being loaded prior to this plugin, but that again is a safe assumption.

Picky.PickableModel

Rather than extending from Backbone.Model directly, a separate object will be added to the Picky namespace. This object will contain all of the functionality and data for the selectable models.

1 Backbone.Picky.PickableModel = function(model){

2 this.model = model;

3 };

This constructor function will create instances of the PickableModel object, with each instance managing the the model instance that is passed to it.

1 var myModel = new MyModel();

2 var pickable = new Backbone.Picky.PickableModel(myModel);

The PickableModel needs methods to pick and unpick the model, as well. As a simple first implementation, these methods only need to set a picked attribute on the model.

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

2

3 pick: function(){

4 this.model.picked = true;

5 },

6

7 unpick: function(){

8 this.model.picked = false;

9 }

10

11 });

_.extend

The use of _.extend is a shortcut to add multiple methods and/or attributes to an existing object. It takes the first parameter as the target object and copies all of the methods and attributes from the remaining parameters to it.

When dealing with prototypes, this will make the method available to every instance of the type.

But these method implementations are very naive. There is more behavior that needs to be accounted for when picking or unpicking a model.

Picking a model not only needs to set the correct state, but also trigger an event from the model that says the model has been picked. To prevent infinite loops from multiple events from being triggered, this method should also check to see if the model is currently picked. If it is, don’t set the state or trigger the event again.

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

2

3 pick: function(){

4 if (this.model.picked){ return; }

5

6 this.model.picked = true;

7 this.model.trigger("picked", this.model);

8 },

9

10 // ...

11 });

Unpicking a model should run the same basic logic, but in reverse. It should trigger an “unpicked” event, but only if the model is currently picked.

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

2 // ...

3

4 unpick: function(){

5 if (!this.model.picked){ return; }

6

7 this.model.picked = false;

8 this.model.trigger("unpicked", this.model);

9 }

10 });

In both functions, JavaScript’s “truthiness” - or coercive nature of evaluation - is being taken advantage of in the guard clauses. If a model has just been created, calling pick will check for a picked attribute which won’t exist. The attribute evaluation will return undefined, which gets coerced in to a false value for the if statement. Similarly, the guard clause in the picked method will negate the coerced value if unpick is called on a model that does not have a picked attribute. Once the model has been picked, though, the attribute will exist and the guard clauses will no longer coerce the value.

Also note that the picked attribute is not being stored directly on the model. Rather, the instance of the SelectableModel is keeping track of this, which will provide more flexibility and options in how the SelectableModel instance is used.

With the basic implementation of a SelectableModel in place, it can be used in simple scenarios that do not require any interaction with other selected models or collections. (A more complex scenario covering collections and multiple selections will be covered shortly.)

Using A SelectableModel

The SelectableModel implementation cannot be used as a model directly. It does not extend from Backbone.Model and it does not provide the same API as a Model. Instead, it needs to be created as an object instance of its own.

1 var model = new Backbone.Model();

2 var pickable = new Backbone.Picky.PickableModel(model);

The pickable object can be managed on its own, with no requirement for the model to know anything about whether or not it is pickable. Passing the selectable object to a view allows the view to pick and unpick the model. The events that are triggered would have to be bound from theselectable.model or a reference to the original model, though. This would look a little odd if the selectable is passed in as the model for a view. It would be better to pass it in as a separate option. But it would be best to have the view encapsulate the knowledge of needing the model to be pickable entirely.

1 // Build a PickableView that wraps a model

2 // in a PickableModel instance

3 var PickableView = BBPlug.ModelView.extend({

4

5 events: {

6 "change .pick": "togglePick",

7 },

8

9 initialize: function(options){

10 // wrap the view's model in a pickable

11 this.pickable = new Backbone.Picky.PickableModel(this.model);

12 },

13

14 togglePick: function(e){

15 // toggle picking the model, based on

16 // the current state of the pickable

17 if (this.pickable.picked){

18 this.pickable.unpick();

19 } else {

20 this.pickable.pick();

21 }

22 }

23 });

With the PickableView definition in place, a new instance can be created, passing in the model without having to do anything particularly special to it. The view itself has encapsulated the code that is needed to make the model pickable. The DOM event configuration looks for a ‘.pick’ element - a checkbox, most likely. Changing this checkbox (checking or un-checking it) will toggle the PickableModel’s state.

The end result is that any model of any type can be used, and that model will be pickable from the view.

1 // define a new model, but don't do anything to it

2 var MyModel = Backbone.Model.extend({});

3

4 // create a model and view instance

5 var myModel = new MyModel();

6 var view = new SelectableView({

7 model: myModel

8 });

The model that is passed in to this view is now pickable. This is not because the model had any changes applied to it, but because the view wrapped it in a PickableModel instance.

The “toggle” Boilerplate

You may have noticed that the PickableView provides a method to toggle the model being picked or not. This is exactly the kind of boilerplate code that should be avoided in projects, as it will be copy & pasted from view to view, project to project. I’ve left a .toggle() function out of the PickableModel implementation, though, to keep this chapter short. But it would be a good idea for you, dear reader, to add this method to your implementation.

Lessons Learned

There are several things that the Backbone.Picky plugin did differently than the previous BBPlug add-on. Most notably is the namespace, but also the ability to create a selectable object on its own instead of having to extend from a base model or collection. Several of these lessons may apply to models and collection specifically, but many of them can be more broadly applicable to creating plugins and writing well structure Backbone applications.

Build Add-Ons As Separate Objects

It’s often tempting to create specialized versions of objects that can be extended from directly. Backbone provides a .extend method on every one of it’s types, after all. In the first part of this book, specialized view types were extended from Backbone.View, as well. But this can be problematic. With models, it is generally a bad idea to extend from Backbone.Model to create a specialized type. There are far too many possibilities for extensions and add-ons, and forcing a developer to extend from your model type prevents them from using any other model type as the base.

Using The Backbone Namespace

Using a custom namespace for an object helps to keep the global object space of the JavaScript runtime clean. However, having a handful of plugins that each create their own global object can still pollute more than is desired. Its common for Backbone plugins to use the Backbone namespace to help reduce this pollution and also to give the developer using the plugin more of a direct sense that this plugin is meant to be used exclusively with Backbone.

When using the Backbone namespace for a plugin, though, be sure to create a child namespace that hangs off of it instead of placing the plugin’s objects directly on the Backbone namespace. Backbone.Picky is a good example of this, as it creates a child namespace called Picky, which contains all of the objects for the plugin.

The Principle Of Least Surprise

The principle of least surprise says that the code being used should look and work in a manner that is least surprising. That is, a method’s name should be consistent with the behavior of the method. In the case of Backbone.Picky, the use of the SelectableModel can either enforce or violate this principle. In the case of using the selectable object directly in a view, as either the model or a separate object passed in as the options, the use of the functionality provided may be somewhat odd or “surprising”. By mixing the functionality directly in to a model instance, though, access to the selectable functionality is more consistent with the expectations of developers writing views.

Encapsulate Features And Wrappers

It is generally reasonable to ask another developer to send a specific type of object to a method parameter. This is how software development and method calls work, after all. Passing the wrong type to the wrong parameter usually causes bad things to happen. But there are situations where this becomes an undue burden to developers - when a feature’s method needs a type that is specific to the feature, asking a developer to wrap their object in this type can be problematic. Encapsulating features and feature-specific wrappers in to method calls allows for flexibility and eases the burden on developers using the feature.