Arrays in a Backbone.js Model are essentially static? - backbone.js

Why are arrays in a Backbone.js Model essentially static variables?
class exports.Content extends Backbone.Model
tags: []
then if i make a few models:
contentA = new Content()
contentB = new Content()
and add one string to each models' array:
contentA.tags.push('hello')
contentB.tags.push('world')
they both end up with the same array:
contentB.tags // ['hello','world']
but if it's a string, then there is no problem:
contentA.name = "c1"
contentB.name = "c2"
contentA.name // "c1"

The short answer
When you call extends to define your object, you are passing the new object's configuration in as an object literal. Objects are passed by reference, and the extends function only passes a reference to the tags array in to the new type definition.
As noted by others, you can correct this by assigning tags to a function. This works because a function delays the evaluation of the tags until the object is instantiated. There's nothing native in JavaScript that does this, but it's Backbone itself that recognizes tags as a function or a value.
The long answer
In spite of your code being in CoffeeScript, this comes down to a combination of a few things in JavaScript:
There are no classes in JavaScript
Object literals are evaluated immediately
JavaScript objects are passed around by reference
In JavaScript, there are no classes. Period. CoffeeScript gives you the notion of a class, but in reality, it gets compiled down to JavaScript which has no classes.
You can have types and type definitions (constructor functions) but not classes. Backbone provides a class-like definition, that looks similar to Java's "extend" class-based inheritance. It's still just JavaScript, though, which has no classes.
What we have, instead, is an object literal being passed in to the extends method. It's as if you write this code:
var config = {
tags: []
}
var MyModel = Backbone.Model.extends(config);
In this code, config is an object literal, or a hash, or a key/value pair, or an associative array. Whatever name you call it, it's the same basic idea. You end up with a config object that has a tags attribute. The value of config.tags is an empty array, [], which is itself an object.
Which brings us back to the short answer:
When you call extends to define your object, you are passing the new object's configuration in as an object literal. Objects are passed by reference, and the extends function only passes a reference to the tags array in to the new type definition.
As noted by others, you can correct this by assigning tags to a function. This works because a function delays the evaluation of the tags until the object is instantiated. There's nothing native in JavaScript that does this, but it's Backbone itself that recognizes tags as a function or a value.

"tags" is being declared on the Content.prototype - which is shared across all instances of Content. You probably need to use defaults instead: http://backbonejs.org/#Model-defaults. However, it should be noted that you must use a function (instead of a hash) when defining defaults, otherwise you still run into the same problem when using types which are passed by reference (e.g. Array).
var Content = Backbone.Model.extend({
defaults: function() {
return {
tags: []
};
}
});

benpickles is correct when it comes to Backbone models, and the way to do this in coffeescript in general is to initialize instance properties in the constructor:
class Foo
constructor: ->
#bar = []

As Derick Bailey mentioned, pass by reference is the problem. I solved the problem by cloning the Array (passing-by-value essentially):
var _events = window.active_model.get('events').slice(0);
_events.push({ key: "xxx", value: "yyy" });
window.active_field.set('events', _events);

Related

Reference to HTML elements in view, a convention?

