Backbone.js view content keep multiplying instead of clearing - backbone.js

I'am new to Backbone.js and this problem has really got me stumped.
A view is built up from a collection, the collection results are filtered to place each set of results into their own array and then I make another array of the first items from each array, these are the 4 items displayed.
This works fine the first time the page is rendered but when I navigate away from this page and then go back the page now has 8 items, this pattern of adding 4 continues everytime I revisit the page.
// Locatore List Wrapper
var LocatorPageView = Backbone.View.extend({
postshop: [],
postbox: [],
postboxlobby: [],
postboxother: [],
closestPlaces: [],
el: '<ul id="locator-list">',
initialize:function () {
this.model.bind("reset", this.render, this);
},
render:function (eventName) {
//console.log(this)
// Loop over collecion, assigining each type into its own array
this.model.models.map(function(item){
var posttype = item.get('type').toLowerCase();
switch(posttype) {
case 'postshop':
this.postshop.push(item);
break;
case 'postbox':
this.postbox.push(item);
break;
case 'postbox lobby':
this.postboxlobby.push(item);
break;
default:
this.postother.push(item);
}
return ;
}, this);
// Create a closest Places array of objects from the first item of each type which will be the closest item
if (this.postshop && this.postshop.length > 0) {
this.closestPlaces.push(this.postshop[0]);
}
if (this.postbox && this.postbox.length > 0) {
this.closestPlaces.push(this.postbox[0]);
}
if (this.postboxlobby && this.postboxlobby.length > 0) {
this.closestPlaces.push(this.postboxlobby[0]);
}
if (this.postother && this.postother.length > 0) {
this.closestPlaces.push(this.postother[0]);
}
// Loop over the Closest Places array and append items to the <ul> contianer
_.each(this.closestPlaces, function (wine) {
$(this.el).append(new LocatorItemView({
model:wine
}).render().el);
}, this);
return this;
}
})
// Locator single item
var LocatorItemView = Backbone.View.extend({
tagName:"li",
template:_.template($('#singleLocatorTemplate').html()),
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .locator-map": "loadMap"
},
loadMap: function(e) {
e.preventDefault();
// Instantiate new map
var setMap = new MapPageView({
model: this.model,
collection: this.collection
});
var maptype = setMap.model.toJSON().type;
App.navigate('mappage', {trigger:true, replace: true});
setMap.render();
App.previousPage = 'locator';
}
});
window.App = Backbone.Router.extend({
$body: $('body'),
$wrapper: $('#wrapper'),
$header: $('#header'),
$page: $('#pages'),
routes: {
'' : '',
'locator': 'locator'
},
locator:function () {
this.$page.empty(); // Empty Page
this.places = new LocatorPageCollection(); // New Collection
this.placeListView = new LocatorPageView({model:this.places}); // Add data models to the collection
this.places.fetch();
this.$page.html(this.placeListView.render().el); // Append the renderd content to the page
header.set({title: 'Locator'}); // Set the page title
this.$body.attr('data-page', 'locator'); // Change the body class name
this.previousPage = ''; // Set previous page for back button
}
});

All the properties in your Backbone.View.extend argument are attached to the view's prototype. In particular, these properties:
postshop: [],
postbox: [],
postboxlobby: [],
postboxother: [],
closestPlaces: [],
end up attached to LocatorPageView.prototype so each LocatorPageView instance shares the same set of arrays and each time you use a LocatorPageView, you push more things onto the same set of shared arrays.
If you need any mutable properties (i.e. arrays or objects) in your Backbone views, you'll have to set them in your constructor:
initialize: function() {
this.postshop = [ ];
this.postbox = [ ];
this.postboxlobby = [ ];
this.postboxother = [ ];
this.closestPlaces = [ ];
}
Now each instance will have its own set of arrays.

This sounds like a classic Zombie View problem. Basically when you do this:
this.model.bind("reset", this.render, this);
in your view, you never unbind it. Thus, the view object is still bound to the model and can't be removed from memory. When you create a new view and reset, you have that listener still active which is why you see the duplicate view production. Each time you close and redo the view, you're accumulating listeners which is why it increases in multiples of 4.
What you want to do is unbind your listeners when you close out the view and rid your program of binds.
this.model.unbind("reset", this.render, this);
This should eliminate the pesky zombies. I'll add a link with more detailed information when I find it.
UPDATE - added useful references
I also ran into this problem a while back. It's quite the common gotcha with Backbone. #Derick Bailey has a really good solution that works great and explains it well. I've included the links below. Check out some of the answers he's provided in his history regarding this as well. They're all good reads.
Zombies! Run!
Backbone, JS, and Garbage Collection

