I'm not sure if I'm doing this right, first time playing with Backbone.js.
I have two views with two models and I want to use the event aggregator method to fire events between the two.
The aggregator declaration:
Backbone.View.prototype.eventAggregator = _.extend({}, Backbone.Events);
So in one view I have a line like this that will fire the removeRow method.
this.eventAggregator.trigger("removeRow", this.row);
In another view
MyView = Backbone.View.extend({
initialize: function() {
this.eventAggregator.bind("removeRow", this.removeRow);
this.model.get("rows").each(function(row) {
// Do stuff
});
},
removeRow: function(row) {
// row is passed in fine
// this.model is undefined
this.model.get("rows").remove(row);
}
});
I think I understand why this.model is undefined, but what can I do to maintain a reference so that I can use this.model in the callback? I thought about passing the model to the first view and then passing it back in the trigger call, but that seems to make the entire point of an event aggregator pointless. If I have the model I can just call the .remove method directly and have lost the benefit of my first view being unaware of the model. Any suggestions?
I think you have binding problem.
You have two ways to assure that this will be the View instance:
1. Using bindAll
In your View.initialize() you can add this line:
_.bindAll( this, "removeRow" )
Interesting post of #DerickBailey about this matter
2. Using the optional third argument in your bind declaration
Like this:
this.eventAggregator.bind("removeRow", this.removeRow, this);
Backbone documentation about this matter
Supply your View object as third parameter of the bind method:
this.eventAggregator.bind("removeRow", this.removeRow, this);
The third parameter is the context of calling your callback. See the docs.
Also, you can use .on() instead of .bind() which is shorter...
You need to bind this so scope isn't lost. The blog link on the other answer uses underscore's bindAll
initialize: function() {
_.bindAll(this, 'removeRow');
this.eventAggregator.bind("removeRow", this.removeRow);
this.model.get("rows").each(function(row) {
// Do stuff
});
},
Related
Is there any possibility to change the scope of the subscribe in Knockout?
I have something like this:
element =
{
type: ko.observable()
name: ko.observable()
content: ko.observable()
}
element.type.subscribe(this._typeChanged.bind(element))
Basically I want to have an access to the object which property I am subscribed to. Binding like in my code does nto work since it binds to the whole VeiwModel and not the object.
Maybe the knockout handle that when you subscribe an observable you can pass 2 parameters the first is the callback and the second is the scope/context, try something like this:
element.type.subscribe(this._typeChanged, element)
The subscribe function accepts three parameters: callback is the function that is called whenever the notification happens, target (optional) defines the value of this in the callback function, and event (optional; default is "change") is the name of the event to receive notification for.
Ref. http://knockoutjs.com/documentation/observables.html
The problem is the way in which you're creating your view model. The view model shuld be self-contained, including the functions that operate on it. It should be something like this:
var ViewModel = function() {
var self = this;
self.type = ko.observable();
self.name = ko.observable();
self.content = ko.observable();
self.type.subscribe(function(newVal) {
// here you have access to all the viewmodel properties through self
});
return self;
};
This is a constructor using the var self=this; pattern.To use the view model you need to instantiate it, i.e. var vm = new ViewModel(). (You can omit the new).
Of course, you can also define a function, and bind it to self, or receive a callback in the constructor, and bind it to self. In that case, the function implementation will have the view model accesible via this, and not self, which will be undefined inside the function body.
var doSomethignWithVm = function(newVal) {
// acces viewmodel via this
// you can also use newVal
};
You modify the constructor to receive this as a callback:
var ViewModel = function(doSomethingCallback) {
self.type.subscribe(callback.bind(self));
};
This pattern doesn't make much sense because your callback is supposed to have knowledge of your view model. In that case it makes more sense to include the subscription functionality directly inside the model.
EDIT
Note: as I've mentioned in a comment to Joel Ramos Michaliszen's answer, both of this codes are equivalent:
self.type.subscribe(callback.bind(self));
self.type.subscribe(callback.bind, self);
You can check that by seeing the source code of subscribable in knockout's gitbhub, in the file knockout/src/subscribales/subscribable.js. If you look for subscribe implementation you'll see this:
subscribe: function (callback, callbackTarget, event) {
// ...
boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
I.e. if you provide a second argument, it's used tob bind the function passed in the firt argument to it.
Although I get that I may have the wrong approach top this I am also in a stage where I will not be able to do any breaking changes to the app.
I figured out that I could use lodash to help me with this.
I ended up using partial function to append the element as a parameter in the subscribe callback:
element.type.subscribe(_.partial(this.typeChanged, element))
or in coffeescript
element.type.subscribe $_.partial #typeChanged, element
Now the chartTypeChanged has 2 parameters on the input instead of one.
Of course i read manual, but as i see in classical example it doesn't really make a difference if i comment the line with binding. Are the methods bound now by default?
(function($){
var ListView = Backbone.View.extend({
el: $('#TheList'), // el attaches to existing element
events: {
'click button#add': 'addItem'
},
initialize: function(){
// _.bindAll(thathis, 'render', 'addItem'); // every function that uses 'this' as the current object should be in here
this.counter = 0; // total number of items added thus far
this.render();
},
render: function(){
$(this.el).append('<button id="add">Add list item</button>');
$(this.el).append('<ul></ul>');
// console.log(this);
// console.log(this.el);
},
addItem: function(){
this.counter++;
$('ul', this.el).append('<li>hello world'+this.counter+'</li>');
}
});
var listView = new ListView();
})(jQuery);
Jax is right that you don't have to manually bind your View methods and events in the latest version of Backbone (Currently 1.1.0). There are earlier versions where this is true too, but I can't remember which ones.
There are cases where you will need to bind your view methods to make them work correctly. These cases are basic Javascript scoping and not related to Backbone JS specifically.
It is best not to use _.bindAll. Instead you should bind on a case-by-case basis using _.bind if you are using Underscore JS.
Actually if you are using Backbone, you won't need to use _.bind either. There are shortcut methods on the Backbone Events class. This means you can use these shortcuts in the Collection and Model classes too since they each have the Events class mixed in essentially.
This what you will need to do in your view class to attach listeners to collections or models.
this.collection.on('add', this.appendItem, this);
or
this.model.on('change', this.render, this);
The third parameter binds the view object scope to the render method. Without this, the render method will be called with the scope of the model I believe.
On a side note you can also do this:
this.collection.bind('add', this.appendItem, this);
The on method is actually an alias to bind, but I think it is clearer to use on. Using bind can be confusing. Looks like the Backbone docs prefer on and off to bind and unbind.
Here are two jsFiddles that illustrates what I'm talking. My examples are modified from this tutorial example: http://arturadib.com/hello-backbonejs/docs/5.html
This tutorial has been around for a long time actually, so may not be the best one to use. It does seem like it may have been updated a bit since it does use Backbone 1.10. I will also note that it uses _.bindAll.
Version 1 uses binding correctly: http://jsfiddle.net/ChTjs/
Relevant methods here:
initialize: function() {
this.collection = new List();
this.collection.on('add', this.appendItem, this);
this.collection.on('add', this.updateCount, this);
this.collection.on('remove', this.updateCount, this);
this.counter = 0;
this.render();
},
and here:
this.model.on('change', this.render, this);
this.model.on('remove', this.unrender, this);
Here is a jsFiddle version that doesn't bind the view scope to the callbacks. http://jsfiddle.net/LpEW8/1/
Try it out and slowly add in the bindings to get the code working again. I just realized this is actually mentioned in the Backbone docs. Search Binding "this"
EDIT 2
Just realized that it would be better to use the listenTo method. The advantage here is that you the callback will always be bound to the view/object that call listenTo. So no need to pass this like when using on. The additional benefit is that the listeners will be removed automatically, whereas that is not the case when using on
So instead of this:
this.collection.on('add', this.appendItem, this);
this.collection.on('add', this.updateCount, this);
this.collection.on('remove', this.updateCount, this);
You do this in your view:
this.listenTo(this.collection, 'add', this.appendItem);
this.listenTo(this.collection, 'add', this.updateCount);
this.listenTo(this.collection, 'remove', this.updateCount);
Here is the anchor link to listenTo in Backbone docs: http://backbonejs.org/#Events-listenTo
Also here is an updated fiddle: http://jsfiddle.net/ChTjs/2/
Yes, 'this' is bound by default to the View for all functions attached to the Backbone View, manually binding is no longer required.
I understand how custom events work in Backbone and how to trigger them, but I'm having trouble understanding when exactly to use them and what purpose they serve over just calling the function directly.
e.g.
var MyView = Backbone.View.extend({
tagName: 'div',
className: 'myview',
initialize: function() {
this.model.on("mycustomevent", this.doSomething, this);
},
doSomething: function() {
console.log('you triggered a custom event');
}
});
If I am not mistaken, The doSomething method can be called by using this.model.trigger("mycustomevent") within other methods, but can be also called directly with this.doSomething()
Outside the view, it can be called similarly with
var myview = new MyView({model:somemodel});
myview.model.trigger("customevent");
myview.doSomething();
What I am confused about is why not forgo the the custom event and just call the method directly when you need it? Any example uses would be greatly appreciated!
You might want to add multiple handlers in different places in the code, f.ex:
this.model.on("mycustomevent", this.doSomething, this);
// ... and somewhere else you add an anonymous function
this.model.on("mycustomevent", function() {
console.log('do something');
});
Then when you trigger the event, it will execute all handlers. You can also use off to unbind/manage individual or multiple handlers.
If you are asking about a generic explanation of the event pattern (also called observer pattern, publish/subscribe, etc...), you should probably look for a more in-depth article or book.
With backbone, the perfect example is changing a property of a model. Using a function, you would have to do something like this...
$( '#someUI' ).click( function {
// update the model property
myModel.someProperty = 'somethingDifferent';
// update any ui that depends on this properties value
$( '#uiThatDisplaysModelData' ).find( 'someSelector' ).html( 'somethingDifferent' );
// save the model change to the server
$.ajax( {
url: 'somesaveurl',
data: { someProperty: 'somethingDifferent' }
success: callback
} );
} );
And then repeat those steps all over your code for each property change.
With backbone and a little setup the same things can be accomplished with:
myModel.set( 'property', 'somethingDifferent' );
This is because we have attached handlers to the change and change:property events of this model. These are custom events that are created automatically for models by backbone. So whenever any part of your code manipulates the model, the DOM updates and saving can be done automatically. We can also bind input validation or whatever we want to these custom events.
It's basically just applying the observer pattern to your application, where events belong to an object that is observable, and the handlers belong to its observers.
http://en.wikipedia.org/wiki/Observer_pattern
I'm just getting into Backbone, and one thing that I don't understand is why the 'on()' method for models always takes three arguments--event, handler, and context.
It seems that almost always 'this' is used for context and I haven't seen any other usage. Even if there were, since I haven't seen one yet it must be pretty rare.
So my question is: When does one use a context other than 'this', and why is Backbone designed this way? By the way, I do understand why you need to provide context, it's just that I wonder why the method syntax specifies that I use three arguments instead of making the last argument optional--which seems to be always 'this' and feels redundant. I'm sure I'm missing something. Please someone help me understand. Thank you!
[EDIT] Why can't one do something like:
model.on = function(event, callback){
model.on_with_three_args.call(this, event, callback, this);
});
model.on_with_three_args = function(event, callback){
/* whatever the on() is supposed to do */
});
Suppose we're in a view that's based on a model and we want to bind to the model's change event:
this.model.on('change', this.render);
The on call sees two things:
The event name, a simple string.
The handler, a function.
on has no way of knowing what this means in this.render, it just sees a function; on won't even know the difference between the call above and this:
this.model.on('change', function() { ... });
If your function needs a particular context then you have two choices:
Create a bound function using _.bind, _.bindAll, Function.bind, $.proxy, CoffeeScripts =>, the var _this = this closure trick, or any of the ways of creating or simulating a bound function.
Tell it which context you want by saying:
this.model.on('change', this.render, this);
There's no way to unroll the call stack to see which this you want so you have to be explicit about it.
Backbone will call the callback like this:
node.callback.apply(node.context || this, ...);
where node.callback is the callback function and node.context is the third argument (if any) given to on. If you don't specify the context then you'll get whatever this happens to be when trigger is called; in the example above, this would end up being the model.
So the third argument to on actually is optional but the default value isn't terribly useful and there is no way to choose a better default, the information you need to choose a sensible context simply isn't accessible in JavaScript. This is why you see so much _.bindAll(this, ...) boilerplate in Backbone views.
If you tried something like this:
model.on = function(event, callback){
model.on_with_three_args.call(this, event, callback, this);
});
then this in that context would usually be model so you'd really be saying:
model.on = function(event, callback){
model.on_with_three_args.call(model, event, callback, model);
});
or
model.on = function(event, callback){
model.on_with_three_args(event, callback, model);
});
and there's little point to any of that. The value of this inside on has little if anything to do with the value of this in the code that calls on. this in JavaScript is not a variable, it is a keyword which refers to the current calling context.
I had some interesting tribulations in trying to test whether views were correctly bound to events. In backbone, we typically bind to events in the initialize method, using something along the lines of: something.bind("change", this.render);. In my test, I want to make sure that this binding is set up, so I did the following:
this.myView = new MyView();
spyOn(this.myView, "render");;
this.legendView.groupData.trigger("change");
expect(this.legendView.render).toHaveBeenCalled();
But, that won't work. Because the bind occurs in MyView's initialize function, the event get's bound to myView's render function AT THAT TIME. So, when you add your spy, it wraps the render function and sets it back into place at myView.render. But the closure created by the first bind still exists, and we are totally hozed. So what can we do about it? What I did, is move my bind call's to a seperate function, something like:
myView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "render");
this.initialize_model_bindings();
},
initialize_model_bindings: function(){
something.bind("change", this.render);
},
render: function(){ //... }
});
and my test then looks like:
this.myView = new MyView();
spyOn(this.myView, "render");
this.myView.initialize_model_bindings();
this.legendView.groupData.trigger("change");
expect(this.legendView.render).toHaveBeenCalled();
This works, but I'm looking for a better solution. Thanks
I have managed to achieve this using prototype patching. Before you create the instance of the view, spyOn the constructor's prototype.
spyOn(MyView.prototype, 'changeSelected');
var view = new MyView();
view.selectSomething();
expect(view.changeSelected).toHaveBeenCalled();
Instead of spying on the callback you might try spying on something.bind. Then test that bind was called w/ the appropriate arguments. This is working for me so far. I'm using sinon.js instead of jasmine's built-in spies. sinon.js makes it a bit easier to test for args passed to a method call in a stack of same method calls (eg a bunch of calls to bind in a view init). So I haven't tested this idea w/ jasmine alone but believe it should be possible.
spyOn(this.legendView.groupData, 'bind');
this.myView = new MyView();
expect(this.legendView.groupData.mostRecentCall.args).toEqual('change', this.myView.render); // example!! only works if testing a single call to bind or the last call in a series (ie mostRecentCall)
And w/ sinon.js
sinon.spy(this.legendView.groupData, 'bind');
this.myView = new MyView();
expect(this.legendView.groupData.bind.calledWith('change', this.myView.render); // works w/ any number of calls to bind
I solved this problem by spying on a function called by my render function. So in your example:
myView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "render");
something.bind("change", this.render);
},
someOtherFunction: function(){}, //this function only called from render
render: function(){ this.someOtherFunction(); /* rest of render function */ }
});
test looks like:
this.myView = new MyView();
spyOn(this.myView, "someOtherFunction");
this.myView.something.trigger("change");
expect(this.myView.someOtherFunction).toHaveBeenCalled();
then I wrote a separate test for whatever someOtherFunction does.
You should consider looking at Sinon.js. You could stub/mock the render() call and not even have to worry about 'someOtherFunction()'.
This may be too closely coupled with Backbone internals, but you can check the callback chain manually:
expect(this.legendView.groupData._callbacks['change']).toContain(this.myView.render)
I ran into the same problem and changed my Views code from:
this.model.on('change', this.render, this);
to:
this.model.on('change', function () {
this.render();
}, this);
And my jasmine tests worked as expected.