Overriding Backbone.View’s ‘constructor’ - 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 6: Overriding Backbone.View’s ‘constructor’

When overriding the base Backbone.View to create your own view types from the base views, there are some issues that you may run in to with inheritance. The inheritance of constructor functions can be broken in strange ways, and code that you override in the constructor or other base ModelView or CollectionView functions may not be called when you expect it to be. This can be a tremendously frustrating problem, as the code you write looks correct, but it does not fire like you expect.

Fortunately, the fix is simple. Unfortunately, it’s not obvious. Worse, though, is that the simple but not obvious nature of the fix often makes it look like the solution is unnecessary.

Understanding the solution, then, is great - but it is not enough to know the solution. If you don’t understand the problem and the cause, it’s likely that you’ll allow the solution to be removed at a later point. Before seeing the solution, then, you need to understand the problem and the cause. It’s also a good idea to mark the solution with comments that explain why it’s there, so that others won’t accidentally remove the solution in the future.

Setting An Automation ID On All Views

Say you have an application that always needs to set a specific data-attribute on view instances. You may need to supply an attribute like data-automation-id with a unique yet repeatable id for test automation. This id should only be available for test automation, though, and not in the production environment.

To facilitate this, you can add a new JavaScript file to a project and include code such as the following in it.

1 // automationid.js

2 (function(){

3

4 // store a reference to the original constructor function

5 var originalConstructor = Backbone.View.prototype.constructor;

6

7 // create a function that can return a unique, repeatable id

8 function getAutomationId(view){

9

10 // do something here to get a unique, yet repeatable id

11 // based on the view instance that is passed in. send it

12 // back as a return value

13 var id = …;

14

15 return id;

16 }

17

18 // override the View's constructor

19 Backbone.View.prototype.constructor = function(options){

20

21 // call the original constructor function with

22 // the options provided to this instance

23 originalConstructor.call(this, options);

24

25 // set the automation id on the

26 var automationId = getAutomationId(this);

27 this.$el.data("automation-id", automationId);

28

29 // log a message, saying the automation-id is set

30 console.log("Automation ID set: ", automationId, view);

31 };

32

33 })();

information

That’s A Bit If-y

The automation ID code is wrapped in an Immediately Invoking Function Expression (“IIFE”, pronounced “if-e”) so that the reference variable and view definition can be hidden from global scope. You may or may not need this wrapper for private scope. You can learn more about IIFEs from Ben Alman’s blog post on the subject.

In your project’s HTML, include this file immediately after the backbone.js file. Putting it here will ensure that your automation id function is attached to the Backbone objects before anything like BBPlug is loaded.

1 <script src="/js/backbone.js"></script>

2 <script src="/js/automationid.js"></script>

3 <script src="/js/bbplug.js"></script>

Now to test this and see the automation id.

Testing The AutomationID Plugin

To test this, you only need to run an application after the automationid.js file has been included.

1 (function(){

2

3 var V = BBPlug.ModelView.extend({

4 template: "#model-template"

5 });

6

7 $(function(){

8 var v = new V();

9 v.render();

10 $("#main").html(v.$el);

11 });

12

13 })();

Every view instance should log a message stating the automation id with the view instance. Only instead of seeing the message you expect, you will see nothing at all in the console logs.

This is definitely not what you wanted or expected. It should be showing the automation ids for each view, after all, and it isn’t.

So why isn’t it showing the automation id?

The Problem Of Taxidermy

The short version of the problem is that replacing the constructor method on Backbone.View.prototype doesn’t work. The prototype.constructor attribute is part of the JavaScript prototypal inheritance system, and isn’t something you should replace at all. Doing so is essentially trying to replace the type itself, but only replacing a specific reference to the type. Backbone provides the ability to specify a constructor in it’s type definitions, and it uses that function as the constructor function for the type that is returned. But this is only a convenience if not a coincidental naming of a method.

Unfortunately, this creates a larger problem. There are times when you will want to override the constructor of a type, but you shouldn’t try to do so the way that the automation id plugin was working.

Taxidermy On Prototypes

Replacing the prototype.constructor is a bit like taxidermy - the end result of “stuffing” a dead bear may still look like a bear on the outside, but it’s not really a bear anymore. It just looks like one. In the case of prototype.constructor though, this is especially dangerous because you can break code that relies on type checking or prototypal inheritance features that look at the prototype.constructor.