Related

Marionette CompositeView Sort Rendering

I have a Marionette (2.4.1) CompositeView and when I do a sort it re-renders the entire view rather than the childView. The header icons revert back. I could fix them on render but is there a way that I can just render the childView?
diaryEntries = Backbone.Marionette.CompositeView.extend({
template : diaryEntries,
className: 'diary-entries',
collection: new Diary(),
childViewContainer: 'tbody',
reorderOnSort: true,
events: {
'click th[data-sort]': 'sort',
'click .pagination a': 'paginate'
},
initialize: function() {
this.itemsPerPage = 5;
this.currentPage = 1;
this.pages;
},
...
sort: function(e) {
var $th, dir, sort, sorted;
e.preventDefault();
$th = $(e.currentTarget);
sort = $th.data('sort');
if (sort === this.collection.sortField) {
this.collection.sortDirection *= -1;
} else {
this.collection.sortDirection = 1;
}
this.collection.sortField = sort;
$('span.glyphicon').removeClass('active-sort');
$th.siblings('th').find('span.glyphicon').removeClass('glyphicon-chevron-down glyphicon-chevron-up').addClass('glyphicon-sort');
if (this.collection.sortDirection === 1) {
$th.find('span.glyphicon').removeClass('glyphicon-chevron-down glyphicon-sort').addClass('glyphicon-chevron-up active-sort');
} else {
$th.find('span.glyphicon').removeClass('glyphicon-chevron-up glyphicon-sort').addClass('glyphicon-chevron-down active-sort');
}
this.collection.sort();
},
...
});
Well, looks like Marionette was concerned about the same thing you are. I couldn't find this in the docs, but it's pretty plain in the source. If you pass this option:
reorderOnSort: true
into your Collection/Composite view, on a 'sort' event the Collection/View will not re render, just its children.
See this line in the Marionette source: https://github.com/marionettejs/backbone.marionette/blob/v2.4.1/src/collection-view.js#L166
UPDATE If you're filtering your children views, running sort on your collection will invoke render on the Collection/CompositeView. The logic is that if you're paginating your children results, then you must sort the original, unfiltered, collection to properly display paginated results.
Nonetheless, I don't see anything intrinsically wrong with paginating a filtered set.
Fortunately, its easy to override the sort method to render whether your results are filtered or not. On you Collection/CompositeView include this method:
reorder: function() {
var children = this.children;
var models = this._filteredSortedModels();
// get the DOM nodes in the same order as the models
var els = _.map(models, function(model) {
return children.findByModel(model).el;
});
this.triggerMethod('before:reorder');
this._appendReorderedChildren(els);
this.triggerMethod('reorder');
}
},

Backbone.Model: set collection as property

I'm new with backbone and faced the following problems. I'm trying to emulate some sort of "has many relation". To achieve this I'm adding following code to initialize method in the model:
defaults: {
name: '',
tags: []
},
initialize: function() {
var tags = new TagsCollection(this.get('tags'));
tags.url = this.url() + "/tags";
return this.set('tags', tags, {
silent: true
});
}
This code works great if I fetch models through collection. As I understand, first collection gets the data and after that this collection populates models with this data. But when I try to load single model I get my property being overridden with plain Javascript array.
m = new ExampleModel({id: 15})
m.fetch() // property tags get overridden after load
and response:
{
name: 'test',
tags: [
{name: 'tag1'},
{name: 'tag2'}
]
}
Anyone know how to fix this?
One more question. Is there a way to check if model is loaded or not. Yes, I know that we can add callback to the fetch method, but what about something like this model.isLoaded or model.isPending?
Thanks!
"when I try to load single model I get my property being overridden with plain Javascript array"
You can override the Model#parse method to keep your collection getting overwritten:
parse: function(attrs) {
//reset the collection property with the new
//tags you received from the server
var collection = this.get('tags');
collection.reset(attrs.tags);
//replace the raw array with the collection
attrs.tags = collection;
return attrs;
}
"Is there a way to check if model is loaded or not?"
You could compare the model to its defaults. If the model is at its default state (save for its id), it's not loaded. If it doesn't, it's loaded:
isLoaded: function() {
var defaults = _.result(this, 'defaults');
var current = _.wíthout(this.toJSON(), 'id');
//you need to convert the tags to an array so its is comparable
//with the default array. This could also be done by overriding
//Model#toJSON
current.tags = current.tags.toJSON();
return _.isEqual(current, defaults);
}
Alternatively you can hook into the request, sync and error events to keep track of the model syncing state:
initialize: function() {
var self = this;
//pending when a request is started
this.on('request', function() {
self.isPending = true;
self.isLoaded = false;
});
//loaded when a request finishes
this.on('sync', function() {
self.isPending = false;
self.isLoaded = true;
});
//neither pending nor loaded when a request errors
this.on('error', function() {
self.isPending = false;
self.isLoaded = false;
});
}

