this.model.on change event not firing - javascript

The "change" event is not firing in the following code.
var PageView = Backbone.View.extend({
el: $("body"),
initialize: function(){
this.model.on("change:loading", this.loader, this);
},
loader: function(){
if(this.model.get("loading")){
this.$el.find('.loader').fadeIn(700);
}
else
this.$el.find('.loader').fadeOut(700);
},
});
var PageModel = Backbone.Model.extend({
defaults: {
loading: null,
},
initialize: function(){
this.set({loading:false});
},
});
$(function(){
var pageModel = new PageModel({});
var pageView = new PageView({model: pageModel});
})
It works if I'm adding this in the model's initialize function:
setTimeout(function() {
this.set({'loading': 'false'});
}, 0);
I can leave it this way, but this is a bug.

The situation explained
Here's the order the code runs:
the model is created,
model's initialize function is called, setting the loading attribute to false,
then the model is passed to the view,
then a listener is registered for the "change:loading"
The event handler is never called because the event never occurs after it was registered.
Quick fix
First remove the set from the model.
var PageModel = Backbone.Model.extend({
defaults: {
loading: null
}
});
Then, after creating the view, set the loading attribute.
var pageModel = new PageModel();
var pageView = new PageView({ model: pageModel });
pageModel.set('loading', false); // now the event should trigger
Since the listener is now registered before the model's loading attribute is changed, the event handler will be called.
Optimized solution
Use Backbone's best practices:
Favor .listenTo over .on to avoid memory leaks
Cache jQuery objects
Try to avoid setting the el property on the view
A view is an atomic component that should only care about itself and its sub-views.
While in your case, it wouldn't matter much that you use the el property on the view, it still goes beyond the responsibilities of the view. Let the calling code deal with passing the element to use for this view.
var PageView = Backbone.View.extend({
initialize: function() {
this.model = new PageModel();
this.$loader = this.$('.loader');
this.listenTo(this.model, "change:loading", this.loader);
},
loader: function() {
this.$loader[this.model.get("loading")? 'fadeIn': 'fadeOut'](700);
},
render: function() {
this.loader();
return this;
}
});
Put the defaults where they belong.
var PageModel = Backbone.Model.extend({
defaults: {
loading: false
}
});
Here we choose the body as the element to use for the view, using the el option, and then call render when ready.
$(function() {
var pageView = new PageView({ el: 'body' }).render();
});
The event won't be triggered by the listener right away, instead, we use the render function to put the view in its default state. Then, any subsequent changes of the loading attribute will trigger the callback.
I have listed the most useful answers I've written about Backbone on my profile page. You should take a look, it goes from the beginning to advanced and even provides some clever Backbone components that solves common problems (like detecting a click outside a view).

Related

listenOnce() running twice in backbone.js event