Visualize this through code, again:

1 function MyObject(){}

2

3 MyObject.prototype.constructor = function(){};

4

5 console.log(MyObject.prototype.constructor);

6 console.log(MyObject.prototype.constructor === MyObject);

The prototype.constructor is no longer pointing to MyObject, so the original MyObject “constructor function” is not being applied to the new object instance.

Correctly Overriding The Constructor Function

All of the problems associated with the prototype.constructor replacement may be a bit disheartening. But there is hope, and a fairly simple solution.

There are two things you will need, to solve the problem of overriding a Backbone object’s constructor function without any other code having to extend from it directly.

1. A new type with the “super-constructor” pattern (where a type calls back to the prototype.constructor manually)

2. A complete replacement of the base view who’s constructor you want to replace.

By creating a new type that calls back to the original type’s constructor, you can ensure the correct chain of inheritance and constructors is handled. Then to get your new type in place without forcing others to extend from your type, you will need to replace the type from which your plugin extends, prior to any other code using it.

The Super-Method Pattern

The “super-method” pattern is where you call the parent type’s method from your own method. Most other languages make it easy to call the “super” function when you override method - to call the original method that you are overriding. JavaScript, unfortunately, doesn’t have a “super” or “base” or anything like that. There are ways around this, though, all of which involve getting a reference to the prototype of your type, and calling the method directly.

For example, overriding the toJSON method of a model:

1 var MyModel = Backbone.Model.extend({

2

3 // a super-method

4 toJSON: function(){

5 // call the "super" method -

6 // the original, being overriden

7 var json = Backbone.Model.prototype.toJSON();

8 // manipulate the json here

9 return json;

10 }

11 });

This will call the toJSON method of the type from which the new type was extended. It’s a little more code than other languages, but it still gets the job done.

information

More Than One Way To Call Prototype

There are a lot of different ways to get to and call the methods of a prototype object. If you’d like to learn more, check out my triple pack of screencasts covering Scope, Context and Objects & Prototypes

The Super-Constructor Pattern

The constructor function of a Backbone object looks like it would be even easier to replace than a normal method. If you extend from Backbone.View, you don’t need to access the prototype. You only need to apply Backbone.View as a function, to the current object instance.

Once you have your type set up, you will need to replace the original type with your new type. By doing this, any new type that tries to extend from the original named type, will get yours instead of the original. But you can’t just replace Backbone.View directly.

1 // define the new type

2 var MyBaseView = Backbone.View.extend({

3 constructor: function(){

4 var args = Array.prototype.slice.apply(arguments);

5 Backbone.View.apply(this, args);

6 }

7 });

8

9 // replace Backbone.View with the new type

10 Backbone.View = MyBaseView;

information

Args And Old Browsers

The args line is in the super-constructor example to ensure compatibility with older browsers. Some versions of IE, for example, will throw an error if the arguments object is null or undefined, and you pass it in to the apply method of another function. To work around this, you can slice the arguments object in to a proper array. This will return an empty array if the arguments is null or undefined, allowing older versions of IE to work properly.

Unfortunately, this setup will fail horribly. When the call to Backbone.View.apply is made, it will find your new type’s constructor sitting in Backbone.View, causing an infinite loop.

To fix this, you need to store a reference to the original Backbone.View separately from the new view type. Then you will need to call this reference from your constructor function, and not the Backbone.View named function, directly.

1 (function(){

2 // store a reference to the original view

3 var Original = Backbone.View;

4

5 var MyView = Original.extend({

6 // override the constructor, and all the original

7 constructor: function(){

8 var args = Array.prototype.slice.call(arguments);

9 Original.apply(this, args);

10 }

11 });

12

13 // Replace Backbone.View with the new one

14 Backbone.View = MyView;

15 })();

The end result of this is Backbone.View having been replaced with your view type, while still maintaining a reference to the original so that it can be called when needed. Provided the file that includes this code is loaded prior to any other code extending from Backbone.View, all views will receive any functionality defined in MyView - which is exactly what you wanted in the Automation ID view.

Putting The Pieces Together

With the “super-constructor” pattern, the problem of replacing Backbone.View without any other code knowing it has been replaced, can be solved. The Automation ID plugin can be salvaged and it can be used, with relatively minor modifications.

1 // automationid.js

