Scope problems when using setTimeout in backbone.js - javascript

I am trying to toggle a state variable in my Backbone collection ("Posts") after a certain period of time (called from a view), and am trying to use setTimeout. However, I think I am screwing up my scope as my toggle function is not working (it is getting called, but it is not changing properly).
If I use
setTimeout(this.model.collection.toggleReadyToPreloadNewPost, 1000);
, the code does not work, while if I use
this.model.collection.toggleReadyToPreloadNewPost();
it toggles it correctly. I was wondering how I can solve this?
Backbone View
//ensures namespace is not already taken yet
wedding.views = wedding.views || {};
//each PostView corresponds to a single post container
//which contains the user name, user comment, and a photo
wedding.views.PostView = Backbone.View.extend({
tagName: "li",
template: "#item-template",
className: "hideInitially post",
initialize: function() {
_.bindAll(this);
this.template = _.template($(this.template).html());
},
render: function() {
this.preload();
return this;
},
//preloads an image and only after it is done, then append
preload: function() {
var image = new Image();
image.src = this.model.get("src");
this.model.incrementTimesSeen();
//hides the element initially, waits for it to finish preloading, then slides it down
//updates lastSeen only after the image is displayed
image.onload = $.proxy(function() {
var html = this.template( {model: this.model.toJSON()} );
this.model.setLastSeen();
//shows the image by sliding down; once done, remove the hideInitially class
this.$el.hide().append(html).slideDown();
this.$el.removeClass("hideInitially");
setTimeout(this.model.collection.toggleReadyToPreloadNewPost, 1000);
}, this);
}
});
Backbone Collection
//check if namespace is already occupied
wedding.collections = wedding.collections || {};
wedding.collections.Posts = Backbone.Collection.extend({
model: wedding.models.Post,
initialize: function() {
this.readyToPreloadNewPost = 1;
},
//toggles "readyToPreloadNewPost" between 1 and 0
toggleReadyToPreloadNewPost: function() {
this.readyToPreloadNewPost = this.readyToPreloadNewPost ? 0 : 1;
}
});

