How to unbind all the socket.io events from my backbone view? - backbone.js

I have a page which include two backbone views (views related to two template). I am changing content of one views based on clicking event on different items on another view. For this, Every time I click on any items in one view I just create a instance of another view which include some socket.io events. At the first time It's work well but everytime I click on item on first view it just create the instance of 2nd one so that all the socket.io events is binding. Except first click every time I click on items on first view and call an socket.io events, it fired more than one time based on how many click I have done to different items.
I know that every time I click an items it create an instance of a view with socket.io event bind. But I can not get the way to unbind the previous socket.io events.
I have tried to use this reference:
Backbone.js View removing and unbinding
But it is not working in my case. May be I did not use it in proper way.
Can anyone please give me a solution or way to unbind all the socket.io events binded before?
Here is my Clicking event from where I am creating a new instance of another view where all the socket.io events binds.
LoadQueueDetails: function (e) {
e.preventDefault();
var queues = new Queues();
queues.fetch({
data: $.param({ Code: this.model.get("QueueCode") }),
success: function () {
$("#grid21").html(new SearchResultListView({ collection: queues }).el);
},
error: function (queues) {
alert('error found in fetch queue details');
}
});
}
And here is my actual view where I bind all the socket.io events.
window.SearchResultListView = Backbone.View.extend({
initialize: function () {
this.collection.on('change', this.render, this);
this.render();
},
render: function () {
var Queues = this.collection;
var len = Queues.length;
$(this.el).html(this.template());
for (var i = 0; i < len; i++) {
$('.QueueListItem', this.el).append(new SearchResultListItemView({ model: Queues.models[i]}).render().el);
}
return this;
}
});
window.SearchResultListItemView = MainView.extend({
tagName: "tr",
initialize: function () {
this.__initialize();
var user;
if ($.super_cookie().check("user_cookie")) {
this.user = $.super_cookie().read_JSON("user_cookie");
}
this.model.bind("change", this.render, this);
this.model.on("destroy", this.close, this);
socket.emit('adduser', this.user.UserName, this.model.get("Code"));
},
events: {
"click a": "JoinQueue"
},
onClose: function(){
this.model.unbind("change", this.render);
},
close: function () {
this.remove();
this.unbind();
this.model.unbind("change", this.render);
},
socket_events: {
"updatechat": "updatechat",
"changeroom": "changedroom"
},
changedroom: function (username, data) {
alert(data);
socket.emit('switchRoom', data);
},
updatechat: function (username, data) {
alert(username);
alert(data);
},
JoinQueue: function (e) {
e.preventDefault();
if ($.super_cookie().check("user_cookie")) {
user = $.super_cookie().read_JSON("user_cookie");
}
socket.emit('sendchat', "new user");
},
render: function () {
var data = this.model.toJSON();
_.extend(data, this.attributes);
$(this.el).html(this.template(data));
return this;
}
});
window.Queue = Backbone.Model.extend({
urlRoot: "/queue",
initialize: function () {
},
defaults: {
_id:null,
Code: null,
ServiceEntityId: null,
ServiceEntityName:null,
Name: null,
NoOfWaiting: null,
ExpectedTimeOfService: null,
Status: null,
SmsCode: null
}
});
window.Queues = Backbone.Collection.extend({
model: Queue,
url: "/queue",
initialize: function () {
}
});
Backbone.View.prototype.close = function () {
this.remove();
this.unbind();
if (this.onClose) {
this.onClose();
}
}
And this is my main view to bind socket.io event in searchResultItemview.
var MainView = Backbone.View.extend({
initialize: function () {
this.__initialize();
},
__initialize: function () {
if (this.socket_events && _.size(this.socket_events) > 0) {
this.delegateSocketEvents(this.socket_events);
}
},
delegateSocketEvents: function (events) {
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) {
method = this[events[key]];
}
if (!method) {
throw new Error('Method "' + events[key] + '" does not exist');
}
method = _.bind(method, this);
socket.on(key, method);
};
}
});
For extra information:
1. I am opening socket connection globally. Like this :
var socket = io.connect('http://localhost:3000');
I am waiting for any kind of advice or solution to get out of this problem. Please feel free to ask any kind of inquiries.