Is it okay to call initialize() to initialize a view?

In my Backbone app, I have the following
playlistView = new PlaylistView({ model: Playlist });
Playlist.getNewSongs(function() {
playlistView.initialize();
}, genre, numSongs);
Playlist.getNewSongs() is called back when some ajax request is finished. I want to re-initialize the view then. However, I believe the way I'm doing it leads to this problem of a view listening to a same event twice. Is calling initialize() like this acceptable? If not, what should I do instead?
Update:
I wrote this chrome extension in Backbone to learn Backbone, and it's in a design hell at the moment. I am in the middle of refactoring the entire codebase. The snippet below is my PlaylistView initialize() code block.
var PlaylistView = Backbone.View.extend({
el: '#expanded-container',
initialize: function() {
var playlistModel = this.model;
var bg = chrome.extension.getBackgroundPage();
if (!bg.player) {
console.log("aborting playlistView initialize because player isn't ready");
return;
}
this.listenTo(playlistModel.get('songs'), 'add', function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').prepend(songView.render().el);
});
this.$('#song-search-form-group').empty();
// Empty the current playlist and populate with newly loaded songs
this.$('.playlist-songs').empty();
var songs = playlistModel.get('songs').models;
// Add a search form
var userLocale = chrome.i18n.getMessage("##ui_locale");
var inputEl = '<input class="form-control flat" id="song-search-form" type="search" placeholder="John Lennon Imagine">' +
'<span class="search-heart-icon fa fa-heart"></span>'+
'<span class="search-input-icon fui-search"></span>';
}
this.$('#song-search-form-group').append(inputEl);
var form = this.$('input');
$(form).keypress(function (e) {
if (e.charCode == 13) {
var query = form.val();
playlistModel.lookUpAndAddSingleSong(query);
}
});
// Fetch song models from bg.Songs's localStorage
// Pass in reset option to prevent fetch() from calling "add" event
// for every Song stored in localStorage
if (playlistModel.get('musicChart').source == "myself") {
playlistModel.get('songs').fetch({ reset: true });
songs = playlistModel.get('songs').models;
}
// Create and render a song view for each song model in the collection
_.each(songs, function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').append(songView.render().el);
}, this);
// Highlight the currently played song
var currentSong = playlistModel.get('currentSong');
if (currentSong)
var currentVideoId = currentSong.get('videoId');
else {
var firstSong = playlistModel.get('songs').at(0);
if (!firstSong) {
// FIXME: this should be done via triggering event and by Popup model
$('.music-info').text(chrome.i18n.getMessage("try_different_chart"));
$('.music-info').fadeOut(2000);
//console.log("something wrong with the chart");
return;
}
var currentVideoId = firstSong.get('videoId');
}
_.find($('.list-group-item'), function (item) {
if (item.id == currentVideoId)
return $(item).addClass('active');
});
},
It is not wrong but probably not a good practice. You did not post the code in your initialize but maybe you have too much logic here.
If you are simply initializing the view again so that the new data is rendered, you should use event listener as such:
myView = Backbone. View.extend ({
initialize : function() {
// We bind the render method to the change event of the model.
//When the data of the model of the view changes, the method will be called.
this.model.bind( "change" , this.render, this);
// Other init code that you only need once goes here ...
this.template = _.template (templateLoader. get( 'config'));
},
// In the render method we update the view to represent the current model
render : function(eventName) {
$ (this.el ).html(this .template ((this.model .toJSON())));
return this;
}
});
If the logic in your initiialize is something totally else, please include it. Maybe there is a beter place for it.