I'm currently in the progress of learning Backbone.js and I'm using the book Developping Backbone Applications.
I have a questions about the reference to HTML elements and how they are stored. For example:
initialize: function() {
this.$input = this.$('#new-todo');
Here the HTML element with ID to-do is stored in the this.$input, why do we use the $ in front of input, is this merely a convention? If I change this.$input to this.input my code works fine. I find this confusing because the book states:
The view.$el property is equivalent to $(view.el) and view.$(selector) is equivalent to $(view.el).find(selector).
I would think that $(view.el) does something completely different than (view.el).
How is this.$input saved in Backbone.js? If I console.log it, it produces:
Object[input#new-todo property value = "" attribute value = "null"]
Could someone give me some insight? :)
Using $ infront of a variable name is just a naming convention. It helps developer in distinguishing variable holding jQuery objects from others.
view.$el is a helper variable provided by Backbone, so that we can use it directly, instead of explicitly forming the jQuery object. Hence view.$el is equivalent to $(view.el).
view.$el is assigned in setElement method:
setElement: function(element, delegate) {
// Some code
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
// Some code
}
Backbone.$ is reference to $ global variable exported by jQuery.
view.$(selector) is a method defined in View. It's definition does exactly same as $(view.el).find(selector)
$: function(selector) {
return this.$el.find(selector);
}

Should my Backbone defaults be an object or a function?

I have been reading and following several Backbone.js tutorials and when it comes to defaults for the model people seem to do it one of two ways.
First Way - Defaults are an object
The first way is that defaults are declared as an object, for example;
my_model = Backbone.Model.extend({
defaults: {
title: 'Default Title'
}
});
This makes most sense to me, I immediately know that the defaults is an object and it works fine.
Second Way - Defaults are a function
The second way I have seen this is is that defaults are declared as a function, for example;
my_model = Backbone.Model.extend({
defaults: function() {
return {
title: 'Default Title'
}
}
});
This function obviously ends up returning an object, and to me makes little sense (unless you wanted to pass something into the function eventually.
My Question
My question is, is there a benefit to using one over the other assuming that you will not be passing any parameters using the function way. My feeling is that there may be a minuscule overhead from having the anonymous function be called but would love a more informed opinion.
Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Defaults containing objects passed by reference should be defined using a function if you do not wish to share objects between all instances
https://github.com/documentcloud/backbone/issues/1145
Pretty much sums it up. The function method is only recommended when you have object attributes.
I think there are not any performance difference between the 2 techniques you have described. The way the defaults is resolved (= function called or just the object returned) is decided by this line in underscore.js:
return _.isFunction(value) ? value.call(object) : value;
As for the benefits. The regular object offers a static way of declaring the model defaults. You declare them when extending and thats it, they won't change. The function on the other hand provides you with the ability to change the model defaults of the fly without re-creating the whole class, by modifying the object the function is supposed to return.
Another reason to use a function instead of an object is if one of your defaults depends on a method in the model.
e.g.
defaults: function() {
return {
title: this.getTitle()
}

Can't extend backbone.events in coffeescript

I'm getting the error:
Uncaught TypeError: Cannot read property 'constructor' of undefined
When declaring the following class:
class ViewHelpers extends Backbone.Events
I can use the same syntax to extend Backbone.Router, Views, Model etc. Here is the compiled javascript which I wrote in a quick log to make sure Backbone.Events was there
__t('views').ViewHelpers = (function(_super) {
#how i know it is definied here
console.log(_super.trigger)
__extends(ViewHelpers, _super);
function ViewHelpers() {
return ViewHelpers.__super__.constructor.apply(this, arguments);
}
return ViewHelpers;
})(Backbone.Events);
So the line causing the error is
ViewHelpers.__super__.constructor.apply(this, arguments);
What is different about __extends() method that it would work for Backbone.View and not Backbone.Events?
That's because Backbone.Events is not a "class", so it cannot be extended, it's a "module" that can be mixed-in into other objects (see docs here). In JavaScript terms that means that it's not a Function, that can be called as a constructor (i.e. new Backbone.Events will throw an error), it's just a plain JS object whose properties (methods) can be assigned to other objects to make them event dispatchers.
In CoffeeScript, you can mix-in the Backbone.Events into your objects when they are created:
class ViewHelpers
constructor: ->
_.extend #, Backbone.Events
Or you can just extend the class' prototype and avoid having those methods as (own) properties of all ViewHelpers instances:
class ViewHelpers
_.extend #prototype, Backbone.Events
These two approaches should work and let you instantiate and use ViewHelpers as event dispatchers:
vh = new ViewHelpers
vh.on 'foo', -> alert 'bar'
vh.trigger 'foo'​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​
There's another way (from what #epidemian answered), which doesn't involve copying Backbone.Events into a new object to use as your prototype - instead, use Object.create to create a new object to use as your prototype, using Backbone.Events as its prototype.
class ViewHelpers
#prototype = Object.create(Backbone.Events)
Now ViewHelpers' prototype is a new, empty object whose prototype is Backbone.Events. You can define methods on ViewHelpers' prototype without affecting Backbone.Events, but all the Backbone.Events methods are still available to ViewHelpers, without having to copy them into a new object. This not only saves (a miniscule amount of) memory, but if you ended up adding on to Backbone.Events later, all ViewHelperss would see the change.
For this, you'll need either a browser that has ES5's Object.create function, or an Object.create polyfill.
To build on the excellent answer by #epidemian I would add this, it's a bit of a hack but it allows you to write your class with the extends statement as you specified in the question (which lets you call super on all the Backbone.Events methods):
class Events
_.extend Events.prototype, Backbone.Events
class CustomEvents extends Events
trigger: (event, etc...) ->
# You can add overrides before
super "custom:#{event}", etc...
# or after the super class methods
It would be neat to work the _.extend call into the Events.constructor function but I couldn't make it work...

Backbone.js extend and this

I'm using experimenting with .extend() to set up my views and initialise them with. I've found it's convenient to assign config variables to view objects nested deep within a hierarchy.
My problem is that my Views lose their this context. This becomes the ctor object which I asume is the constructor. How can I fix this?
My coffeescript is below. The first class would be nested deep within a tree, the second is at the top level where the application boots up:
# This is a child somewhere deep within a tree of views.
class View extends Backbone.View
initialize: ->
console.log # # returns object ctor
MyView = View.extend({
initialize: ->
# do config stuff then init prototype
App.Views.MyView.prototype.initialize()
})
view = new MyView
Two things:
First, and not as importantly, you can use
class MyView extends View
instead of View.extend. CoffeeScript classes and Backbone classes are interoperable.
Second—and this is the important part—instead of
App.Views.MyView.prototype.initialize()
you should simply use the CoffeeScript keyword
super
That effectively does the same thing, but also ensures that the function is called in the correct context. Bonus: It also passes in all of your function arguments for you.
If you're curious, super here compiles into
initialize.__super__.constructor.apply(this, arguments)
(where __super__ is a reference to the superclass that's set by both CoffeeScript's extends). Read about apply at MDN.
My coffe script isnt so hot but can you call the function sending the view you want to be the value for this in as the first parameter
I suppose in standardish js
var view = null;
MyView = View.extend({
initialize: function() {
// do config stuff then init prototype
App.Views.MyView.prototype.initialize.call(view)
}
})
view = new MyView;
Im not sure what you have access to at that point or the generated js either.
The point being if you have access to what should be the value for this when you call the function you should be able to pass it in.
I would check myself but i don't comprehend coffeescript :)

Access Nested Backbone Model Attributes from Mustache Template

I have one Backbone model which has an attribute that is a reference to another Backbone model. For example, a Person has a reference to an Address object.
Person
FirstName
LastName
Address
Street
City
State
Zip
These are classes that extend the Backbone model. So, then if I construct an object like the following...
var address = new Address({ Street: "123 Main", City: "Austin" });
var person = new Person({ FirstName: "John", Address: address });
I cannot seem to figure out how to access it in my Mustache template.
Hi {{FirstName}}, you live in {{Address.City}}.
Obviously does not work. When I look at the internals in Firebug, Address is an object, but the City is an attribute within the attributes object of Address. I cannot find any examples of how to access these attributes of associated objects.
I appreciate any help! Thanks!
I ended up solving this issue with the following approach.
I switched from Mustache.js to Handlebars.js for the templating engine. This allowed me to use path based expressions to access nested or associated objects and their attributes.
Hi {{FirstName}}. You live in {{Address.City}}.
But, I also had to change the way I was passing a JSON object to the template. I was using the toJSON method that is part of the Backbone.Model class. But, this was not generating JSON for the associated Address correctly (for the templating to work.) It was burying the address attributes in a member titled "attributes". So, instead, I ended up doing this...
var jsonForTemplate = JSON.parse(JSON.stringify(person));
This gave me a "raw" version of the objects and their associated objects which the template could access using the syntax shown above. JSON.parse and JSON.stringify are both part of json2.js.
I handled this by making another version of toJSON called deepToJSON that recursively traverses nested models and collections. The return value of that function can then be passed to a handlebars.js template.
Here is the code:
_.extend(Backbone.Model.prototype, {
// Version of toJSON that traverses nested models
deepToJSON: function() {
var obj = this.toJSON();
_.each(_.keys(obj), function(key) {
if (_.isFunction(obj[key].deepToJSON)) {
obj[key] = obj[key].deepToJSON();
}
});
return obj;
}
});
_.extend(Backbone.Collection.prototype, {
// Version of toJSON that traverses nested models
deepToJSON: function() {
return this.map(function(model){ return model.deepToJSON(); });
}
});
Try using Handlebars, a templating engine based on Mustache with nested properties support.
Then it would be as easy as {{Address/City}}.
If you don't want to change your templating engine, you can flatten results from Address object and pass them as properties directly on the Person.
The way to go about the same in Mustache would be as follows:
Hi
{{FirstName}}, you live in {{#Address}}{{City}} {{/Address}}
Hope it helps..

Resources