I had asked a question here that referred me to use using the backbone events. It works great, except that my event listener onFormSubmit() gets called twice. Normally I wouldn't care, but I have a function that toggles some states, and toggling this twice creates a problem.. I thought my view was being rendered twice (based on other answers on SO), but it does not appear to be so. I am trying to understand the 'why' behind what is happening here.. Here is some code (with non-relevant stuff removed)..
Here is my article form view that calls triggers the events on a form save and it gets triggered once (correct intended behavior) and it gets redirected back to the dashboard..
var PostView = Backbone.View.extend({
el: '#content',
articletemplate: _.template($('#article-template-add').html()),
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.articletemplate({}));
},
events: {
"click .save": "savePost",
},
savePost: function() {
var mypost = new PostModel();
this.model = mypost;
this.model.save(null, {
success: function(model) {
eventBus.trigger('formSubmitted', {
message: "form submitted"
});
app.navigate("/", false);
},
});
},
});
Here is my Dashboard view() that gets called after the form submit above.. Here is where onFormSubmit() executes twice (the console.log() gets printed twice).
var DashboardView = Backbone.View.extend({
el: '#content',
dashboardtemplate: _.template($('#dashboard-template').html()),
initialize: function() {
this.listenToOnce(eventBus, 'formSubmitted', this.onFormSubmit);
this.render();
},
onFormSubmit: function(datahere) {
console.log("onFormSubmit called"); // *** This gets printed twice
},
render: function() {
$(this.el).empty().html(this.dashboardtemplate(this.model.attributes));
},
});
Now, I am beginning to think that there might be some problem in the main app routing?
var AppRouter = Backbone.Router.extend({
routes: {
"": "dashboard",
"article/add": "addarticle",
},
dashboard: function() {
mydashview = new DashboardView();
},
addarticle: function() {
var articleView = new PostView();
},
});
var eventBus = _.extend({}, Backbone.Events);
var app = new AppRouter();
Backbone.history.start();
EDIT
I updated the savePost() to include that the trigger is called in the callback of the this.model.save()..I've forced it to create a dummy model instead of taking it from a form. The good news is that I was able to recreate the behavior here: http://jsfiddle.net/okswxngv/ If you open your console, you can see the onFormSubmit called printing twice.
Your problem is linked to the ghost view. Some call it zombie. The DashboardView is created every time you enter the root page, but never removed.
That is why he is going to exist, even if you link a new view to the #content div.
You can put a break point on DashboardView ->initialize and see that is called twice.
To better understand I have changed you code and added a name to the view (which is the date when it was created) and printed this name.
To get read of the problem you have to remove the unneeded view when you create a new one.

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.

Bind Model to a View asynchronously in Backbone

my problem is to bind a model, that I istanciate at certain time during my application, to a View that has been created at initialization time. Let me explain better:
In my route I istanciate a View:
...
var view = new app.FirstView();
view.render();
In
app.FirstView = Backbone.View.extend({
initialize: function(){
...
this.SidebarView = new app.SidebarView();
}
... });
At certain time X during my application I have this behaviour in an another view:
app.AnotherView = Backbone.View.extend({
...
onClickHandler: function(){
var aModel = app.aModel();
var view = app.aView({ model: aModel });
}
})
My problem now, is to bind aModel defined in the onClickHandler to SidebarView which I created and bounded to the FirstView at initialization time.
Please tell me if something is not clear.
Thanks in advance
one solution is to use an event for example,
In your AnotherView trigger a event and pass to model to View
onClickHandler: function(){
var aModel = app.aModel();
Backbone.trigger('model:assigned',aModel);
}
in your sidebarView's initialize you should listen for event
Backbone.on("model:assigned",function(passedModel){
this.model = passedModel;
})
Note1: You must be sure that sidebarView is initialized by the time you are triggering event.
Note2: Try avoiding Global events , i used as i don't know your code structure.

Backbone.js. Make change event correctly

I'm writing Todo app with Backbone.js
You can see part of my code below.
Model:
var Todo = Backbone.Model.extend({
defaults : {
title: 'Task Title',
complete: false
},
initialize: function(){
this.on("change:complete", function () {
alert("foo");
});
}
});
View:
var AppView = Backbone.View.extend({
collection: todoCollection,
el: 'body',
events: {
'click #tasks li .complete-task' : 'toggleComplete'
}
toggleComplete: function (e) {
var modelCid = $(e.target).parent('li').attr('id');
if ( this.collection.get(modelCid)['complete'] ){
this.collection.get(modelCid)['complete'] = false;
} else {
this.collection.get(modelCid)['complete'] = true;
};
}
});
But something working wrong and change event in the model doesn't working. I can't understand where I have mistakes.
Help me, please.
10q.
As per the Backbone Documentation:
Set model.set(attributes, [options])
Set a hash of attributes (one or
many) on the model. If any of the attributes change the model's state,
a "change" event will be triggered on the model. Change events for
specific attributes are also triggered, and you can bind to those as
well, for example: change:title, and change:content. You may also pass
individual keys and values.
So you need to be using the set method on the model for these events to be fired. So you would need to use something like this:
this.collection.get(modelCid).set('complete',false);

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