Undelegate view events when browser back button pressed - backbone.js

Could anyone explain please how to remove events in order to prevent triggering duplication when clicking browser back button. Or is there any way to undelegate events when initalizing view again. Really stuck how to deal with it.
Pressing back button and then back again causes firing events for multiple times. When saving model form data for instance. Thank you.
var App = {};
// extending models, collections etc.
App.SamplesCollectionView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click a.sample-item': 'onEdit'
},
render: function(){
this.$el.append(this.template());
var $sample_list = this.$el.find('ul#sample-list');
this.collection.each(function(sample) {
var rendered = new App.CategoryView({model: sample}).render().el;
$sample_list.append(rendered);
});
},
onEdit: function(e) {
this.undelegateEvents();
// go to edit view
Backbone.history.navigate(e.target.getAttribute('href'), {trigger: true});
return false;
}
});
App.SampleEditView = Backbone.View.extend({
el: '#samples',
template: _.template($('#sample-edit-template').html()),
events: {
'click button.save': 'onSave',
'click button.cancel': 'onCancel',
},
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this;
},
onSave: function() {
this.undelegateEvents();
var data = Helpers.getFormData(this.$el.find('form'));
this.model.save(data);
// go back to index view
Backbone.history.navigate('/samples', {trigger: true});
return false;
}
});
App.SamplesRouter = Backbone.Router.extend({
routes: {
'samples': 'index',
'samples/edit/:id': 'edit'
},
index: function() {
App.samples = new App.SamplesCollection;
App.samplessView = new App.SamplesCollectionView({collection: App.samples});
},
edit: function(id) {
App.sampleEdit = new App.SampleEdit({id: id});
App.sampleEditView = new App.SampleEditView({model: App.sampleEdit})
}
});
App.samplesRouter = new App.SamplesRouter;
Backbone.history.start({pushState: true, hashChange: false});

The problem is that you have many views pointing to same element #samples. You can't remove one view because if you call view.remove() your other view's element is gone.
And as long as that that element exists in DOM, the view you thought to be gone will exist in memory since the shared element has event handlers referring the view instance.
If you want to delegate display functionality and edit functionality under same element, do it in same view using something like show/hide techniques without creating a new view instance.
Otherwise they should have it's own elements, you shouldn't have two view instances pointing to same element. While switching to a different view, make sure you call it's remove() method which removes the element from DOM and invokes undelegateEvents so that it get's garbage collected properly.

Related

Backbone.js - delete views

This is my second day trying to use backbone and im completely lost. I am following this tutorial - http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-3/
What I have done is loaded a contacts list and rendered it to the screen, but if you look at my render1 function - this takes a form input and appends it to my template. The problem is that I can't delete these items after they are created - the others can be deleted. help please?
var ContactView = Backbone.View.extend({
tagName: "contacts",
className: "contact-container",
template: $("#contactTemplate").html(),
initialize: function(){
this.model.on('change', this.render, this);
this.model.on('add', this.render1, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .deleteUser': 'delete'
},
test: function () {
alert("here");
},
delete: function () {
this.model.destroy();
},
render: function () {
console.log(this);
var tmpl = _.template(this.template);
$(this.el).html(tmpl(this.model.toJSON()));
temp = tmpl(this.model.toJSON());
console.log(temp);
return this;
},
render1: function () {
console.log(this);
var tmpl = _.template(this.template);
temp = tmpl(this.model.toJSON());
temp='<contacts class="contact-container">'+temp+'</contacts>';
console.log(temp);
$("#contacts").append(temp);
$(this.el).html(tmpl(this.model.toJSON()));
return this;
}
});
var AddPerson = Backbone.View.extend({
el: $("#addPerson"),
// el: $("form/"),
events: {
'click': 'submit',
'submit': 'submit'
},
submit: function(e) {
// alert("here");
e.preventDefault();
this.collection = new Directory();
// var data = (contacts[0]);
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
}
});
seasick, there are quite a few issues in this code.
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
Contact is a Backbone.Model but ContactView is a Backbone.View. Yet, you are adding both to the this.collection (which I assume is a Backbone.Collection of Contact?). See the problem here? In Backbone, there is no such concept of a 'collection of views'. You just get one concept: views, that are tied to a model.
So, here, you create a Contact and you add it to the Collection. That is all! It takes care of the Model part. The rendering part needs to be handled with events and renders.
When you add a model to a collection (this.collection.add(contact)), the collection will trigger a 'add' event, that you can hook to with a .on to create a new ContactView and append it to the DOM somewhere.
So when you write this...
this.model.on('add', this.render1, this);
You are actually saying 'When the Contact model triggers an add event, run render1', which isn't what you want, what you probably want is a collection.on('add', ...). The model will never trigger an add event (well, you could make it trigger one, but it wouldn't be an expected behavior!), the add/remove events are at the collection level.
In other words, you are missing some binding on the collection in your AddPerson view to 'react' to adding a new Contact to the collection. The code of the function bound to the add event should probably look a bit like:
onAdd: function(newContact){
var newContactView = new ContactView({model: newContact});
$("#contacts").append(newContactView.render().el);
}
There are other issues in your code, but I guess an outline of the steps to take would be like:
Remove the binding to add in ContactView: ContactView is only concerned with one contact, not how to manage multiple contacts. This is probably why you are having issues with only some (the first one?) of the contacts 'working'
Move that logic to the AddContact view which seems to be more concerned with the Collection of contacts. Use the collection 'add' event to create new ContactView and append them to the DOM
Hope this helps!

