Backbone: event lost in re-render - javascript

I have super-View who is in charge of rendering sub-Views. When I re-render the super-View all the events in the sub-Views are lost.
This is an example:
var SubView = Backbone.View.extend({
events: {
"click": "click"
},
click: function(){
console.log( "click!" );
},
render: function(){
this.$el.html( "click me" );
return this;
}
});
var Composer = Backbone.View.extend({
initialize: function(){
this.subView = new SubView();
},
render: function(){
this.$el.html( this.subView.render().el );
}
});
var composer = new Composer({el: $('#composer')});
composer.render();
When I click in the click me div the event is triggered. If I execute composer.render() again everything looks pretty the same but the click event is not triggered any more.
Check the working jsFiddle.

When you do this:
this.$el.html( this.subView.render().el );
You're effectively saying this:
this.$el.empty();
this.$el.append( this.subView.render().el );
and empty kills the events on everything inside this.$el:
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So you lose the delegate call that binds events on this.subView and the SubView#render won't rebind them.
You need to slip a this.subView.delegateEvents() call into this.$el.html() but you need it to happen after the empty(). You could do it like this:
render: function(){
console.log( "Composer.render" );
this.$el.empty();
this.subView.delegateEvents();
this.$el.append( this.subView.render().el );
return this;
}
Demo: http://jsfiddle.net/ambiguous/57maA/1/
Or like this:
render: function(){
console.log( "Composer.render" );
this.$el.html( this.subView.render().el );
this.subView.delegateEvents();
return this;
}
Demo: http://jsfiddle.net/ambiguous/4qrRa/
Or you could remove and re-create the this.subView when rendering and sidestep the problem that way (but this might cause other problems...).

There's a simpler solution here that doesn't blow away the event registrations in the first place: jQuery.detach().
http://jsfiddle.net/ypG8U/1/
this.subView.render().$el.detach().appendTo( this.$el );
This variation is probably preferable for performance reasons though:
http://jsfiddle.net/ypG8U/2/
this.subView.$el.detach();
this.subView.render().$el.appendTo( this.$el );
// or
this.$el.append( this.subView.render().el );
Obviously this is a simplification that matches the example, where the sub view is the only content of the parent. If that was really the case, you could just re-render the sub view. If there were other content you could do something like:
var children = array[];
this.$el.children().detach();
children.push( subView.render().el );
// ...
this.$el.append( children );
or
_( this.subViews ).each( function ( subView ) {
subView.$el.detach();
} );
// ...
Also, in your original code, and repeated in #mu's answer, a DOM object is passed to jQuery.html(), but that method is only documented as accepting strings of HTML:
this.$el.html( this.subView.render().el );
Documented signature for jQuery.html():
.html( htmlString )
http://api.jquery.com/html/#html2

When using $(el).empty() it removes all the child elements in the selected element AND removes ALL the events (and data) that are bound to any (child) elements inside of the selected element (el).
To keep the events bound to the child elements, but still remove the child elements, use:
$(el).children().detach(); instead of $(.el).empty();
This will allow your view to rerender successfully with the events still bound and working.

Related

Events Wont work on Backbone.js

I created a view and has the ff codes:
var app = app || {};
app.singleFlowerView = Backbone.View.extend({
tagName: 'article',
className: 'flowerListItem',
// tells where to apply the views
template: _.template( $("#flowerElement").html() ),
// render
render: function(){
var flowerTemplate = this.template(this.model.toJSON());
// el contains all the prop above and pass to backbone
this.$el.html(flowerTemplate);
return this;
},
events: {
'mouseover': 'addBgColor',
'mouseout': 'removeBgColor'
},
addBgColor: function(){
this.$el.addBgColor('bgColorImage');
},
removeBgColor: function(){
this.$el.removeBgColor('bgColorImage');
}
});
When I run this to my HTML file I got the error addBgColor and removeBgColor is not a function. I have the CSS for this and all the models and views were set up.
Am I missing something here? Any idea why events doesn't work?
this.$el.addBgColor is the problem.
The events are triggering but you're calling addBgColor on the $el jQuery object, which is not a jQuery function, like the error message is telling you.
Check what's the difference between $el and el.
Tony, your events are cool and they are running they're just not doing anything.
this.addBgColor() will call your function in a view.
this.$el is referring to the html and there's no property called addBgColor assigned to $el.
You need to do something like change the class on your tag with the functions like so...
addBgColor: function(){
this.$el.className = 'bgColorImage'
},
.bgColorImage {
background-image: url('bgColorImage.jpg');
}