Basically you have to do socket.removeListener for every socket.on when you close your View.
You can update your MainView and add a close method.
This is how it looks in my code (CoffeeScript)
close: ->
self = #
_.each #socket_events, (method, key) ->
method = self[self.socket_events[key]]
socket.removeListener key, method

Related

How to hook async Backbone event to display of HTML

What I am trying to do is make a call to the database and then display the result in some HTML. I have everything working (the data comes back from the database just fine), except I can't figure out to display the data.
I know that fetch() is async, but I'm not sure how to wire it into my collection view. Here is my Backbone:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
initialize: function(mains) {
this.fetch({success: function(main) {
$('#web-leads').html(main);
}});
},
url: '../leads/main_contact'
});
App.Views.Mains = Backbone.View.extend({
tagName: 'ul',
render: function() {
var ul = this.collection.each(this.addOne, this);
return ul;
},
addOne: function(main) {
var mainC = new App.Views.Main({ model: main});
this.$el.append(mainC.render().el);
return this;
}
});
App.Views.Main = Backbone.View.extend({
tagName: 'li',
template: template('mainContactTemplate'),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
main = new App.Views.Main();
mains = new App.Collections.Mains(main);
})();
What I need to be able to is call $('#web-leads').html() with the value returned from mains. How do I do that?
The general pattern for this sort of thing in Backbone is:
create a model or collection
pass that model/colleciton to a view
that view registers an event handler on the model/collection
the model/collection triggers an AJAX request (probably in response to a fetch call)
the view's event handler is triggered
the view's event handler updates the page
So, as mu is too short suggested, your best bet is to follow this pattern and have your view bind a handler to your collection's reset event.
It's worth mentioning however that reset won't always be the event you want to bind. For instance, you might not want to respond an AJAX request unless it changed attribute 'X' of the model. In that case you could instead bind to change:X, and then your handler would only be triggered if the AJAX response changed X.
To see all your possible options, see:
http://documentcloud.github.com/backbone/#Events-catalog
You were on the right track just needed to have the view listening to the Collection rather than the collection listening to the view.
The below is your code with the slight modification of who listens to who.
Why? Ideally we want the Collections to know nothing of the Views.
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
url: '../leads/main_contact'
});
App.Views.Mains = Backbone.View.extend({
tagName: 'ul',
initialize : function(){
this.collection.on('reset', this.render, this);
},
render: function() {
var ul = this.collection.each(this.addOne, this);
return ul;
},
addOne: function(main) {
var mainC = new App.Views.Main({ model: main});
this.$el.append(mainC.render().el);
return this;
}
});
App.Views.Main = Backbone.View.extend({
tagName: 'li',
template: template('mainContactTemplate'),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
mains = new App.Collections.Mains();
main = new App.Views.Main( {'collection' : mains} );
mains.fetch();
})();

Backbone, one field not set when calling view.render after model.save