Backbone 1.0 js events still attached after .html([content])

The problem I am having is click events keep piling up (still attached after changing the view). I have fixed the problem by only having one instance of the view (shown below). I thought backbone got rid of events when the markup is changed. I haven't had this problem with other views.
BROKEN CODE: Click events keep piling up on loadPlayerCard as more views are created.
//Player Thumb View
PgaPlayersApp.PlayerThumbView = Backbone.View.extend({
events: {
'click': 'loadPlayerCard'
},
tagName: 'li',
template: _.template( $('#player_thumb').html()),
render: function()
{
this.$el.html(this.template(this.model.toJSON()));
return this;
},
loadPlayerCard: function()
{
new PlayerCardView({model: this.model}).render();
return false;
}
});
//Router
var Router = Backbone.Router.extend({
routes:{
'': 'loadPlayers'
},
loadPlayers: function()
{
PgaPlayersApp.Players.fetch({reset: true, success: function()
{
//When players is first fetched, we want to render the first player into the card area
new PlayerCardView({model: PgaPlayersApp.Players.first()}).render();
}});
}
});
PgaPlayersApp.Router = new Router();
Backbone.history.start();
FIXED CODE: Code that fixes the problem:
PgaPlayersApp.CurrentPlayerCard = new PlayerCardView();
//Player Thumb View
PgaPlayersApp.PlayerThumbView = Backbone.View.extend({
events: {
'click': 'loadPlayerCard'
},
tagName: 'li',
template: _.template( $('#player_thumb').html()),
render: function()
{
this.$el.html(this.template(this.model.toJSON()));
return this;
},
loadPlayerCard: function()
{
PgaPlayersApp.CurrentPlayerCard.model = this.model;
PgaPlayersApp.CurrentPlayerCard.render();
return false;
}
});
//Router
var Router = Backbone.Router.extend({
routes:{
'': 'loadPlayers'
},
loadPlayers: function()
{
PgaPlayersApp.Players.fetch({reset: true, success: function()
{
//When players is first fetched, we want to render the first player into the card area
PgaPlayersApp.CurrentPlayerCard.model = PgaPlayersApp.Players.first();
PgaPlayersApp.CurrentPlayerCard.render();
}});
}
});
PgaPlayersApp.Router = new Router();
Backbone.history.start();
PlayerCardView (For reference):
var PlayerCardView = PgaPlayersApp.PlayerCardView = Backbone.View.extend({
events: {
'click': 'flipCard'
},
el: '#pga_player_card',
template: _.template( $('#player_card').html()),
render: function()
{
this.$el.html(this.template(this.model.toJSON()));
return this;
},
flipCard: function()
{
this.$("#player_card_container").toggleClass('flip');
}
});
In your router you keep creating new PlayerCardViews:
new PlayerCardView({model: PgaPlayersApp.Players.first()}).render();
All of those views share exactly the same el:
el: '#pga_player_card'
So you keep creating new PlayerCardViews and each one binds to #pga_player_card.
Every time you do that, you bind a brand new view to exactly the same DOM element and each of those views will call delegateEvents to bind the event handlers. Note that delegateEvents binds to el and that jQuery's html method:
removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
So html does nothing to el but it will remove event handlers from child elements. Consider this simple example with <div id="d"></div>:
$('#d').on('click', function() {
console.log('Before .html');
});
$('#d').html('<p>Where is pancakes house?</p>');
$('#d').on('click', function() {
console.log('After .html');
});
If you then click on #d, you'll see both the before and after messages in the console.
Demo: http://jsfiddle.net/ambiguous/ftJtS/
That simple example is, more or less, equivalent to what you're doing.
You'll have a better time if you:
Put the view inside #pga_player_card and let the router do $('#pga_player_card').append(view.render().el).
Keep track of the view that's already there and view.remove() it before adding the new one.
Avoid trying to reuse DOM elements for multiple view instances and avoid trying to reuse views, neither is worth the hassle.

Backbone Layout Manager subview's events doesn't work after reload

I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.

Right way for wiring backbone views

