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...
Related
For context I use coffeescript. If I create a base model that extends Backbone.Model and I create another class (i.e. App.Models.Project extends App.Models.Base).. everything works as expected.. what would be the difference to an instance of Project if in this base class I wrote:
initialize: ->
super
console.log 'hi'
and just plain
initialize: ->
console.log 'hi'
Without spending too much time, it seems in my console an instantiated object acts as expected in both cases.. I hear you should 'always call super' here but I don't know what I'm getting..
Backbone.Model.initialize does nothing.
From the annotated source code, you can see the empty function defined in Backbone.Model
initialize: function(){}
It's upto your model to override. Usually, model variables are set here. Whenever you create a model object, initialize is called internally.
The same principle holds good when creating Views and Collections too.
I need to execute some code both on initialize and render methods, but as I understand, I canot just modfy them directly when using Chaplin - when I define my own initialize method, my routes stop working.
I also tried afterInitialize() but it seems its not meant to be overrided: https://github.com/chaplinjs/chaplin/issues/168#issuecomment-8015915
[...] but as I understand, I canot just modfy them directly when using
Chaplin
You can modify them directly as long you appropriately delegate to the extended prototype.
As you haven't tagged your question javascript or coffeescript, the following are two solutions for each one. First up is javascript. Notice how we must explicitly invoke the extended function.
var View = Chaplin.View.extend({
initialize: function(options) {
// Add code here ..
// The below invokes the initialize function of the
// base Chaplin.View class in the context of
// this class
Chaplin.View.prototype.initialize.call(this, arguments);
// .. or here
}
});
Next is coffeescript, which makes this kind of thing significantly easier.
class View extends Chaplin.View
initialize: ->
// Add code here ..
// The below auto-magically invokes the
// initialize method of the base class
super
// .. or here
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 :)
I would like to access the calculated fields I have implemented in the model (backbone.js) from the template.
Do I need always to define a helper to do it?
I think the problem has to do with the way I pass the model to the template.
If I pass this.model.toJSON() I have access to the properties but not to the functions I have defined in it.
If I pass this.model directly I can access the function but not the properties of the backbone model.
Always pass this.model.toJSON() to your templates.
What you need to do to get your calculated values, is override your toJSON method on your model.
MyModel = Backbone.Model.extend({
myValue: function(){
return "this is a calculated value";
},
toJSON: function(){
// get the standard json for the object
var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
// get the calculated value
json.myValue = this.myValue();
// send it all back
return json;
}
})
And now you have access to myValue from the the JSON that is returned by toJSON, which means you have access to it in the view.
The other option, as you mentioned, is to build helper methods and register them with Handlebars. Unless you have some functionality that changes based on how the template is being rendered, and/or what data is being passed to the template, I wouldn't bother with that.
Here is another possibility: (from the model initialize)
initialize: function() {
this.on("change", function () {
this.set({ calculatedColumn: this.get("otherColumn") }, { silent: true });
});
},
Computed properties in Backbone
I have had the same issue. #DerickBailey is right, of course, that overriding toJSON does the job. But it also leaks into the communication with the server (see muu's comment on his answer).
So eventually, I have built a Backbone plugin to specifically handle data export to templates, and do so with a minimum of fuss: Backbone.Marionette.Export. It also deals with nested structures, takes care of circular references etc. See the docs.
Here's how it works. Include the plugin file into your project and declare
MyModel = Backbone.Model.extend({
foo: function () {
return "I am a calculated value";
},
exportable: "foo" // <-- this is the one line you have to add
});
If you are a Marionette user, you are already done at this point. foo shows up in your templates as if it were a model attribute.
In plain Backbone views, just call myModel.export() or myCollection.export() instead of their toJSON counterparts when you render.
For methods taking arguments, there is an onExport handler. Examples, again, are in the docs.
The best way to do it is to add this to your model:
function initialize() {
this.set("calculatedColumn", function () { return this.otherColumn; });
}
A backbone model normally stores the actual data values internally in "model.attributes". That is why when you pass your model directly to the template, it only has functions added directly to model and not any data. And if you use model.toJSON() it is normally implemented in backbone as _.clone(model.attributes) (see backbone.js). So you have the data and not the functions added directly to the model. That is why the above works - you set the function on model.attributes, not on the model object itself. Do not reference model.attributes directly, use model.get("calculatedColumn") and model.set("calculatedColumn", ...).
So model.get("calculatedColumn") returns a function. If you go {{calculatedColumn}} in handlebars (assuming you're using handlebars), it shows the value returned by the function. But calculatedColumn will not be sent to the server because backbone does a JSON.stringify to model.toJSON in sync (in backbone.js) and JSON.stringify ignores functions. If you want JSON.stringify to not ignore the function (so the function is turned into a data value whenever toJSON is run on the model - during view rendering and model sync-ing), override model.toJSON just as #Derick Bailey described.
Also, you can derive your own BaseModel from Backbone.Model and override .toJSON and derive all your models from BaseModel if you need to. Then you would need a generic version of .toJSON that could be applied to any model.
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);