I have the following problem. On a user-event (click on .twitterDefault) I call save event with
twitter : {
handle : handle,
ignore : false
}
Then the success function gets called and I set fields on the model (klout, twitter and tester). All fields are set (logging statements all print out appropiate objects.
However, then I call view.render() and here twitter is not set anymore. I have no idea why, there is no sync happening after the save so twitter does not get overwritten (additionally I made sure twitter is also saved on the server before the success method gets called).
Any help greatly appreciated!
Code as follows (stripped to improve readability)
$(function() {
var ContactModel,
ContactModelCollection,
ContactView,
ContactCollectionView,
contacts,
contactCollectionView;
//base model
ContactModel = Backbone.Model.extend({
defaults : {
},
initialize : function() {
}
});
ContactModelCollection = Backbone.Collection.extend({
model : ContactModel,
url : '/api/contacts',
comparator : function(contact) {
return contact.get('strength_of_relationship');
},
initialize : function() {
}
});
ContactView = Backbone.View.extend({
tagName : 'li', //attempting to create a new element
render: function() {
var compiled_tmpl = _.template($('#contact-template').html());
var html = compiled_tmpl(this.model.toJSON());
console.log('model.get("twitter")=('+JSON.stringify(this.model.get('twitter)'))+')');
console.log('model.get("klout")=('+JSON.stringify(this.model.get('klout'))+')');
console.log('model.get("tester")=('+JSON.stringify(this.model.get('tester'))+')');
this.$el.html(html);
console.log('rendered view successfully)');
return this;
},
initialize: function() {
console.log('contactView initalized');
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
events: {
'click .twitterDefault' : 'assignDefaultTwitterHandle',
},
assignDefaultTwitterHandle : function(event) {
var handle = $(event.currentTarget).data('twitter');
this.assignTwitterHandle(handle);
},
assignTwitterHandle : function(handle) {
console.log('model assignTwitterHandle. handle='+handle+')');
var view = this,
model = view.model;
model.save({
twitter : {
handle : handle,
ignore : false
},
id : model.get('id')
}, {
error : function() {
console.log('saving twitter handle failed');
},
success : function(model, response) {
console.log('response=('+JSON.stringify(response)+')');
if(response.error) {
console.log('error on server ='+response.error);
}
if(response.twitter) {
console.log('twitter is set');
var twitter = {
handle : handle,
tweet : response.twitter,
age : new Date()
};
console.log('setting twitter to '+JSON.stringify(twitter));
model.set('twitter', twitter);
model.set('tester', 'tester');
console.log('twitter after setting it = '+JSON.stringify(model.get('twitter')));
console.log('view\'s model twitter after setting it = '+JSON.stringify(view.model.get('twitter')));
}
if(response.klout) {
console.log('klout is set');
var klout = {
topics : response.klout
}
console.log('setting klout to '+JSON.stringify(klout));
model.set('klout', klout);
}
if(response.twitter || response.klout) {
console.log('Rerendering view after setting klout/twitter');
view.render();
}
}
});
}
});
contacts = new ContactModelCollection;
ContactCollectionView = Backbone.View.extend({
el : $('#suggestions-list'),
initialize : function(){
contacts.bind('add', this.addOne, this);
contacts.bind('reset', this.addAll, this);
contacts.bind('all', this.render, this);
},
render : function(){
console.log('contactcollectionview render');
},
addOne : function(contact) {
console.log('addOne');
var view = new ContactView({model: contact});
var el = view.render().el;
console.log('el=('+el+')');
$('#suggestions-list').append(el);
},
addAll : function() {
console.log('addAll');
contacts.each(this.addOne);
}
});
contactCollectionView = new ContactCollectionView;
App.contacts = contacts;
App.contactCollectionView = contactCollectionView; });
I guess the problem is the scope of the render function.
Depending from where is called, this takes a different value.
To warranty that always this is pointing to the View scope, add to your initialize:
_.bindAll(this,"render");
Also, it's not good habit to call view.render manually. You should let the events do their work. Model save already triggers some events. Just listen to them to update your View.

Backbone view not rendering correctly on subsequent calls to route handler

I'm having a problem with rendering a backbone.js view successfully from a route handler (browser application).
My javascript module is currently setup like this:
$(function () { // DOM ready
myModule.Init();
});
var myModule = (function () {
// Models
var DonorCorpModel = Backbone.Model.extend({ });
// Collections
var DonorCorpsCollection = Backbone.Collection.extend({ model : DonorCorpModel });
// Views
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = Handlebars.compile($('#pre-sort-actions-template').html())
this.collection.bind('reset', this.render);
},
render : function () {
$(this.el).html(this.template({}));
this.collection.each(function (donorCorp) {
var donorCorpBinView = new DonorCorpBinView({
model : donorCorp,
list : this.collection
});
this.$('.donor-corp-bins').append(donorCorpBinView.render().el);
});
return this;
}
});
var DonorCorpBinView = Backbone.View.extend({
tagName : 'li',
className : 'donor-corp-bin',
initialize : function () {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.template = Handlebars.compile($('#pre-sort-actions-donor-corp-bin-view-template').html());
},
render : function () {
var content = this.template(this.model.toJSON());
$(this.el).html(content);
return this;
}
})
// Routing
var App = Backbone.Router.extend({
routes : {
'' : 'home',
'pre-sort' : 'preSort'
},
initialize : function () {
// ...
},
home : function () {
// ...
},
preSort : function () {
if (donorCorps.length < 1) {
donorCorps.url = 'http://my/api/donor-corps';
donorCorps.fetch();
}
var donorCorpsList = new DonorCorpsListView({ collection : donorCorps }).render().el;
$('#document-action-panel').empty().append(donorCorpsList);
// ...
}
});
// Private members
var app;
var donorCorps = new DonorCorpsCollection();
// Public operations
return {
Init: function () { return init(); }
};
// Private operations
function init () {
app = new App();
Backbone.history.start({ root: '/myApp/', pushState: true });
docr.navigate('/', { trigger: true, replace: true});
}
}(myModule || {}));
Everything runs just fine when I run the app...it navigates to the home view as expected. I have links setup with handlers to navigate to the different routes appropriately, and when I run app.navigate('pre-sort', { trigger: true, replace: true}) it runs just fine, and renders the expected items in the list as expected.
The problem comes when I try to go back to the home view, and then back to the pre-sort view again...the items in the list don't get displayed on the screen as I am expecting them to. I'm just getting the empty pre-sort-actions-template rendered with no child views appended to it. I've stepped through the code, and can see that the collection is populated and the items are there...but for some reason, my code isn't rendering them to the view properly, and I can't seem to figure out why or what I'm doing wrong. Any ideas?? I'm pretty new to backbone, so I'm sure this code isn't written totally right...constructive feedback is welcome. Thanks.
how is the code rendering it to the view after going back home, then to pre-sort again? could you provide some details on that? duplicate items? empty view?
Also, I like to keep an index of my sub-views when I render them so the parent view can always access them regardless of scope. I found a really nice technique for this here: Backbone.js Tips : Lessons from the trenches
A quick overview of the pattern I'm referring to is as follows:
var ParentView = Backbone.View.extend({
initialize: function () {
this._viewPointers = {};
},
render: function (item) {
this._viewPointers[item.cid] = new SubView ({
model: item
});
var template = $(this._viewPointers[item.cid].render().el);
this.$el.append(template);
}
});
var SubView = Backbone.View.extend({
initialize: function () {
this.model.on("change", this.render, this);
},
render: function () {
this.$el.html( _.template($('#templateId').html(), this.model.attributes) );
return this;
}
});
I realize this answer is rather "broad," but it will be easier to answer with more specifics if I can understand the exact issue with the rendering. Hope its of some help regardless :)