I have a two views:
1 LeftView (maximized when RightView is minimized & vice versa)
2 RightView (containing)
- collection of
- RightItemView (rendering RightItemModel)
When RightView is maximized and the user clicks a RightItemView, I want to maximize LeftView and display something according to the data from the clicked RightItemView.
What's the proper way to wire them?
I would recommend using the Backbone.Events module:
http://backbonejs.org/#Events
Basically, this line is all it takes to create your event dispatcher:
var dispatcher = _.clone(Backbone.Events);
Then all of your views can trigger/listen for events using the global dispatcher.
So, in RightItemView you would do something like this in the click event:
dispatcher.trigger('rightItemClick', data); // data is whatever you need the LeftView to know
Then, in LeftView's initialize function, you can listen for the event and call your relevant function:
dispatcher.on('rightItemClick', this.maximizeAndDisplayData);
Assuming your LeftView would have a function like so:
maximizeAndDisplayData: function(data) {
// do whatever you need to here
// data is what you passed with the event
}
The solution #jordanj77 mentioned is definitely one of the correct ways to achieve your requirement. Just out of curiosity, I thought of another way to achieve the same effect. Instead of using a separate EventDispatcher to communicate between the two views, why shouldn't we use the underlying model as our EventDispatcher? Let's try to think in those lines.
To start with, add a new boolean attribute to the RightItem model called current and default it to false. Whenever, the user selects the RightItemView, set the model's current attribute to true. This will trigger a change:current event on the model.
var RightItem = Backbone.Model.extend({
defaults: {
current: false,
}
});
var RightItemView = Backbone.View.extend({
events: {
'click li': 'changeCurrent'
}
changeCurrent: function() {
this.model.set('current', true);
}
});
On the other side, the LeftView will be handed a Backbone.Collection of RightItem models during creation time. You would anyways have this instance to supply the RightView isn't it? In its initialize method, the LeftView will listen for change:current event. When the event occurs, LeftView will change the current attribute of the model it is currently displaying to false and start displaying the new model that triggered this event.
var LeftView = Backbone.View.extend({
initialize: function() {
this.collection.on('change:current', this.render, this);
},
render: function(model) {
// Avoid events triggered when resetting model to false
if(model.get('current') === true) {
// Reset the currently displayed model
if (this.model) {
this.model.set('current') = false;
}
// Set the currently selected model to the view
this.model = model;
// Display the view for the current model
}
}
});
var leftView = new LeftView({
// Use the collection that you may have given the RightView anyways
collection: rightItemCollection
});
This way, we get to use the underlying model as the means of communication between the Left and Right Views instead of using an EventDispatcher to broker for us.
The solution given by #Ganeshji inspired me to make a live example
I've created 2 views for this.
var RightView = Backbone.View.extend({
el: $('.right_view'),
template: _.template('<p>Right View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
this.$link = this.$el.append('Item to view').children('#left_view_max');
},
events: {
'click #left_view_max' : 'maxLeftView'
},
maxLeftView: function () {
//triggering the event for the leftView
lView.trigger('displayDataInLeftView', this.$link.attr('title'));
},
initialize: function (options) {
this.renderTemplate();
}
});
var LeftView = Backbone.View.extend({
el: $('.left_view'),
template: _.template('<p>Left View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
},
displayDataInLeftView: function (data) {
this.$el.append('<p>' + data + '</p>');
},
initialize: function (options) {
//set the trigger callback
this.on('displayDataInLeftView', this.displayDataInLeftView, this);
this.renderTemplate();
}
});
var lView = new LeftView();
var rView = new RightView();
Hope this helps.

Backbone.js: How do I force the View to automatically refresh when changing a models collection

So I have a View that looks like this.
//base class
var SelectListView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'addOne', 'addAll');
this.collection.bind('reset', this.addAll);
},
addAll: function() {
this.collection.each(this.addOne);
},
events: {
"change": "changedSelected"
},
changedSelected: function() {
this.selected = $(this.el);
this.setSelectedId($(this.el).val());
}
});
//my extended view
var PricingSelectListView = SelectListView.extend({
addOne: function(item) {
$(this.el).append(new PricingView({ model: item }).render().el);
}
});
I have instantiated the view like this...
var products = new ProductPricings();
var pricingsView = new PricingSelectListView({
el: $("#sel-product"),
collection: products
});
Somewhere else (another views custom method)I have updated the pricing view's collection
pricingsView.collection = new ProductPricings(filtered);
This does not seen to do anything.
pricingsView.render();
So now the collection has fewer items but the new view is never rendered or refreshed in the DOM.
How to I do I 1.) refresh the rendering in the DOM? 2.) Make it automatically refresh the DOM? Do I have to somehow tell it to render when ever the collection changes?
You bound addOne() to a reset event. When you just replace the pricingsView.collection instance then that event is not triggered and addOne() is not executed.
Try instead:
pricingsView.collection.reset(filtered);
This might work since you bind to collection's reset event already:
pricingsView.collection.reset(filtered);
http://backbonejs.org/#Collection-reset
You still have tweak your rendering logic to remove old markup from the view when reset happens.

Resources