Event triggered multiple times after using back button in Backbone.js - javascript

I'm building a Backbone app and I came across this weird issue. In the state A (route: ""), I've got a view like that:
var view = Backbone.View.extend({
events : {
"click a.continue" : "next"
},
next : function(e) {
//Some stuff
Backbone.history.navigate("/page2");
}
});
and once I click on the anchor with "continue" class, I am redirected to a state B (route: "/page2"). If I click on the back button of my browser, and then I click on the anchor, debugging I've noticed that the next function is triggered twice. Actually if I keep going back and forth the number of times the event is triggered keeps increasing.
Any clue?

You've got a zombie view hanging around.
The gist of it is that when you are instantiating and displaying the second view ("state B"), you are not disposing of the first view. If you have any events bound to the view's HTML or the view's model, you need to clean those up when you close the form.
I wrote a detailed blog post about this, here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Be sure to read the comments as "Johnny O" provides an alternative implementation which I think is quite brilliant.

I Have the same problem, the solution is...
App.Router = Backbone.Router.extend({
routes: {
"fn1": "fn1",
"fn2": "fn2"
},
stopZombies: function(objView){
if(typeof objView === "object"){
objView.undelegateEvents();
$(objView.el).empty();
}
},
fn1: function(){
this.stopZombies(this.lastView);
var view1 = new App.v1();
this.lastView = view1;
},
fn2: function(){
this.stopZombies(this.lastView);
var view2 = new App.v2();
this.lastView = view2;
}
});
Store the last execute view in this.lastView, then stopZoombies() remove the events from this view.

Related

Completely remove Backbone view from memory

I have a very simple backbone dialog which simply shows a bootstap modal with a message. Im using it throughout the app and have built it so that you pass the title and message to be displayed, and the callback to be executed on click of the button. I have an errorListener and in there the view is created, attached to DOM and rendered:
var messageDialog;
var callback = function() {
....
messageDialog.remove();
messageDialog.unbind();
};
....
var errorListener = function() {
if (!messageDialog) {
messageDialog = new MessageDialog({
title: 'Error',
message: 'We have encountered an error. Please try again.',
buttonText: 'Try Again'
});
$('body').append(messageDialog.$el);
messageDialog.render();
}
messageDialog.setCallback(tryAgain);
messageDialog.show();
}
The problem is after the first time the messageDialog is created, attached to the DOM and shown, it wont be shown again. This is because if i do a console.log() on messageDialog, I stil see it's a varaible containing a Backbone view. I thought after calling remove() and unbind() in the callback, the messageDialog variable would be garbage collected. Do I need to do:
messageDialog = null;
after the unbind()? Is this the correct way of doing things?
I think that .remove() is enough, it removes it from the DOM and also removes binded events to avoid phantom views.
http://backbonejs.org/#View-remove
Is there any reason that myview.remove(); doesn't feet your needs ?
Hope it helps.

Events not firing after reinitialising backbone views

I'm trying to tidy code in my Backbone project. One of the problems I face is that I initialise all my views in the render's initialise function. I've now decided to only initialise a single view (and it's children) at a time.
The rendering of the pages works and I can swap backward and forward between views. Events that are bound in the view fire after a hard refresh of the page (F5 etc) but once I've moved to another view, the events no longer fire. I don't understand why as the previous view should be totally removed on second initialisation. We should then get a clean render, just as it would be on first load after a refresh. Can anyone explain why the events aren't firing?
Below are code examples to demonstrate the problem:
newView: function(view){
//Check if the holding variable is defined. If it is then
//call the close function
if (routerView == undefined){
console.log("routerview is undefined")
} else {
// This calls a function on the view which will remove any
//children, once it's done that it will remove its self.
routerView.close();
}
// When removing the view it removes the parent element in the index file.
// Here we add the container back in and set it to the new view's el
if ( $('#AppContainer').length == 0 ){
// Add new div container to the app and assign it to the 'el'
view.el = $('body').prepend('<div id="AppContainer"></div>');
}
// Return view to the route for rendering.
return routerView;
},
The close function inside one of the views would look something like this:
close: function(){
//Deal with any child views here as well.
this.remove();
},
Finally, in the route where we'd call the newView function would look
admin: function(){
// Call the newView function with a new instance of the AdminView and then assign it back
this.adminView = router.newView( new AdminView({ el : $("#AppContainer")} ));
//Render the view
this.adminView.render();
},
I have done some more work investigating the problem and I've discovered it. The problem was two fold but appeared on the same line of code.
view.el = $('body').prepend('<div id="AppContainer"></div>');
I discovered on the backbone docs that you should use the setElement function to alter a view's element. This then transfers all bound events which now means they work.
I then discovered that $('body').prepend('<div id="AppContainer"></div>') would return a reference to body and not the new #AppContainer but it actually returns a reference to the body which meant that the content of view was being placed in the body.

