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.
Related
As an exercise I have to do a little online bike reservation app. This app begins with a header which explains how to use the service. I wanted this tutorial be optional so I wrote a welcome message in HTML and if the user doesn't have a var in his cookies saying he doesn't want to see the tutorial again, the welcome message is replaced by a slider that displays the information.
To achieve that is fetch a JSON file with all the elements I need to build the slider (three divs : the left one with an arrow image inside, the central one where the explanations occur and the right one with another arrow). Furthermore I want to put "click" events on the arrows to display next or previous slide. However, when I do so, only the right arrow event works. I thought of a closure problem since it is the last element to be added to the DOM that keeps its event but tried many things without success. I also tried to add another event to the div that works ("keypress") but only the click seems to work. Can you look at my code give me an hint on what is going on?
Here is the init function of my controller:
init: function() {
var load = this.getCookie();
if(load[0] === ""){
viewHeader.clearCode();
var diapoData = ServiceModule.loadDiapoData("http://localhost/javascript-web-srv/data/diaporama.json");
diapoData.then(
(data) => {
// JSON conversion
modelDiapo.init(data);
// translation into html
controller.initElementHeader(modelDiapo.diapoElt[0]);
controller.hideTuto();
}, (error) => {
console.log('Promise rejected.');
console.log(error);
});
} else {
viewHeader.hideButton();
controller.relaunchTuto();
}
}
There is a closer look at my function translating the JSON elements into HTML and adding events if needed :
initElementHeader: function(data){
data.forEach(element => {
// Creation of the new html element
let newElement = new modelHeader(element);
// render the DOM
viewHeader.init(newElement);
});
}
NewElement is a class creating all I need to insert the HTML, viewHeader.init() updates the DOM with those elements and add events to them if needed.
init: function(objetElt){
// call the render
this.render(objetElt.parentElt, objetElt.element);
// add events
this.addEvent(objetElt);
},
Finally the addEvent function:
addEvent: function(objet){
if(objet.id === "image_fleche_gauche"){
let domEventElt = document.getElementById(objet.id);
domEventElt.addEventListener("click", function(){
// do things
});
}
else if(objet.id === "image_fleche_droite"){
let domEventElt = document.getElementById(objet.id);
domEventElt.addEventListener("click", function(){
// do stuff
});
};
},
I hope being clear enough about my problem. Thank You.
Ok, I found the problem, even if the element was actually created, it somehow stayed in the virtual DOM for some time, when the "getElementById" in "addEvent" was looking for it in the real DOM it didn't find the target and couldn't add the event. This problem didn't occur for the last element since there was nothing else buffering in the virtual DOM.
On conclusion I took out the function adding events out of the forEach loop and created another one after the DOM is updated to add my events.
I am working on a Backbone demo app that shows a list of tweets. As I am replacing all the "tweets" with different data I clear the list using $.html()
render: function() {
$("#item-table").html('');
this.collection.each(this.addItem);
}
I was wondering if anyone could give me a hint with what can I replace this $.html() for better performance, because by using $.html() I am causing reflows and which gives bad layout process times.
There are two other places in the code where I use $.html() and it would be really great if someone could give me advice on how to change those too if those other places are even possible.
Create a new DocumentFragment to pre-render all the items, then update the DOM once.
Also, favor this.$(...) over the global jQuery selector $(...).
this.$ is a proxy to this.$el.find(...) which is more efficient, and less prone to select something outside of the view.
Using jQuery's core function ($()) inside a view can fail if the view wasn't rendered yet. So it's better to always manipulate through this.$el so you can make changes even before the view is actually put in the DOM.
Keep all the sub views created in an array to cleanly remove them later.
initialize: function() {
this.childViews = [];
},
render: function() {
// cache the list jQuery object
this.$list = this.$("#item-table");
// Make sure to destroy every child view explicitely
// to avoid memory leaks
this.cleanup();
this.renderCollection();
return this;
},
The real optimization starts here, with a temporary container.
renderCollection: function() {
var container = document.createDocumentFragment();
this.collection.each(function(model) {
// this appends to a in memory document
container.appendChild(this.renderItem(model, false).el);
}, this);
// Update the DOM only once every child view was rendered.
this.$list.html(container);
return this;
},
Our renderItem function can still be used to render a single item view and immediatly put it in the DOM. But it also provides an option to postpone the DOM manipulation and it just returns the view.
renderItem: function(model, render) {
var view = new Item({ model: model });
this.childViews.push(view);
view.render();
if (render !== false) this.$list.append(view.el);
return view;
},
To avoid memory leaks with dangling listeners, it's important to call remove on each view before forgetting about it.
I use an additional optimization by deferring the actual call to remove so we don't waste time now while the user waits.
cleanup: function() {
var _childViewsDump = [].concat(this.childViews);
this.childViews = [];
while (_childViewsDump.length > 0) {
var currentView = _childViewsDump.shift();
// defer the removal as it's less important for now than rendering.
_.defer(currentView.remove.bind(currentView), options);
}
}
I have broken down this issue into the smallest example possible (i.e., it's only to demonstrate the problem, not to necessarily represent a real-world scenario).
Let's say I have a parent view ("MainView" here) that contains a child view ("SubView" here). If, at any point, I need to re-render the parent view (which thereby re-renders the child view), I am losing the event bindings in the child view, despite calling delegateEvents.
A jsfiddle can be found here: http://jsfiddle.net/ya87u/1/
Here is the code in full:
var MainView = Backbone.View.extend({
tagName : "div",
initialize : function() {
this.subView = new SubView();
},
render : function() {
this.$el.html(this.subView.render().$el); // .html() breaks binds
this.subView.delegateEvents(); // this re-establishes them
return this;
}
});
var SubView = Backbone.View.extend({
tagName : "div",
events : {
"click .button1" : "onButtonPress"
},
onButtonPress : function() {
alert("Button pressed");
},
render : function() {
var html = "<button class='button1'>Button1</button>";
this.$el.html(html);
return this;
}
});
var v = new MainView();
$("#area1").html(v.render().$el); // binds work fine
// do some work... then re-render...
$("#area1").html(v.render().$el); // breaks event binds
I do not understand why this is happening. Any input would be greatly appreciated.
To preserve the bindings you could do three things.
Use $.append instead of $.html
Using $.append will keep your bindings intact and also take care that you don't lose any sibling content.
Just re-render without re-attaching
In your code you re-render your view and also re-attach it's content. This is not necessary. Simply calling v.render() will achieve the same visual result but keep your bindings intact.
var v = new MainView();
$("#area1").html(v.render().$el); // binds work fine
// do some work... then re-render…
v.render(); // keeps event binds
$("#area1").append(v.render().$el); // works too
Use $.empty() before calling $.html()
According to the jQuery documentation for $.html(htmlString)
If you are keeping references to these DOM elements and need them to be unchanged, use .empty().html( string ) instead of .html(string) so that the elements are removed from the document before the new string is assigned to the element.
The important part is that $.html expects an html string, not elements and also unbinds any event handlers present on child nodes. The only way I can explain this behavior is that somehow through transformation of the dom tree to a string and vice versa the events get removed. Interestingly enough, using $.empty() as mentioned in the documentation solves the problem.
// any $.html reference needs to have an $.empty too
this.$el.empty().html(this.subView.render().$el);
// and then later for your mainView
$("#area1").empty().html(v.render().$el);
I am having issues at solving backbone memory leaks, so far I attempted to clean the views using jquery CleanData when the views are removing from the DOM.
Each time a view is removed (even via jQuery .html()) it hit the dispose() method that should theoretically kill all references that stop the view from being collected. Unfortunately the app just build up memory
code below,
Backbone.View.prototype.dispose = function(){
// unbind all events, so we don't have references to child views
this.unbind();
this.off();
// if we have a collection - unbind all events bound to this context
this.collection && this.collection.off(null, null, this);
// do the same with a model
this.model && this.model.off(null, null, this);
delete this;
};
clean data :
$.cleanData = function( elems ) {
$.each( elems, function( i, elem ) {
if ( elem ) {
$(elem).trigger("dispose");
}
});
oldClean(elems);
};
The code work, dispose is hit (I added the event in the view) but it's never collected anyway when I change page.
(About the dispose event..)
I do not explicitly call remove on all views. the app container is emptied, jQuery does cleanData. I added an event dispose & I trigger that func in cleandata
I think the problem is delete this doesn't do what you thought it would. It depends on how you initiate your view. Do you assign your view to any variable or initiate it in a scope that live beyond the point where you change your page?
Also, there is a function remove() on Backbone View
More on JavaScript delete http://perfectionkills.com/understanding-delete/ and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete
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.