I'm not a newbie to JavaScript, but often I find its flexible ways (like defining anonymous function as callbacks and their scope) quite confusing. One thing I'm still struggling with are closures and scopes.
Take this example (from a Backbone model):
'handleRemove': function() {
var thisModel = this;
this.view.$el.slideUp(400, function() { thisModel.view.remove(); });
},
After the model is removed/deleted, this will animate its view and finally remove it from the DOM. This works just fine - but initially I tried the following code:
'handleRemove': function() {
var thisModel = this;
this.view.$el.slideUp(400, thisModel.view.remove );
},
Which is basically the same, but without the function() {} wrapper for the remove() call.
Can someone explain why the latter code does not work? I get the following exception/backtrace:
Uncaught TypeError: Cannot call method 'remove' of undefined backbone-min.js:1272
_.extend.remove backbone-min.js:1272
jQuery.speed.opt.complete jquery-1.8.3.js:9154
jQuery.Callbacks.fire jquery-1.8.3.js:974
jQuery.Callbacks.self.fireWith jquery-1.8.3.js:1084
tick jquery-1.8.3.js:8653
jQuery.fx.tick
Thank you!
That's because the context (this) of the callback function changes to the element being animated by jQuery.
var obj = { fn: function() { // Used as below, the following will print:
alert(this === obj); // false
alert(this.tagName); // "DIV"
}};
$('<div>').slideUp(400, obj.fn);
Furthermore, Backbone's view.remove function looks like this (source code):
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
Because this is not the Backbone view object any more, $el is not defined. Hence you get the "Cannot call method 'remove' of undefined" error.
Another way to avoid the error is to use Underscore's _.bind method:
this.view.$el.slideUp(400, _.bind(thisModel.view.remove, this) );
Related
Recently I learned Javascript ES6 has classes so I tried them but my functions were always giving me errors saying they don't exist. So I made pseudo-classes using javascript associative arrays. It was working absolutely fine until I added some new methods.
Here is the error message I'm receiving:
EventListener.js:132 Uncaught TypeError: this.listen_for_tab_swap is not a function
at HTMLDivElement.<anonymous> (EventListener.js:132)
at Object.alert_eventlistener_of_tabs (EventListener.js:41)
at Object.new_tab (EventListener.js:59)
alert_eventlistener_of_tabs # EventListener.js:41
new_tab # EventListener.js:59
xhr.onreadystatechange # EventListener.js:94
XMLHttpRequest.send (async)
(anonymous) # EventListener.js:101
Here is the relevant code body:
const eventListener = {
listen_for_tab_swap: function() {
$(".footer button").on("click", function (event) {
file_tabs.show_tab(event.target.innerText);
});
},
listen_for_tabs_activation: function() {
$("#AZ_content").on("tabs_loaded", function () {
this.listen_for_tab_swap();
});
},
listen: function() {
$(function () {
console.log("Yeah, I'm listening...");
$(".menu").on("click", function (event) {
AZ_page_is_opened(event);
showTab(event, event.target.id.replace("Button", ""));
});
});
}
};
Please let me know if there is any additional information required to troubleshoot. Thanks in advance for any help.
In js this is dynamic. It depends on how a function is called. I'm assuming you're using jQuery because it looks like jQuery syntax at a glance so for your specific case all you need to know is that in jQuery (and also in regular javascript) the value of this in an onclick event is the element that triggered the event:
{
// ...
listen_for_tabs_activation: function() {
$("#AZ_content").on("tabs_loaded", function () {
this.listen_for_tab_swap(); // this is AZ_content
});
}
In the code above what you are doing is trying to call $("#AZ_content")[0].listen_for_tab_swap() and it is complaining that that HTML element does not have a method called listen_for_tab_swap
There are several ways to fix this. The simplest is probably do:
eventListener.listen_for_tab_swap();
You can also use arrow functions to bind this:
{
// ...
listen_for_tabs_activation: function() {
$("#AZ_content").on("tabs_loaded",() => { // this fixes `this`
this.listen_for_tab_swap();
});
}
There are several more ways to solve this. Check out this answer to another related question for how this actually behaves: How does the "this" keyword in Javascript act within an object literal?
I'm pretty sure this is a general JS question and not just limited to Backbone but I encountered this as I was learning Backbone. In the first example below, I don't get an error but that's not the same for the 2nd example. Can you explain what is going on behind the scenes that makes Chrome Dev Tools show the error in the latter example but not the first. My best guess is that when something is in a function (such as initialize), it doesn't get run right away as the browser is reading the script whereas if it's part of the class being defined (in this case, app.ItemView), the object context needs to be completed so the browser will need to read through this.model#get in order to build the context object.
app.ItemView = Backbone.View.extend({
initialize: function() {
this.context = {
title: this.model.get("title"),
completed: this.model.get("completed")
}
}
});
vs.
app.ItemView = Backbone.View.extend({
context: {
title: this.model.get("title"),
completed: this.model.get("completed")
}
});
Uncaught TypeError: Cannot read property 'get' of undefined
edit: it also doesn't work if the context in the first example isn't being defined appropriately -
app.ItemView = Backbone.View.extend({
initialize: function() {
this.context.title = this.model.get("title");
this.context.completed = this.model.get("completed");
this.render();
}
});
Uncaught TypeError: Cannot read property 'get' of undefined
EDIT Corrected explanation of this after reading mu is too short's comment.
The main reason for the error is that this in your second piece of code could refer to window or the caller of a function where Backbone.View.extend is being called.
That object doesn't have a model property, so it's undefined and calling get on undefined results in the error.
You need to make initialize a function because it will presumably be called by a Backbone.View object, which does have the model property available to it. In that context, this would refer to the Backbone.View object.
Using backbone.js I'm trying to fetch a model from my server and based on that, render an underscore template. I first tried it without using the result of the api call using the following render function:
render: function(options) {
this.ticketId = options.ticketId;
var that = this;
var ticketModel = new TicketModel({'id': that.ticketId});
$.when(ticketModel.fetch()).done(function(){
console.log(ticketModel); // this outputs the correct model
});
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
this.$el.html(template);
},
This works perfectly well. So I tried using the result of the api call to render the template:
render: function(options) {
this.ticketId = options.ticketId;
var that = this;
var ticketModel = new TicketModel({'id': this.ticketId});
$.when(ticketModel.fetch()).done(function(){
console.log(ticketModel);
console.log($('#tab-content-template').html());
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
this.$el.html(template);
});
},
but unfortunately, this results in an error saying
Uncaugt TypeError: Cannot read property 'html' of undefined.
The weird thing is that it outputs the html in the console correctly resulting from console.log($('#tab-content-template').html());. The error I get is on the line which reads this.$el.html(template);
How can it be that it first is able to get the html(), and after that it says it can't find the property html? I'm totally stuck here.. :S
All tips are welcome!
Your issue is this no longer refers to what you think it refers to. in your code you have placed
var that = this;
this is a common pattern/trick to allow the closure to retain the context of "this" when being executed. Inside your closure "that" would now refer to what you think "this" should refer to.
Try changing "this" to "that"
$.when(ticketModel.fetch()).done(function(){
console.log(ticketModel);
console.log($('#tab-content-template').html());
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
that.$el.html(template);
});
I usally make use of jQuery's proxy function that ensures when a function is executed you can be confident of the context in which it is running
$.when(ticketModel.fetch()).done($.proxy(function(){
console.log(ticketModel);
console.log($('#tab-content-template').html());
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
this.$el.html(template);
},this));
oh your other question about why is $('#tab-content-template').html() working, this is because you are using JQuery directly which is in the global namespace so there for accessible where as $el is a property of your view so if you can't access your view you can't access the property.
See http://backbonejs.org/#Model-fetch - in options argument it accepts success and error callbacks:
ticketModel.fetch({
success: function(model, response, options) {
// render the template
}
});
Also if you need to use a context of current view object in this callback you can use Underscore/Lo-Dash _.bind() function to pass the context:
ticketModel.fetch({
success: _.bind(function(model, response, options) {
// Render the template using some method of the view:
this.renderMyTemplate(model);
}, this)
});
or just pass the method itself:
ticketModel.fetch({
success: this.renderMyTemplate,
error: this.handleFetchError
});
You don't need a $.when here, backbone now returns a promise for fetch call. Below code should work for you. Also consider compiling template outside render function. Compiling template is bit heavy task, should be cached once done.
var _this = this;
ticketModel.fetch().done(function () {
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
_this.$el.html(template);
});
I have a fundamental misunderstanding of one of my numerous errors. I use jquery.
I have an object defined as:
var terms = {};
terms.clear_history = function(a, b)
{ /* DO SOMETHING */ }
I can call the terms.clear_history(1,2) function in my main js file, no problem. But when I try to call it from the "click" of a <a/> element:
$(document).on('click', '#clearterms', function(){
terms.clear_history(1, 2);
});
it gives me the following error:
Uncaught TypeError: Object # has no method 'clear_history'
I understand that I don't understand something fundamental here...
Thank you!
It sounds like a scope issue. Maybe the terms in the global scope the same as the one assigned clear_history given the method.
also, you don't want to name your param as this which is a reserved keyword in JS.
try this:
window.terms = {};
window.terms.clear_history = function(foo,bar){console.log(foo,bar);};
//then later:
$(document).on('click', '#clearterms', function(){
window.terms.clear_history(1, 2);
});
Almost all of the examples in the jQuery tutorials that I've read, usually use one major public function for their selecting plugin. When I say 'selecting' plugin, I mean one that is not simply a static function extended onto jQuery.
For example:
(function($) {
jQuery.fn.actionList = function(options) {
var opts = $.extend({}, $.fn.actionList.defaults, options);
return this.each(function(){
alert(this);
});
};
$.fn.actionList.defaults = {
listHtml: '<div>Set the list html</div>'
};
})(jQuery);
but not:
jQuery.log = function(message) {
if(window.console) {
console.debug(message);
} else {
alert(message);
}
};
This works fine for most things, but what I would like to do is be able to call a second function on the object returned from the first call.
var actionBox = $('actionBox').actionList(options);
//Many light-cycles later
actionBox.refreshData(data);
or maybe even:
$('actionBox').actionList(options);
// laaateerr
$('actionBox').actionList.refreshData(data);
I'm guessing one or both of these is not possible or, at least not advisable, but I'm only now getting into the deepest aspects of jQuery and javascript.
Could someone explain how to do this, or if it's not possible or advisable, why? and what they would do instead?
Thanks for reading!
I'm not quite sure what you're getting at, but you can call a second function on the object returned from the first function - in fact, it is very much encouraged to return a jQuery object from your plugins, and the reason why you can chain commands in jQuery.
Using your examples
var actionBox = $('actionBox').actionList(options);
//Many light-cycles later
actionBox.refreshData(data);
would work fine, so long as both .actionList() and .refreshData(data) commands both return a jQuery object.
And
$('actionBox').actionList.refreshData(data);
would need to be
$('actionBox').actionList().refreshData(data);
EDIT:
Looking at the jQuery source code,
jQuery.fn = jQuery.prototype = {
/*
Load of 'property' functions of jQuery object...
*/
}
so, adding properties (a.k.a plugins) to jQuery.fn extends the prototype of the jQuery object. When you call
$(selector, context);
a new jQuery object is returned, using the init property function of the jQuery object
jQuery = window.jQuery = window.$ = function( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init( selector, context );
},
I think I've got a plugin that might be very useful for you. It allows you to apply any constructor/object to jQuery as it's own namespace AND you can use 'this' as you would normally with jQuery to refer to the object set. Using this[methodName] will call a method on your object, etc.
http://code.google.com/p/jquery-plugin-dev/source/browse/trunk/jquery.plugin.js
Some code samples are here:
http://groups.google.com/group/jquery-dev/browse_thread/thread/664cb89b43ccb92c/34f74665423f73c9?lnk=gst&q=structure+plugin+authoring#34f74665423f73c9
It's about halfway down the page.
I hope you find it useful!