Backbone event callback is called when bound, not triggered - javascript

I am trying to implement a simple mediator pattern for events, but my event callbacks are being called when I register them with the mediator.
The Mediator is simply:
define(function(require){
'use strict';
var _ = require('underscore');
var Backbone = require('backbone');
return _.extend( Backbone.Events);
});
I have created a simple View with no backing model or template
define(function (require) {
'use strict';
var Backbone = require('backbone');
var mediator = require('mediator');
var Sandbox = Backbone.View.extend({
el: '.sandbox',
initialize: function () {
this.render();
this.bindEvents();
},
bindEvents:function(){
mediator.on("sandbox:change", this.changeText(), this );
},
render: function () {
$(this.el).html("SANDBOX VIEW IS WORKING");
},
changeText: function () {
$(this.el).html("THE TEXT HAS CHANGED");
}
});
return Sandbox;
});
When the view is loaded, the sandbox:change event is fired off and the changeText function is called even though nothing has called mediator.trigger('sandbox:change')
Why is the callback invoked when it is simply being bound to the mediator object?

When the browser evaluates your sandbox change event binding mediator.on("sandbox:change", this.changeText(), this ); the changeText function is being executed because the () are included. What you really want to do is pass the changeText function object to the event binding:
mediator.on("sandbox:change", this.changeText, this );
Remember that changeText, is an object that is a member of your view.
var Sandbox = Backbone.View.extend({
...
changeText: function () {...}
});
When you want to execute the function, use the parenthesis. When you want to refer to the object, omit the the parenthesis.

Related

Model.fetch() callback not working after separating Backbone.js objects with RequireJS