2 (function(){

3

4 // store a reference to the

5 // original constructor function

6 var Original = Backbone.View;

7

8 // create a function that can return

9 // a unique, repeatable id

10 function getAutomationId(view){

11

12 // do something here to get a unique, yet repeatable id

13 // based on the view instance that is passed in. send it

14 // back as a return value

15 var id = …;

16

17 return id;

18 }

19

20 // override the View's constructor

21 AutomationIDView = Bacbone.View.extend({

22

23 constructor: function(options){

24 // call the original constructor function with

25 // the options provided to this instance

26 Original.call(this, options);

27

28 // set the automation id on the

29 var automationId = getAutomationId(this);

30 this.$el.data("automation-id", automationId);

31

32 // log a message, saying the automation-id is set

33 console.log("Automation ID set: ", automationId, view);

34 }

35 });

36

37 // replace Backbone.View

38 Backbone.View = AutomationIDView;

39

40 })();

Now you have a plugin that replaces Backbone.View with your AutomationIDView, and no other code in the app knows about it. You only need to ensure this file is loaded before any other file with code that extends from Backbone.View.

Not Just Backbone.View

This problem and solution applies to any Backbone type, not just View. If you replace the Backbone.Model.prototype.constructor or Backbone.Router.prototype.constructor, you will run in to the same problem and have the same options for a solution.

1 (function(){

2

3 var origConst = Backbone.Model.prototype.constructor;

4

5 Backbone.Model.prototype.constructor = function(){

6 // do custom code here

7

8 var args = Array.prototype.slice.apply(arguments);

9 origConst.apply(this, args);

10

11 // do custom code here

12 }

13

14 })();

Not Every Type You Define

Fortunately, you don’t need to apply the super-constructor pattern of calling back to the parent’s constructor function, every time you define a new type. You only need to do this when you are defining a base type from which you will be extending.

1 var MyBaseView = Backbone.View.extend({

2

3 // apply the super-constructor pattern

4 // because this is a base view type from which

5 // other views will extend

6 constructor: function(){

7 var args = Array.prototype.slice.apply(arguments);

8 Backbone.View.prototype.constructor.apply(this, args);

9 }

10 });

11

12 var CreateUserForm = MyBaseView.extend({

13 // no need to apply the super-constructor pattern

14 // because this view type is not meant to be extended

15 // in to a new view type

16 });

Lessons Learned

A picture may be worth 1,000 words, but a single line of code to be changed is worth at least 2,0000 if this chapter’s length is to be a judgement.

JavaScript Can Be Complex. Simplify It.

JavaScript’s inheritance, prototypes, method invocation patterns, and other aspects all have rather simple rules on the out-set. But the combination of seemingly simple rules can have some very dramatic effects. Understanding each aspect of the rules and how they all combine can be an overwhelming task, at times. Frameworks like Backbone help us to avoid common mistakes and hide a lot of the complexities. But even great frameworks with thousands of users have edge cases.

Don’t be surprised when your own applications run in to edge cases and complexities. Do your best to hide the complexity from the developers writing day to day code. Create meaningful abstractions that are easy to modify, so that simple fixes can have a meaningful impact on your application.

Constructor Functions vs prototype.constructor Functions

JavaScript’s prototypal inheritance system sets up a default prototype for every function. Each prototype has a constructor function that points to the original function that defined the type, by default. The prototype.constructor should not be touched, in spite of Backbone allowing us to define constructor functions. This is akin to taxidermy where something looks like one thing, but is really something else entirely.

The Super-Constructor Pattern

When creating base types for other types to extend from, you may need to apply the super-constructor pattern. This allows your type to ensure the correct constructor function of the parent type is called.

Code Injection As A Form Of Plugin

Plugins that are provided as base types can be useful. Specialization of view types, for example, creates an easy way to get functionality in to a system, quickly. But this isn’t always the best way to create a plugin. There are times when you need to inject code without any other code knowing.

In the original example for this chapter, there was a need to add an automation id to every view instance. One option for doing this would have been to create an AutomationIdView from which every view could extend from. But that would cause the automation id to be in your production environment, as well. Instead, a plugin to replace the Backbone.View.prototype.constructor function was created. This allows the automation id to be inserted for a test environment, by including the automationid.js file, while leaving the functionality out of the app when in production or other environment that don’t need it.