Collate Backbone collection success and errors

I have two functions that loop through all models in a Backbone collection, and save those that have been changed, or destroy those that have been selected for deletion. What I need to do is collate the success and errors, so that I can notify "X number of changes/deletions were successful" and/or "There was an error changing/saving X number of domains".
I've no experience with saving/destroying Backbone collections, only models; and can't find anything on the internet that shows how to do this.
The save and remove are called by events in the parent view.
The relevant code:
App.Views.SiteDomains = Backbone.View.extend({
el: '.site-domains',
initialize: function() {
this.collection.on('all',this.render, this);
},
render: function() {
$('.site-domains').empty();
this.collection.each( function(model)
{
var view = new App.Views.SiteDomain({model: model});
this.$('.site-domains').append(view.render().el);
return this;
});
},
saveDomainChanges: function() {
this.collection.each( function(model)
{
var ref = model.get('ref');
if ($('#' + ref).val() != model.get('domain')) {
$('.save-domains').prop('disabled', true);
var fields = $(this.el).find('form').serializeArray(), data = {};
$.each(fields, function(i, pair)
{
data[pair.name] = pair.value;
});
model.save(data, {
wait:true,
success: function(model, response, event)
{
// Pass each success to notification function
},
error: function(model, response, event)
{
// Pass each error to notification function
}
});
}
});
$('.save-domains').prop('disabled', false);
},
removeDomain: function() {
this.collection.each( function(model)
{
var ref = model.get('ref');
if ($('#remove-' + ref).prop('checked'))
{
model.destroy({
wait:true,
success:function() {
// Pass each success to notification function
},
error: function() {
// Pass each error to notification function
}
});
}
});
}
});
Many, many, many thanks in advance to anyone that can help with this! :)
You could use an event aggregator and create Model/View or just POJO for the notifications according to your app design. Something like this:
// Event aggregator
App.vent = _.extend({}, Backbone.Events);
// POJO for the notifications
App.notifications = {
var onCreateSuccess = function (model, response) {
...
};
var onCreateError = function (model, response) {
...
};
App.vent.on('sitedomain:create:success', onCreateSuccess);
App.vent.on('sitedomain:create:error', onCreateError);
};
// Add event triggering to corresponding callbacks
model.save(data, {
wait:true,
success: function(model, response, event) {
App.vent.trigger('createdomain:create:success', model, response);
},
error: function(model, response, event) {
App.vent.trigger('createdomain:create:error', model, response);
}
});