When you do this:
setTimeout(this.model.collection.toggleReadyToPreloadNewPost, 1000);
You're just handing setTimeout the plain unbound toggleReadyToPreloadNewPost function and setTimeout will call it as a simple function. The result is that this will be window inside toggleReadyToPreloadNewPost when setTimeout calls the function.
You can get the right this by wrapping your method call in an anonymous function:
var _this = this;
setTimeout(function() {
_this.model.collection.toggleReadyToPreloadNewPost();
}, 1000);
You can also use _.bind:
setTimeout(
_(this.model.collection.toggleReadyToPreloadNewPost).bind(this.model.collection),
1000
);
You could also use _.bindAll inside the collection's initialize to always bind that method to the appropriate this:
wedding.collections.Posts = Backbone.Collection.extend({
initialize: function() {
_.bindAll(this, 'toggleReadyToPreloadNewPost');
//...
}
And then your original
setTimeout(this.model.collection.toggleReadyToPreloadNewPost, 1000);
should do the right thing. You'd only want to go this route if you always wanted toggleReadyToPreloadNewPost to be bound, I'd probably go with the first one instead.

Related

Backbone view events do not fire

I have a simple backbone view as follows:
/**
* Renders a form view for an event object.
*/
APP.EventFormView = Backbone.View.extend({
tagName: 'form',
events: {
'keydown': 'keyPressed',
'focus input': 'inputChanged',
'change select': 'selectChanged',
'change textarea': 'textareaChanged'
},
initialize: function() {
this.template = _.template($('#newevent-form').html());
this.listenTo(this.model, 'change', this.render);
this.listenTo(APP.eventTypes, 'update', this.render);
this.listenTo(APP.selectedEvent, 'update', this.render);
},
render: function() {
var modelJSON = this.model.toJSON();
if ('id' in modelJSON && modelJSON.id !== "") {
this.loadForm();
} else if (!('id' in modelJSON) || modelJSON.id === "") {
this.loadForm();
} else {
this.$el.html('');
}
return this;
},
loadForm: function() {
var templateData = $.extend(this.model.toJSON(),
{"event_types":APP.eventTypes.toJSON()});
this.$el.html('');
this.$el.html(this.template($.extend(this.model.toJSON(),
{event_types: APP.eventTypes.toJSON()})));
$('.ev-main-container').html('').html(this.el);
},
inputChanged: function(e) {
console.log('inputChanged');
},
selectChanged: function(e) {
console.log('selectChanged');
},
textareaChanged: function(e) {
console.log('textareaChanged');
},
keyPressed: function(e) {
console.log('key pressed');
}
});
I initialize this view as follows under document.ready:
// Initialize the form view
APP.selectedEvent = APP.selectedEvent || new APP.Event();
APP.eventFormView = new APP.EventFormView({model: APP.selectedEvent});
APP.eventFormView.render();
But none of the events I have defined are firing for some reason, What is it that I am doing wrong here ?
Update:
Ok, I fugred out if i remove $('.ev-main-container').html('').html(this.el); from the loadForm method and instead intialize the view as follows, it works:
APP.eventFormView = new APP.EventFormView({
model: APP.selectedEvent,
el: $('.ev-main-container'),
});
I was able to resolve it but I still don't understand why this happens, could anyone throw a little light on what's going on and how this works.
jQuery's html function has a side effect that many people seem to forget about, from the fine manual:
jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Consider what that means when you do something like this:
container.html(view.el);
container.html(view.el);
Everything will be fine after the first container.html() call. But the second will "remove ... event handlers from child elements" (such as view.el) before adding the new content. So after the second container.html() call, all the events on view.el are gone. Sound familiar?
You have lots of things that will call render on your view and render will eventually do this:
$('.ev-main-container').html('').html(this.el);
Your events will silently disappear the second time that gets called but the HTML will look just fine.
Consider this simplified example (http://jsfiddle.net/ambiguous/otnyv93e/):
var V = Backbone.View.extend({
tagName: 'form',
events: {
'click button': 'clicked'
},
initialize: function() {
this.template = _.template($('#t').html());
},
render: function() {
this.$el.html('');
this.$el.html(this.template());
$('.ev-main-container').html('').html(this.el);
return this;
},
clicked: function() {
console.log('clicked');
}
});
var v = new V;
v.render();
$('#re-render').click(function() {
v.render();
console.log('Re-rendered');
});
and you'll see exactly your problem.
If you make the view's el the .ev-main-container then you'll be using html() to alter the contents of el rather than altering the contents of the element that contains el. Once you're working entirely inside the el you're no longer accidentally re-using an element and no longer accidentally removing the event bindings from that element.
My rules of thumb for preventing event problems with Backbone:
Never attach views to existing DOM nodes, always let views create and own their own el and let the caller put that el in a container.
Call remove on views to dispose of them when they're no longer needed.
Don't try to re-use views, create them when you need them and remove them when you don't need them.
No view references anything outside its el.
There are exceptions (of course) and this approach won't solve everything but it is a good starting point and avoids most of the common problems.

Binding custom listener to Backbone View

I am new to javascript and Backbone.js. I would like to bind a custom listener to a Backbone view on initialization. For example, I would like to achieve something like this:
var CampaignListView = Backbone.View.extend({
initialize: function(){
this.on("customFunc")
},
customFunc: function() {
if (this.$el.scrollTop() == 500 ) {
console.log("this has occurred, time to do stuff")
}
}
)}
That whenever a user scrolls to a specified position, I can execute some code.
Thanks.
I'm assuming this is something you want to happen when the window scrolls, no? In that case you have a few options:
The first is something a bit more familiar, and close to what you have, using the remove method to cleanup that binding:
var CampaignListView = Backbone.View.extend({
initialize: function(){
// You probably want to add an identifier to the event name, like 'scroll.campainlist' or something
$(window).on('scroll', _.bind(this.customFunc, this));
},
customFunc: function() {
if (this.$el.scrollTop() == 500 ) {
console.log("this has occurred, time to do stuff")
}
},
remove: function() {
$(window).off('scroll');
Backbone.View.prototype.remove.call(this);
}
)}
The other option is to use an Event Aggregator like so:
var vent = _.extend({}, Backbone.Events);
$(window).on('scroll', function(ev) {
vent.trigger('window:scroll', ev);
});
var CampaignListView = Backbone.View.extend({
initialize: function(){
this.listenTo(vent, 'window:scroll', this.customFunc);
},
customFunc: function() {
if (this.$el.scrollTop() == 500 ) {
console.log("this has occurred, time to do stuff")
}
}
)}
You can make use of delegateEvents in Backbone Views. For example you could attach to the scroll event of your view, but of course it will fire with every scroll. I put together a quick and very simple jsfiddle here based on your example. Below is some of the JavaScript. Notice the use of the events{} in the code.
var CampainListView = Backbone.View.extend({
el: '#list',
events: {
'scroll': 'customFunc'
},
initialize: function() { },
render: function() {
this.$el.html(sampleHtml);
return this;
},
customFunc: function() {
// console.log('scrolling... top is ' + this.$el.scrollTop());
if (this.$el.scrollTop() >= 100 ) {
console.log("this has occurred, time to do stuff")
}
}
});
var view = new CampainListView();
view.render();

backbone events not working

I'm trying to manually trigger a click event right after the html has been rendered but it's not working.
To simplify and verify that it's not working I tried this code:
var _testView = Backbone.View.extend({
events : {
'click a' : 'sayHi'
},
initialize : function() {
this.render();
this.$el.find('a').trigger('click');
},
render : function() {
$(document.body).html(
this.$el.html('alert hi')
);
},
sayHi : function() {
alert('Hi');
return false;
}
});
var y = new _testView;
I'm trying to manually trigger the click event but it's not being triggered. If I'm going to put the trigger in a setTimeout with a delay of 500 it will work. I don't know why.... thx
I found the answer. I looked at the Backbone core and I see that initialize method is being called first before attaching the events to the view.
View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
You are calling the click event on the element which haven't been yet created. You should call the function when the render is finished or you can just call this.sayHi() instead of triggering the click.
hjuster is correct. the $(document.body) wait for the 'document ready' event, and you are calling new _testView before the document is ready. You can change your intialize to this - it queues up the trigger to execute after the document is ready.
initialize : function() {
this.render();
var self = this;
$(function(){
self.$el.find('a').trigger('click');
});
},
I added the var 'self' since you can't reference 'this' to get your _testView object in the function.
It works in this fiddle

How to remove and add a view using an if statement using backbone

I'm trying to remove my old carView and add the next one once the NEXT button is clicked.
Everything is coming out of a JSON file and is incrementing correctly but I want to view to also change.
Here's the code for my view:
window.CarContainerView = Backbone.View.extend({
el: $('#car-container'),
template:_.template($('#tpl-car-container').html()),
initialize:function () {
_.bindAll(this, 'clickNext');
this.car_number = 0;
this.car_model = this.model.get('carCollection').models[this.question_number];
this.question_view = null;
},
render:function () {
$(this.el).html(this.template());
this.car_view = new CarView({el: $(this.el).find('#car'), model: this.car_model});
this.question_view.render();
$('#next').bind('click', this.clickNext);
return this;
},
createNewCar: function () {
//build
console.log('createNewCar');
if(condition) {
//if the next button is pressed, clear screen and add new screen
}
},
clickNext: function () {
this.car_number++;
console.log(this.car_number);
createNewCar();
},
clickPrevious: function () {
}
});
Comments explain the changes. Basically, create a new CarView each time. And don't pass in the el to the view, else when you call remove that element will be gone. Instead, render the new view into #car each time.
window.CarContainerView = Backbone.View.extend({
el: $('#car-container'),
template:_.template($('#tpl-car-container').html()),
// use events hash instead of directly using jquery.
events: {
'click #next': 'clickNext'
},
initialize:function () {
// no need to use bindAll since using events hash
// binds the context of clickNext to this view already.
this.car_number = 0;
},
render:function () {
// use this.$el instead.
this.$el.html(this.template());
this.createNewCar();
return this;
},
createNewCar: function () {
if(this.car_view){
// cleanup old view.
this.car_view.remove();
}
// do some bounds checking here, or in clickNext... or both!
var car_model = this.model.get('carCollection').models[this.car_number];
// create a new view for the new car.
this.car_view = new CarView({model: car_model});
// render into #car instead of passing `el` into the view.
// let Backbone generate a div for the view, you dont need to
// set an `el` in the CarView either.
this.$('#car').html(this.car_view.render().el);
},
clickNext: function () {
this.car_number++;
this.createNewCar();
},
clickPrevious: function () {
}
});

Backbone.js views - binding event to element outside of "el"

The 2nd answer to this question nicely explains how event declarations in Backbone.js views are scoped to the view's el element.
It seems like a reasonable use case to want to bind an event to an element outside the scope of el, e.g. a button on a different part of the page.
What is the best way of achieving this?
there is not really a reason you would want to bind to an element outside the view,
there are other methods for that.
that element is most likely in it's own view, (if not, think about giving it a view!)
since it is in it's own view, why don't you just do the binding there, and in the callback Function,
use .trigger(); to trigger an event.
subscribe to that event in your current view, and fire the right code when the event is triggered.
take a look at this example in JSFiddle, http://jsfiddle.net/xsvUJ/2/
this is the code used:
var app = {views: {}};
app.user = Backbone.Model.extend({
defaults: { name: 'Sander' },
promptName: function(){
var newname = prompt("Please may i have your name?:");
this.set({name: newname});
}
});
app.views.user = Backbone.View.extend({
el: '#user',
initialize: function(){
_.bindAll(this, "render", "myEventCatcher", "updateName");
this.model.bind("myEvent", this.myEventCatcher);
this.model.bind("change:name", this.updateName);
this.el = $(this.el);
},
render: function () {
$('h1',this.el).html('Welcome,<span class="name"> </span>');
return this;
},
updateName: function() {
var newname = this.model.get('name');
console.log(this.el, newname);
$('span.name', this.el).text(newname);
},
myEventCatcher: function(e) {
// event is caught, now do something... lets ask the user for it's name and add it in the view...
var color = this.el.hasClass('eventHappened') ? 'black' : 'red';
alert('directly subscribed to a custom event ... changing background color to ' + color);
this.el.toggleClass('eventHappened');
}
});
app.views.sidebar = Backbone.View.extend({
el: '#sidebar',
events: {
"click #fireEvent" : "myClickHandler"
},
initialize: function(){
_.bindAll(this, "myClickHandler");
},
myClickHandler: function(e) {
window.user.trigger("myEvent");
window.user.promptName();
}
});
$(function(){
window.user = new app.user({name: "sander houttekier"});
var userView = new app.views.user({model: window.user}).render();
var sidebarView = new app.views.sidebar({});
});
Update: This answer is no longer valid/right. Please see other answers below!
Why do you want to do this?
Apart from that, you could always just bind it using regular jQuery handlers. E.g.
$("#outside-element").click(this.myViewFunction);
IIRC, Backbone.js just uses the regular jQuery handlers, so you're essentially doing the same thing, but breaking the scope :)

Categories

Resources