Nested backbone model results in infinite recursion when saving

This problem just seemed to appear while I updated to Backbone 1.1. I have a nested Backbone model:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""},
parse: function (response) {
response.name = response.set_id;
response.problems = new ProblemList(response.problems);
return response;
}
});
var ProblemList = Backbone.Collection.extend({
model: Problem
});
I initially load in a ProblemSetList, which is a collection of ProblemSet models in my page. Any changes to the open_date or due_date fields of any ProblemSet, first go to the server and update that property, then returns. This fires another change event on the ProblemSet.
It appears that all subsequent returns from the server fires another change event and the changed attribute is the "problems" attribute. This results in infinite recursive calls.
The problem appears to come from the part of set method of Backbone.Model (code listed here from line 339)
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
The comparison on the problems attribute returns false from _.isEqual() and therefore fires a change event.
My question is: is this the right way to do a nested Backbone model? I had something similar working in Backbone 1.1. Other thoughts about how to proceed to avoid this issue?
You reinstantiate your problems attribute each time your model.fetch completes, the objects are different and thus trigger a new cycle.
What I usually do to handle nested models:
use a model property outside of the attributes handled by Backbone,
instantiate it in the initialize function,
set or reset this object in the parent parse function and return a response omitting the set data
Something like this:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""
},
initialize: function (opts) {
var pbs = (opts && opts.problems) ? opts.problems : [];
this.problems = new ProblemList(pbs);
},
parse: function (response) {
response.name = response.set_id;
if (response.problems)
this.problems.set(response.problems);
return _.omit(response, 'problems');
}
});
parse gets called on fetch and save (according to backbone documentation), this might cause your infinite loop. I don't think that the parse function is the right place to create the new ProblemsList sub-collection, do it in the initialize function of your model instead.

How can I use backbone.js to regularly check a remote data source and update a table?

Specifically, I have a standard <table> and I want the rows to update as new data comes in or out. My data comes in as a JSON object and if a new item is there, I want to add a row to my table in the corresponding location (meaning order matters). If an item isn't there, I want to remove the <tr>.
Thanks for any help.
http://jsfiddle.net/CoryDanielson/phw4t/
Here you go, hopefully the code is simple enough to follow... a Backbone.js clock.
Basically the idea is that you put a collection inside of a view. Then you make the collection fetch data periodically using a setInterval and call .fetch({ 'reset': true }) so that the reset event will be triggered once the collection has a new set of data. Have the view listen to the collection's reset event and then render the table.
It's a lot easier to just re-render the data instead of trying to figure out what data has changed, what data was removed, and what data was added. You'll end up with the same result if you do it the hard way, or by just re-rendering the whole table. As long as you optimize you're DOM insertions (only write to the DOM once... render offscreen) there won't be a performance hit.
var PollingCollection = Backbone.Collection.extend({
url: "path/to/data/source",
model: new Backbone.Model(),
initialize: function (model, options) {
this.options = options;
setInterval(function () {
this.fetch({
"reset": true
});
}.bind(this), options.pollRate);
},
/**
* Overriding fetch for the example. If the URL was valid, delete the fetch
* function below.
*/
fetch: function () {
this.reset([
new Backbone.Model({
"name": "current",
"time": Date().toString()
}),
new Backbone.Model({
"name": "next fetch",
"time": "in " + this.options.pollRate / 1000 + " second(s)"
})]);
}
});
var TableView = Backbone.View.extend({
initialize: function (options) {
this.pollingCollection = new PollingCollection(null, options);
this.listenTo(this.pollingCollection, 'reset', this.render);
/**
* appending to body from within a view is 'bad'. normally another
* view would place this in the HTML. Just keepin' it simple for
* example
*/
$('body').append(this.$el);
},
render: function () {
// Render html into table then append. 1 DOM insert.
var table = $("<table></table>"),
html = this.renderTable();
table.append(html);
this.$el.html(table);
},
renderTable: function () {
var tableRows = "";
this.pollingCollection.each(function (model) {
tableRows += this.renderRow(model);
}, this);
return tableRows;
},
renderRow: function (model) {
return "<tr><td>" + model.get('name') + ":</td><td>" + model.get('time') + "</td></tr>";
}
});
var tableView = new TableView({ "pollRate": 1000 }); // fetch data every 1000 ms

Resources