Backbone Collection Add Event Firing Once

I have a Backbone collection and when I add a new model to it the "add" event doesn't seem to work as I'd expect. I've bound 2 views to listen for add events on the collection, but only one seems to get notified of the event, and when this happens, no PUT request is sent to my server. When I remove the second bind, the other one works and the PUT request is sent. Here's the code snippets:
var FlagList = Backbone.Collection.extend({
model: Flag // model not shown here... let me know if it would help to see
});
var FlagCollectionView = Backbone.View.extend({
el: $('ul.#flags'),
initialize: function() {
flags.bind('add', this.addFlag, this); // this one doesn't fire!!
},
addFlag: function(flag) {
alert("got it 1"); // I never see this popup
}
});
var AddFlagView = Backbone.View.extend({
el: $("#addFlagPopup"),
events: {
"click #addFlag": "addFlag"
},
initialize: function() {
flags.bind('add', this.closePopup, this); // this one fires!!
}
addFlag: function() {
flags.create(new Flag);
},
closePopup: function() {
alert("got it 2"); // I see this popup
}
});
var flags = new FlagList;
var addFlagView = new AddFlagView;
var flagCollectionView = new FlagCollectionView;
A few suggestions:
ID's vs Classes
you've over qualified your selector by combining a class and an id. jQuery allows this, but the ID selector should be unique on the page anyway so change el: $('ul.#flags') to el: $('ul#flags').
Leveraging Backbone
I like to explicitly pass my collections and/or models to my views and use the magic collection and model attributes on views.
var flags = new FlagList;
var addFlagView = new AddFlagView({collection: flags});
var flagCollectionView = new FlagCollectionView({collection: flags});
which now means that in your view, you will automagically have access to this.collection
unbinding events to avoid ghost views
var FlagCollectionView = Backbone.View.extend(
{
initialize: function (options)
{
this.collection.bind('add', this.addFlag, this);
},
addFlag: function (flag)
{
alert("got it 1");
},
destroyMethod: function()
{
// you need some logic to call this function, this is not a default Backbone implementation
this.collection.unbind('add', this.addFlag);
}
});
var AddFlagView = Backbone.View.extend(
{
initialize: function ()
{
this.collection.bind('add', this.closePopup, this);
},
closePopup: function ()
{
alert("got it 2");
},
destroyMethod: function()
{
// you need some logic to call this function, this is not a default Backbone implementation
this.collection.unbind('add', this.closePopup);
}
});
It looks like I have to agree with #fguillen, that your problem must be somewhere in how you initialize the view, as in my comment I mention that it's most likely related to timing, ie: binding your event to the collection after the 'add' event has already fired.
This code works for me:
var FlagList = Backbone.Collection.extend({});
var FlagCollectionView = Backbone.View.extend({
initialize: function() {
flags.bind('add', this.addFlag, this);
},
addFlag: function(flag) {
alert("got it 1");
}
});
var AddFlagView = Backbone.View.extend({
initialize: function() {
flags.bind('add', this.closePopup, this);
},
closePopup: function() {
alert("got it 2");
}
});
var flags = new FlagList;
var addFlagView = new AddFlagView;
var flagCollectionView = new FlagCollectionView;
flags.add({key:"value"});
check the jsFiddle
Your problem is somewhere else.
If you ended up here after making the same stupid mistake I did, make sure you've got:
this.collection.bind( 'add', this.render )
and NOT:
this.collection.bind( 'add', this.render() )

Resources