I'm splitting up my working Backbone.js application, into separate files for models, views and routers.
in my Router, I create a Model, perform its .fetch() and expect to reach the callback function. This, however, never happens.
return Backbone.Router.extend({
self : this,
initialize: function(){
require(['models/mymodel'],
function(MyModel) {
var myModel = new MyModel();
myModel.fetch({ success: self.callback, error: self.callback });
console.log(myModel.get('myAttr'));
//prints 'undefined', although can be seen in model.attributes
}
);
},
callback: function(){
console.log('Callback reached!); //is never printed
}
});
models/mymodel.js:
define(function (require) {
"use strict";
var $ = require('jquery'),
Backbone = require('backbone');
return Backbone.Model.extend({
url: function(){
return '/apicall';
}
});
});
One major problem in your code is this bit:
self : this,
The self field will be created in the object literal but not to a value which is sensible for what you want to do. (And how would you get this value anyway?) At the beginning of your initialize function. Removing the line above from your code and setting self like this at the beginning of initialize should give you what you want:
initialize: function(){
var self = this;
// The rest is identical.

Access view property when calling render as a callback

I have to use the guid variable in the render() function, but I can pass it only to the constructor. I this code:
app.views.CompanyView = Backbone.View.extend({
el: '#company-view',
guid: '',
initialize: function (options) {
this.guid = options.guid;
},
render: function () {
var guid = this.guid;
}
});
I create my view like this:
app.currentView = new app.views.CompanyView({guid: guid});
Then I pass the render() function as a parameter to use it as a callback:
function call(callback){
callback();
}
call(app.currentView.render);
I tried this.guid, options and this.options too, but all of them were undefined. Is there a way to pass this variable to the render() function without using it's arguments or global variables? Here is a JsFiddle example.
When you call render through this:
function call(callback){
callback();
}
You're calling it as a plain function so this inside render will be window. Remember that this in JavaScript depends on how the function is called, not how it is defined (unless of course you're playing with bound functions).
You have some options:
Bind render to the view using _.bindAll, _.bind, $.proxy, Function.bind, ...
initialize: function() {
_.bindAll(this, 'render');
}
Demo: http://jsfiddle.net/ambiguous/GsUfY/
The more common approach these days is to pass a context with the function and then whoever calls the callback uses the appropriate context using call or apply:
function call(callback, context){
callback.apply(context);
}
Demo: http://jsfiddle.net/ambiguous/LnwPr/
Do it yourself by hand:
call(function() { v.render() });
This one usually takes the form of var _this = this; followed by an anonymous function that uses _this.some_method() instead of just passing this.some_method as a callback.
Demo: http://jsfiddle.net/ambiguous/K2Xj4/
I prefer the second option.
I see. When your render() is called by the callback function, the caller of the method is no longer the view itself, so the "this" inside your render will be the caller of the call function().
see this fiddle:
http://jsfiddle.net/cn8nN/2/
var CompanyView = Backbone.View.extend({
initialize: function (options) {
this.guid = options.guid;
},
render: function () {
console.log('hello');
console.log(this);
}
});
var v = new CompanyView({guid: 'the guid'});
function call(callbcak) {
callbcak();
}
call(v.render);
if you open the console, you will see "this " is actually the window.
to work around this, you want to bind the context to the view it self.
to do that, use _.bindAll();
initialize: function (options) {
_.bindAll(this, "render");
this.guid = options.guid;
}
jsfiddle: http://jsfiddle.net/cn8nN/3/

Calling super class method in Backbone

var BaseView = Backbone.View.extend({
localizedTemplate : function (element) {
template = _.template(element.html());
return function (data) {
return template($.extend({}, data, resource));
};
}
});
var DerivedView = BaseView.extend({
initialize: function (options) {
this.model = options.model;
this.template = function () {
return this.localizedTemplate($("#someTemplate"));
};
},
render: function () {
var output = this.template(this.model.toJSON());
this.$el.append(output);
return this;
}
});
Why the above code is not working? why I am not able to call the someFunction in DerivedView? is there any way to achieve this?
I am using Backbone latest version.
When you do this:
this.template = function () {
return this.localizedTemplate($("#someTemplate"));
};
You're assigning a function to this.template. Note that localizedTemplate also returns a function:
return function (data) {
return template($.extend({}, data, resource));
};
That means that this.template is a function which returns a function and that second function is the one that wants this.model.toJSON() as an argument.
You're doing this:
var output = this.template(this.model.toJSON());
The function in this.template ignores its arguments and returns a function, that leaves you with this function:
function () {
return this.localizedTemplate($("#someTemplate"));
}
in output. You probably think output is a chunk of HTML at this point so you hand that to append:
this.$el.append(output);
But output is a function and what does append do when called with a function as its argument? jQuery calls that function like this:
function(index, html)
Type: Function()
A function that returns an HTML string, DOM element(s), or jQuery object to insert at the end of each element in the set of matched elements. Receives the index position of the element in the set and the old HTML value of the element as arguments. Within the function, this refers to the current element in the set.
So the output function will be called by jQuery's append and append will supply arguments that the compiled template function doesn't understand. The result is a big pile of confusion.
If you really want to do things like this then you'll want to call all the functions yourself so that you can get the right arguments to the right places:
var output = this.template()(this.model.toJSON());
// -----------------------^^
Demo: http://jsfiddle.net/ambiguous/YyJLR/
Or better, don't bother with all the extra wrappers at all. Say this in your view's initialize:
this.template = this.localizedTemplate($("#someTemplate"));
and then this in render:
var output = this.template(this.model.toJSON());
Demo: http://jsfiddle.net/ambiguous/BQjhS/
Also note that you don't need to this.model = options.model, the view constructor will do that for you:
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName and attributes.
var DerivedView = BaseView.extend({
someVariable: function(someData) {
return this.someFunction(someData);
}
});

Backbone this confusion

I have the following code:
var GoalPanelView = Backbone.View.extend({
// Bind to the goal panel DOM element
el: $("#sidebar-goals"),
// Initialize the collection
initialize: function() {
this.collection = Goals;
this.collection.bind('add', this.appendItem);
},
// Create a new goal when a user presses enter in the enter goal input
createOnEnter: function(e) {
if (e.keyCode != 13) return;
this.addItem();
//Goals.create(this.newAttributes());
},
// Add the goal item to the goal list
addItem: function() {
var goal = new Goal();
goal.set(this.newAttributes());
var goalsElem = this.el;
this.collection.add(goal);
$(this.el).children("#enter-goal").val('');
},
// Append DOM element to the parent el
appendItem: function(item) {
var goalView = new GoalView({
model: item,
});
$(this.elem).append(goalView.render().el);
}
});
My problem is inside of the appendItem function. When I use this inside of the appendItem function, I believe that it thinks that the this refers to the this.collection rather than the GoalPanelView. How would I get the this to refer to the GoalPanelView rather than the collection? I tried to pass another variable into the appendItem function which held the contents of this.elem, but it didn't seem to work.
One thing that worked was when I moved the appendItem function into the collection and changed the initialization to bind to this.collection.bind('add', appendItem); but I do not want to put the view stuff into the collection logic.
You can add a scope when binding an event handler, like so:
this.collection.bind('add', this.appendItem, this);
The scope sets the value of this inside the handler. In you case, the current object.
Edit: Javascript Garden has a great explaination why this.appendItem does not actually carry the scope of the function itself, it's just a function pointer, not a method pointer. One of the quirks of Javascript..
Edit 2 Backbone Reference - Events / on
Just to update (as of Backbone 0.9.2), the proper way to do this is:
initialize: function() {
this.collection.on("add", this.appendItem, this);
...
}
Depending on your use case, you may also want to consider:
initialize: function() {
this.listenTo(this.collection, "add", this.appendItem);
...
}
You can also use underscore's _.bindAll function in your initialize method:
initialize: function() {
_.bindAll(this);
this.collection = Goals;
this.collection.bind('add', this.appendItem);
}
Now any call to any method on GoalPanelView (e.g. appendItem) will be scoped such that references to this refer to the GoalPanelView instance.
You can also pass in a list of method names as strings if you don't want to scope all the methods of GoalPanelView
See here: http://underscorejs.org/#bindAll

Backbone.js view instance variables?

I'm learning Backbone.js and am trying to figure out whether it's possible to have instance variables in Backbone views.
My goal is to load a view's templates from an external file when a view is being instantiated. Currently I'm storing them in a global variable in the Backbone app's global namespace, but it would be cleaner to store the templates in a view's instance variables. Currently I have it set up like this:
var templates = {};
MessageView = Backbone.View.extend({
initialize: function() {
$.get('js/Test2Templates.tpl', function(doc) {
var tmpls = $(doc).filter('template');
templates['MessageView'] = [];
tmpls.each(function() {
templates.MessageView[this.id] = $.jqotec($.unescapeHTML(this.innerHTML));
});
});
},
render: function() {
var tpldata = {name: 'Ville', thing: 'Finland'};
$('#display').jqoteapp(templates.MessageView.greeting_template, tpldata);
},
events: {
"click input[type=button]": "additionalTransactions"
},
additionalTransactions: function() {
this.render();
}
});
But instead of using "templates" being defined as a global var, I'd like to create 'templates' in a view's initialize function, along these lines (but this doesn't work):
MessageView = Backbone.View.extend({
view_templates: {},
initialize: function() {
$.get('js/Test2Templates.tpl', function(doc) {
var tmpls = $(doc).filter('template');
tmpls.each(function() {
this.view_templates[this.id] = $.jqotec($.unescapeHTML(this.innerHTML));
});
});
},
render: function() {
var tpldata = {name: 'Ville', thing: 'Suomi'};
$('#display').jqoteapp(this.view_templates.greeting_template, tpldata);
},
events: {
"click input[type=button]": "additionalTransactions"
},
additionalTransactions: function() {
this.render();
}
});
This is probably (?) pretty straightforward and/or obvious, but me being somewhere on the Backbone.js learning curve, I'd much appreciate any help with this!! Thanks!
Your view_templates instance variable is fine (and a good idea as well). You just have to be sure that you're using the right this inside your $.get() callback and inside your tmpls.each() call. I think you want your initialize to look more like this:
initialize: function() {
this.view_templates = { };
var _this = this;
$.get('js/Test2Templates.tpl', function(doc) {
var tmpls = $(doc).filter('template');
tmpls.each(function() {
_this.view_templates[this.id] = $.jqotec($.unescapeHTML(this.innerHTML));
});
});
},
I'm not sure which this.id you want inside the tmpls.each() but I'm guessing that you want the DOM id attribute from the current template so I left it as this.id.
The this.view_templates assignment in your constructor (initialize) is needed because you presumably want each instance of the view to have its own copy of the array. Creating a new view instance doesn't do a deep copy of the the view so if you just have:
MessageView = Backbone.View.extend({
view_templates: {},
// ...
then all the instances will end up sharing the same view_templates object and view_templates will behave more like a class variable than an instance variable.
You can specify your instance variables in the view definition (i.e. the Backbone.View.extend() call) as a form of documentation but you will want to initialize any of them that should behave as an instance variable in your initialize method; read-only or "class variables" like events can be left as part of the view's definition.

Categories

Resources