I am looking through the source of backbone, the events part, and for the most part it makes sense, except for the line in which context is stored twice.
In one case, it gets the value passed to on(). In the second instance, it gets the value passed, but if none is passed, it get the current context - this.
on: function(name, callback, context) {
if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
this._events || (this._events = {});
var list = this._events[name] || (this._events[name] = []);
list.push({callback: callback, context: context, ctx: context || this}); // here
return this;
},
One thing that is confusing is that sometimes this._events[name] is saved to local variable list (on and off) and other times it is saved to local variable effects (trigger).
Explicit uses of each
.ctx is used in triggerEvents().
.context is used in off().
ctx is used as the value of this inside the callback function when an event is triggered. So for example, inside the initialize method of a view you might do this:
this.model.on('change', this.render, this); // where 'this' is the view
Then inside the render method, when that event is triggered, this will be the view.
context is used so that certain usages of off can work, giving you more ways to selectively remove event callbacks. For example, if you wanted to remove the callbacks added to the model in the above example, you could do this:
this.model.off(null, null, this); // where 'this' is the view from above
This would remove any event handlers added to that model where the view was passed as the context.
That line would not remove an event added like:
this.model.on('change', this.foo, bar); // context is bar
It could be useful if you hooked up to several events and wanted to remove them all at once, while not removing any callbacks added with different context. However, now that Events has listenTo, it is probably easier to use that instead, since the view could just call stopListening.
The reason context and ctx are stored separately, is because they can be different values. context is always the third value passed to on and it could be null or undefined, while ctx is either that value, if available, or this.
Related
I have problems with data binding of my custom control.
My control inherits from sap.m.Input and extends it with a special value helper. One of my new properties of my new control is a simple header for the value help dialog. This is bound to an i18n model.
When I now use my control in a normal form, everything works. The title is bound correctly and shows the value of the bound i18n property in that model. If I use my control as a template in a column of a sap.ui.table control, it only shows the default value of the title property. Data binding does not seem to work. But is still working on the inherited properties (such as value).
For simplification here my control which now has only that title property and if value help is requested, it shows the current value in an alert box. In table, it shows the default value. And without table, it shows the bound value from i18n model.
Here the simplified control code:
sap.ui.define([
"sap/ui/core/Control",
"sap/m/Input",
], function(Control, Input) {
"use strict";
return Input.extend("DvpClsSuggestInput", {
"metadata": {
"properties": {
// Title of Value-Help Dialog
"vhTitle": {
type: "string",
defaultValue: "Title"
}
}
},
init: function() {
Input.prototype.init.apply(this, arguments);
this.setShowValueHelp(true);
this.attachValueHelpRequest(this.onValueHelpRequest.bind(this));
},
onValueHelpRequest: function(oEvent) {
var lvTitle = this.getVhTitle();
alert(lvTitle);
},
});
});
});
Usage in sap.ui.table.Table (which doesn't work and shows the default value of the title property):
<table:Column>
<m:Label text="{i18gn>HausWaehrung}" />
<table:template>
<dvp:MyInput
value="{ path: 'Inv>Hwaer', type: 'sap.ui.model.type.String' }"
vhTitle="{i18n>Currency}" />
</table:template>
</table:column>
Usage which works:
<VBox>
<dvp:MyInput
value="{ path: 'Cls>/Currency', type: 'sap.ui.model.type.String' }"
vhTitle="{i18n>Currency}" />
</VBox>
Once again, binding against the value property works in both ways. Problem only exists with my own property vhTitle. Any Ideas are welcome.
Do NOT use .bind when attaching event handlers to ManagedObject's events. The same applies to detaching event handlers. UI5 has its own documented mechanism for passing listener objects for those cases.
Example 1
Attaching / detaching a valueHelpRequest-handler using the corresponding APIs and passing values to the list of arguments as documented in the API reference:
myInput.attachValueHelpRequest(/*obj?,*/this.onValueHelpRequest, this); // No .bind!
myInput.detachValueHelpRequest(this.onValueHelpRequest, this); // Same references
Example 2
Attaching an event handler on control instantiation as documented in ManagedObject's API reference (All controls are ManagedObjects):
new MyInput({
// ...,
valueHelpRequest: [/*obj?,*/this.onValueHelpRequest, this]
});
Valid Names and Value Ranges:
[...]
For events, either a function (event handler) is accepted or an array of length 2 where the first element is a function and the 2nd element is an object to invoke the method on; or an array of length 3, where the first element is an arbitrary payload object, the second one is a function and the 3rd one is an object to invoke the method on [...].
Example 3 (For control developers)
In control definition, however, the listener can be omitted completely because the event provider itself (i.e. your control instance) becomes the listener by default if no listener object is passed.
this.attachValueHelpRequest(this.onValueHelpRequest); // the control instance will be used as the context in that event handler
This is described in the API reference as well:
If <oListener> is not specified, the handler function is called in the context of the event provider.
Drawbacks of using Function.prototype.bind in UI5
When calling .bind on a function, an entire new function is created!
const myFn = function() {};
myFn === myFn.bind(); // returns: false
Meaning if a handler is passed with .bind, that handler becomes never detachable because detachEvent awaits the same function reference and the same listener object reference as when attachEvent was called.
To make things worse, the function created with .bind won't let you change the previously passed thisArg (this) even if the EventProvider tries to call the function afterwards with a different thisArg. This limitation is described in the ECMAScript specification (See Note 2), and also the cause of the issue described in the question. When ManagedObject clones the template control for aggregation binding, the listener cannot be overwritten!
I have a context problem / design problem for my Backbone view.
Goal
The user selects a user from a list / user collection in a separate view.
The mentioned view passes an global event that the editUserView receives ("edit-contact").
The editUserView should receive this event and extract the (user) model.id attribute. By using this model.id I want to update the view with the corresponding object retrieved from the existing view model Tsms.Collection.Users.
Problem
The context passed to the updateView function is wrong, and thus I do not have access to the parent views .render() function. The debugger states "render() is not a function".
Since the context is not that of the parent view I am also unable to set the this.current variable.
How would I go about solving this problem?
View code
Tsms.Views.editUserView = Backbone.View.extend({
model: Tsms.Collections.Users,
initialize: function(options) {
Tsms.require_template('edituser')
this.template = _.template($('#template_edituser').html());
this.current = -1;
Tsms.vent.on('edit-contact', this.updateView)
},
updateView: function(model) {
this.current = model.id;
this.render();
},
render: function() {
this.$el.html(this.template(this.model.get(this.current).attributes));
return this;
}
});
Backbone's on actually takes three arguments:
on object.on(event, callback, [context])
[...]
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
The easiest and (currently) most idiomatic way to solve your problem would be to use the third context argument:
Tsms.vent.on('edit-contact', this.updateView, this);
While mu is too short is right, you should use Backbone's listenTo to avoid memory leaks (zombie views).
this.listenTo(Tsms.vent, 'edit-contact', this.updateView);
The context is automatically set to this, the calling view.
When remove is called on the view, stopListening is called and any references kept for events are removed.
Another reason to avoid on is that it should be the view that is responsible for the events it wants to handle, the event bus shouldn't have to know.
I am new to Backbone.js
I must be doing something wrong here. I'm trying to make a little demo to see what I can do with Backbone and basing it off some sample code.
I an get "Uncaught TypeError: Cannot call method toJSON of undefined".
I see why it is doing this, because the .bind("change", taskView.render) call is setting the context to the model (which the alert confirms) but it seems like there should at least be an argument to the render function to get access to the view. Maybe I am just going about it the wrong way? (see the sample code below).
task.bind("change", _.bind(taskView.render, taskView));
On Backbone Views and Models, the 'this' context for 'bind' is the calling object, so for model.bind('change', ...) this with be the model. For view.bind(... this will be the view.
You are getting the error because task.bind("change", _.bind(taskView.render, taskView)); sets this to be task when render runs, but this actually needs to be taskView.
Since you want to bind the model event to the view, as irvani suggests, the best way to do this is to use the .listenTo method (see: http://backbonejs.org/#Events-listenTo)
taskView.listenTo(task, 'change', taskView.render);
Depending on how you want the view lifecycle to work, you could also put the code in the initialize method of the view, which executes when the view is created. Then use stopListening in the remove method, to clear up the listener when the view is no longer in use.
As the task model is passed into the view, you get a fairly neat:
AskView = Backbone.Model.extend({
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
...
remove: function(){
this.stopListening(this.model);
...
}
});
var askView = new AskView({ model: task });
However, the one line solution to your problem is:
task.on("change", taskView.render, taskView);
bind is just an alias of on (see: http://backbonejs.org/#Events-on). The first argument is the event you are listening to, the second is the method to fire, and the third argument is the context to bind to, in this case, your taskView.
task.listenTo(model, 'change', taskView.render);
This says that listen to the model change and render the taskView on every change.
As irvani suggested, use listenTo instead.
object.listenTo(other, event, callback) Tell an object to listen to a particular event on an other object. The advantage of using this
form, instead of other.on(event, callback, object), is that listenTo
allows the object to keep track of the events, and they can be removed
all at once later on. The callback will always be called with object
as context.
In your case,
taskView.listenTo(task,"change",taskView.render);
assuming taskView is Backbone.View and task is a Backbone.Model.
The chances of memory leaks will be less when you use listenTo compared to using on.
If you must use on, you can specify the context as a third argument as and mu is too short suggested:
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
In your case:
task.on("change", taskView.render, taskView);
Analyzing this code I am not sure what is actually happening.I keep falling into this trap with JS especially with callbacks. Here is an example taken from backbone's documentation.
//creates a new constructor function with a promptColor function as an attribute.
var Sidebar = Backbone.Model.extend({
promptColor: function() {
var cssColor = prompt("Please enter a CSS color:");
this.set({color: cssColor});
}
});
// creates a property on the global window object called sidebar
window.sidebar = new Sidebar;
// .on is an event listener and passed a callback function taking the parameters of model and color. Here is my confusion, what does it do with the model parameter?
sidebar.on('change:color', function(model, color) {
$('#sidebar').css({background: color});
});
sidebar.set({color: 'white'});
sidebar.promptColor();
My main question is what does it do with the model parameter? What is it actually doing with the model parameter?
Thanks!
In your particular case the model parameter is of no real use since their is a 1-to-1 relationship between the change event and the model.
However, there are times when this is not the case. For example, imagine you have a backbone collection of models. You can attach a "change" event listener to the collection which will get called every time any model in the collection changes. In cases like this, it's helpful to know which model originated the "change" event.
I am learning backbone.js following this tutorial, but I run into problem understanding the first example:
(function($){
var ListView = Backbone.View.extend({
...
initialize: function(){
_.bindAll(this, 'render'); // fixes loss of context for 'this' within methods
this.render(); // not all views are self-rendering. This one is.
},
...
});
...
})(jQuery);
Q1: Why use (function($){})(jQuery); instead of a perfectly fine working (function(){})();?
Q2: What does _.bindAll(this, 'render') do? How does it fixes loss of context for 'this' within method?
Q1: by passing jquery in as a parameter you allow yourself 2 things:
if the need of using 2 versions of jquery arises - you are prepared
module pattern is probably better thought of as something well encapsulated and with well defined dependencies, so by declaring that jquery is a parameter - you declare clear dependency. Granted there are other ways of doing it (like RequireJS), but this is also a way
Q2: bindAll is a utility method from Underscore.js that binds this for a specific method - thus, when that method is invoked (as a callback for instance) the correct this would be used inside of it.
For example:
(function($){
var ListView = Backbone.View.extend({
...
initialize: function(){
// fixes loss of context for 'this' within methods
_.bindAll(this, 'render', 'makestuff');
this.render(); // not all views are self-rendering. This one is.
},
...
makestuff : function() {
...
this.stuff = ... // some action on the list's instance
}
});
...
})(jQuery);
and in some part of your code you doing something like:
var list = new ListView({...});
$('#button').on('click', list.makestuff);
this in makestuff method is a reference to the above list and not whatever context the on function is in when makestuff is actually invoked inside it.
The actual implementation relies on using apply and call functions to bind the context of function's execution to a specific object.
(function($){})(jQuery) passes jQuery to the self executing which is using it as $. This makes sure you can safely use the $ symbol inside the closure without having to worry about interference with other libraries or even other versions of jQuery. A common example for this practice would also be passing in window and document and then using shorthands inside the closure:
(function(w, d, $){
$(w).resize(function({}); //equals $(window) now
})(window, document, jQuery);
underscore's _.bindAll does the following:
Binds a number of methods on the object, specified by methodNames, to
be run in the context of that object whenever they are invoked. Very
handy for binding functions that are going to be used as event
handlers, which would otherwise be invoked with a fairly useless this.
If no methodNames are provided, all of the object's function
properties will be bound to it.
See the annotated source for the how.