Backbone collection is not updating when api is polled

I'm having some trouble getting change events to fire when a model is updated via polling of an endpoint. I'm pretty sure this is because the collection is not actually updated. I'm using the new option (update: true) in Backbone 0.9.9 that tries to intelligently update a collection rather than resetting it completely.
When I insert a console.log(this) at the end of the updateClientCollection function, it appears that this.clientCollection is not updating when updateClientCollection is called via setInterval. However, I do see that the endpoint is being polled and the endpoint is returning new and different values for clients.
managementApp.ClientListView = Backbone.View.extend({
className: 'management-client-list',
template: _.template( $('#client-list-template').text() ),
initialize: function() {
_.bindAll( this );
this.jobId = this.options.jobId
//the view owns the client collection because there are many client lists on a page
this.clientCollection = new GH.Models.ClientStatusCollection();
this.clientCollection.on( 'reset', this.addAllClients );
//using the initial reset event to trigger this view's rendering
this.clientCollection.fetch({
data: {'job': this.jobId}
});
//start polling client status endpoint every 60s
this.intervalId = setInterval( this.updateClientCollection.bind(this), 60000 );
},
updateClientCollection: function() {
//don't want to fire a reset as we don't need new view, just to rerender
//with updated info
this.clientCollection.fetch({
data: {'job': this.jobId},
update: true,
reset: false
});
},
render: function() {
this.$el.html( this.template() );
return this;
},
addOneClient: function( client ) {
var view = new managementApp.ClientView({model: client});
this.$el.find( 'ul.client-list' ).append( view.render().el );
},
addAllClients: function() {
if (this.clientCollection.length === 0) {
this.$el.find( 'ul.client-list' ).append( 'No clients registered' );
return;
}
this.$el.find( 'ul.client-list' ).empty();
this.clientCollection.each( this.addOneClient, this );
}
});
managementApp.ClientView = Backbone.View.extend({
tagName: 'li',
className: 'management-client-item',
template: _.template( $('#client-item-template').text() ),
initialize: function() {
_.bindAll( this );
this.model.on( 'change', this.render );
},
render: function() {
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
From what I can gather from your code, you're only binding on the reset event of the collection.
According to the docs, Backbone.Collection uses the .update() method after fetching when you pass { update: true } as part of your fetch options.
Backbone.Collection.update() fires relevant add, change and remove events for each model. You'll need to bind to these as well and perform the relevant functions to update your UI.
In your case, you could bind to your existing addOneClient method to the add event on your collection.
In your ClientView class, you can bind to the change and remove events to re-render and remove the view respectively. Remember to use listenTo() so the ClientView object can easily clean-up the events when it remove()'s itself.

View Events not firing on created elements?

Trying to create a todo example app to mess around with backbone. I cannot figure out why the click event for the checkbox of a task is not firing. Here is my code for the TaskCollection, TaskView, and TaskListView:
$(document).ready(function() {
Task = Backbone.Model.extend({});
TaskCollection = Backbone.Collection.extend({
model: 'Task'
});
TaskView = Backbone.View.extend({
tagName: "li",
className: "task",
template: $("#task-template").html(),
initialize: function(options) {
if(options.model) {
this.model = options.model
}
this.model.bind('change',this.render,this);
this.render();
},
events: {
"click .task-complete" : "toggleComplete"
},
render: function(){
model_data = this.model.toJSON();
return $(_.template(this.template, model_data));
},
toggleComplete: function() {
//not calling this function
console.log("toggling task completeness");
}
});
TaskListView = Backbone.View.extend({
el: $("#task-list"),
task_views: [],
initialize: function(options) {
task_collection.bind('add',this.addTask,this);
},
addTask: function(task){
task_li = new TaskView({'model' : task});
this.el.append(task_li.render());
this.task_views.push(task_li);
},
});
});
Template for the task:
<script type='text/template' id='task-template'>
<li class="task">
<input type='checkbox' title='mark complete' class='task-check' />
<span class='task-name'><%= name %></span>
</li>
</script>
I can't seem to figure out why the toggleComplete event will not fire for the tasks. how can I fix this?
The problem here is that the backbone events only set to the element of the view (this.el) when you create a new view. But in your case the element isn't used. So you have the tagName:li attribute in your view, which let backbone create a new li element, but you doesn't use it. All you return is a new list element created from your template but not the element backbone is creating, which you can access by this.el
So you have to add your events manually to your element created by your template using jQuery or add your template as innerHtml to your element:
(this.el.html($(_.template(this.template, model_data)))
Try changing the lines where you set your listeners using .bind() to use .live(). The important difference is .live() should be used when you want to bind listeners to elements that will be created after page load.
The newest version of jQuery does away with this bit of ugliness and simplifies the methods used to set event listeners.
Your event is binding to a class of .task-complete but the class on your checkbox is .task-check
Try modifying your render function to call delegateEvents() like so:
render: function(){
model_data = this.model.toJSON();
this.el = $(_.template(this.template, model_data));
this.delegateEvents();
return this.el;
},
You'd really be better off changing your template to not include the li and then return this.el instead of replacing it, but if you want the events to work you need to have this.el be the root element one way or another; delegateEvents() re-attaches the event stuff, so when you change this.el that should fix the issue.
#Andreas Köberle answers it correctly. You need to assign something to this.elto make events work.
I changed your template and your TaskView#render() function.
This JSFiddle has the changes applied.
New render function:
render: function(){
var model_data = this.model.toJSON();
var rendered_data = _.template(this.template, model_data);
$(this.el).html(rendered_data);
return this;
}
It is recommended that the render() returns this.
One line in your TaskListView#addTask function changes from this.el.append(task_li.render()); to this.el.append(task_li.render().el);.
Template change
Since we are using this.el in the render() function, we have to remove the <li> tag from the template.
<script type='text/template' id='task-template'>
<input type='checkbox' title='mark complete' class='task-complete' />
<span class='task-name'><%= name %></span>
</script>

Backbone and jQuery events

I have a simple task - retrieve click listener function from DOM element.
I've fased two problems:
I have no idea how to obtain event listener, that was set via addEventListener function
$(element).data('events') is always empty
Talking about first problem - I think it's not critical as I'm using this function only in one place. But the second problem is a huge pain...
I've tested on pure jQuery environment:
$(element).click(function(){})
$(element).data('events') /*contains events info*/
But with Backbone:
$(element).click(function(){})
$(element).data('events') /*alway empty*/
I'm not a JS guru but it seems like there no bound data at all... Maybe it's just a typical Backbone behaviour, but still - how can I retrieve event handler?
If you are using Backbone.js you should be managing your events inside a Backbone.View object and avoid capturing the event with JQuery directly.
You should try something like this:
var myBody = $( 'body' );
var myDIV = $( '<DIV id="contentDIV"></DIV>' );
myBody.append( myDIV );
var myButton = $( '<button id="aButton">test</button>' );
myDIV.append ( myButton );
var MyView = Backbone.View.extend({
el : myDIV,
events: { 'click button#aButton' : 'doSomething' }, //here you bound the
//event to a function.
initialize: function(){
_.bindAll(this, 'render')
},
render: function(){
myDIV.append('<br>working...');
},
doSomething: function(){
alert( 'doSomething function.. after click event' );
}
});
var myView = new MyView;
myView.render();
PS: A good tutorial for understanding how it works: http://arturadib.com/hello-backbonejs/

Trouble getting Backbone.js view event to fire

It appears as though the following code is getting inside initialize but my event doesn't appear to be firing.
What am I missing here?
var index = (function ($, window, document) {
var methods = {};
methods = {
init: function () {
},
getView: Backbone.View.extend({
el: $('.settings'),
events: {
'click .settings': 'addUl'
},
initialize: function () {
console.log('init');
},
render: function () {
},
addUl: function () {
console.log('addUI');
this.el.append("<ul> <li>hello world </li> </ul>");
}
})
};
return methods; } (jQuery, window, document));
var stuff = new index.getView();
Link to the jsbin
Remove the space in 'click .settings'
Actually remove .settings entirely.
'click .settings' is registering a click handler for a descendant of this.el that matches '.settings'.
In your example you want to register an event on this.el directly so you don't need the descendant selector.
The problem is that it is your view element ($el) that has the settings class and not a child.
click .settings tells backbone to bind a "click" event on the $el for any children that have .settings. However, because, it is $el which has the class settings the binding never match.
This is why when you remove .settings it works, because you say "any 'click' on $el"
The reason the documentation says click .blah is because it assumes that the html element(s) with the class='blah' are children of the $el element.
Hope this help.

Categories