Understanding click events in Backbone and Express

I am trying to learn some javascript and I've gone through several tutorials, now I'm trying to understand a real-life system. Here is a demo site that has been pretty well put together:
http://nodecellar.coenraets.org/
https://github.com/ccoenraets/nodecellar
I think I understand the basics of how events can be assigned to elements on the page but then when I look through his source code I can't figure out how even the first click works. When you click "Start Browsing" it should be caught by javascript somehow which fires off an asynchronous request and triggers the view to change with the data received. But in his / public/js/views/ nowhere is there event catching plugged in (except in the itemdetail view but that's a different view entirely).
I also tried using chrome developer tools to catch the click and find out what script caught it.
Under sources I tried setting an event breakpoint for DOM mutation and then clicked.... but no breakpoint (how is that possible? There's definitely a DOM mutation happening)
I then went under elements and checked under the "click" event listener and didn't see anything revealing there either.
Obviously I don't know what I'm doing here. Can anyone help point me in the right direction?
This app is using backbones routing capabilities to switch contexts.
It is basically using hash tags and listening for location change events to trigger updates to the page.
The routing configuration is in main.js:
See: Backbone.Router for more information.
Code Reference: http://nodecellar.coenraets.org/#wines
var AppRouter = Backbone.Router.extend({
routes: {
"" : "home",
"wines" : "list",
"wines/page/:page" : "list",
"wines/add" : "addWine",
"wines/:id" : "wineDetails",
"about" : "about"
},
initialize: function () {
this.headerView = new HeaderView();
$('.header').html(this.headerView.el);
},
home: function (id) {
if (!this.homeView) {
this.homeView = new HomeView();
}
$('#content').html(this.homeView.el);
this.headerView.selectMenuItem('home-menu');
},
list: function(page) {
var p = page ? parseInt(page, 10) : 1;
var wineList = new WineCollection();
wineList.fetch({success: function(){
$("#content").html(new WineListView({model: wineList, page: p}).el);
}});
this.headerView.selectMenuItem('home-menu');
},
// etc...
});
utils.loadTemplate(['HomeView', 'HeaderView', 'WineView', 'WineListItemView', 'AboutView'], function() {
app = new AppRouter();
Backbone.history.start();
});

JavaScript MVC: how do views notify the controller

If I'm following a rough MVC pattern in JavaScript, what is the best way for the view (such as a button element) to notify the controller?
Should the button fire an event that the controller has to listen to? Or, should the button call a controller function directly? Or maybe the controller should assign the event to the view?
Thanks for any input!
I would say that the View should catch the event fired by the button and fire its own event that will be handled by the controller.
Let me explain:
#raynos wrote:
Controllers listen on input. This means controllers listen on events
from DOM nodes
personally even though I agree with the first statement I don't like the interpretation.
To follow this statement means that the controller has to know of every button/text field/element in the UI and its ID/Selector.
I prefer to have the View fire semantic events such as "languageSelected" or "searchRequested" and add the relevant data to the event.
so a typical flow would be that the View renders some UI (lets say it has a search box and a button), when the user clicks the button - the View handles the event and fires its own "searchRequested" event. This event is handled by the Controller that would call the Model asking it to perform the search. when done, the Model will fire a "searchResultsUpdated" evnet which will be handled by the View causing it to show the results.
if you now choose to change the design of your app to show search term links instead of a search box and a button (or even if you have then side by side - or on different screens) the only thing you need to change is the View.
A technical side-note:
If using JQuery and assuming your view is a javascript object you can use
$(view).triggerHandler(
$.Event('eventName',{'object:'with','more':'event','related':'data'})
);
to fire the event
And
$(view).on('eventName',handler);
to listen for and handle the event.
Interesting question. I think it would depend quite a lot on your situation, the complexity of your example, and the particular JavaScript patterns that you're using.
If the button you're talking about is simply an HTML element, this might be a simple way:
var MyController = function() {
this.particularMethod = function() {
// update model
}
// Using jquery
var button = $("#myButton");
button.click( function() { myController.particularMethod() } )
}
Or, if your button is an object or module that you've created, you could set a callback:
var Button = function(selector, clickFunction) {
// Using jquery
$(selector).click(clickFunction)
...
}
var MyController = function() {
this.particularMethod = function() {
// update model
}
var button = new Button("#myButton", this.particularMethod);
...
}
Unfortunately, trivial examples don't really illustrate the benefits of different approaches!
There are many ways to do it. Here is one way.
var app = new App();
function App() {
var model = new Model();
var view = new View();
var controller = new Controller(view, model);
}
function Controller(view, model) {
view.addActionListener(function() {
alert("on activate");
});
}
function View() {
var self = this;
$("button").onclick = function() {
self.listener();
}
}
View.prototype.addActionListener = function(listener) {
this.listener = listener;
}

Click binding in jQuery with backbone

I'm using Backbone.js, and in one of my main views I've encountered a very strange bug that I can't for the life of me figure out how to solve.
The view looks a look like the new Twitter layout. It receives an array of objects, each of which describes a collection and views elements that act on that collection. Each collection is represented by one tab in the view. The render() method on my view takes this array of collection objects, clears out the tabContainer DOM element if it isn't already empty, renders the tabs and then binds events to each of those tabs.
Now in my code I have the method to render the tabs and the method to bind the click handlers to those tabs sequentially. This works fine the first time I execute render(), but on subsequent calls of render(), the click handlers are not bound. Here's the relevant code snippet:
initialize: function() {
// Context on render(), _addAllTabs and _bindTabEvents is set correctly to 'this'
_.bindAll(this, 'render', 'openModel', 'closeModel', 'isOpen', 'addAllModels', '_switchTab',
'addOneModel', '_addTab', '_removeTab', '_addAllTabs', '_loadCollection',
'_renderControls', '_setCurrentCollection', '_loadModels', '_bindTabEvents');
this.template = JST['ui/viewer'];
$(this.el).html(this.template({}));
// The tabContainer is cached and always available
this.tabContainer = this.$("ul.tabs");
this.collectionContainer = this.$("#collection_container");
this.controlsContainer = this.$("#controls");
this.showMoreButton = this.$("#show_more_button");
},
render: function(collections, dashboard) {
// If _bindTabEvents has been called before, then this.tab exists. I
// intentionally destroy this.tabs and all previously bound click handlers.
if (this.tabs) this.tabContainer.html("");
if (collections) this.collections = collections;
if (dashboard) this.$("#dashboard").html(dashboard.render().el);
// _addAllTabs redraws each of the tabs in my view from scratch using _addTab
this._addAllTabs();
// All tabs *are* present in the DOM before my _bindTabEvents function is called
// However these events are only bound on the first render and not subsequent renders
this._bindTabEvents();
var first_tab = this.collections[0].id;
this.openTab(first_tab);
return this;
},
openTab: function (collectionId, e) {
// If I move _bindTabEvents to here, (per my more thorough explanation below)
// my bug is somehow magically fixed. This makes no friggin sense.
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
_addAllTabs: function() {
_.each(this.collections, this._addTab );
},
_bindTabEvents: function() {
this.tabs = _.reduce(_.pluck(this.collections, "id"), _.bind(function (tabEvents, collectionId) {
var tabId = this.$("#" + collectionId + "_tab");
tabEvents[collectionId] = tabId.click(_.bind(this._switchTab, this, collectionId));
return tabEvents
}, this), {});
},
_addTab: function(content) {
this.tabContainer.append(
$('<li/>')
.attr("id", content.id + "_tab")
.addClass("tab")
.append($('<span/>')
.addClass('label')
.text(content.name)));
//this._loadCollection(content.id);
this.bind("tab:" + content.id, this._loadCollection);
pg.account.bind("change:location", this._loadCollection); // TODO: Should this be here?
},
etc..
As I said, the render() method here does work, but only the first time around. The strange part is that if I move the line this._bindTabEvents(); and make it the first line of the openTab() method like in the following snippet, then the whole thing works perfectly:
openTab: function (collectionId, e) {
this._bindTabEvents();
if (this.isTabOpen(collectionId)) return false;
this._switchTab(collectionId, e);
},
Of course, that line of code has no business being in that method, but it does make the whole thing work fine, which leads me to ask why it works there, but doesn't work sequentially like so:
this._addAllTabs();
this._bindTabEvents();
This makes no sense to me since, it also doesn't work if I put it after this line:
var first_tab = this.collections[0].id;
even though that is essentially the same as what does work insofar as execution order is concerned.
Does anyone have any idea what I'm doing wrong and what I should be doing to make this correct (in terms of both behavior and coding style)?
In your view's render function, return this.delegateEvents(); I think you are losing your event bindings across your renderings and you need to re-establish them.
See this link for the backbone.js documentation for that function:
backbone.js - delegateEvents
When you switch tabs you are not simply showing/hiding content you are destroying and rebuild dom element so you are also destroying event liseners attached to them. that is why the events only work once and why adding _bindTabEvents into render works, because you are re-attaching the events each time.
when this line executes : this.tabContainer.html(""); poof... no more tabs and no more tab events.

Categories