In the router I do this
function test() {
self.topbarView = new TopbarView();
self.topbarView.render();
GhostviewHunter.addView(self.topbarView);
}
function clean() {
console.log(GhostviewHunter.currentViews.length);
GhostviewHunter.clean();
}
setInterval(test, 1000);
setInterval(clean, 1000);
ghostviewhunter should clean/remove the views:
define('ghostviewHunter', [], function() {
var GhostviewHunter = function() {};
GhostviewHunter.prototype.currentViews = [];
GhostviewHunter.prototype.addView = function(view) {
this.currentViews.push(view);
}
GhostviewHunter.prototype.clean = function() {
_.each(this.currentViews, function(view) {
view.remove();
});
this.currentViews.length = 0;
}
GhostviewHunter.__instance = null;
GhostviewHunter.getInstance = function() {
if( GhostviewHunter.__instance == null ) {
GhostviewHunter.__instance = new GhostviewHunter();
}
return GhostviewHunter.__instance;
}
return GhostviewHunter.getInstance();
})
TopView is fetching a model, the model is updated every 1seconde with setInterval function.
I thought that remove(); would be enough be the memory leak is very quick when I monitor the app.
Any idea ?
EDIT:
TOPBARVIEW
define('topbarView', [
'backbone',
'parameterManager',
'text!views/topbarView/topbarTemplate.html',
'drupalidModel',
'weatherModel',
'refreshTime',
'dateParser'
], function(Backbone, ParameterManager, TopbarTemplate, DrupalidModel, WeatherModel, RefreshTime, DateParser) {
var TopbarView = Backbone.View.extend({
el: '#topbar',
template: _.template(TopbarTemplate),
events: {},
initialize: function() {
var self = this;
_.bindAll(this, 'render', 'startDateRefresh');
this.dateParser = new DateParser();
self.startDateRefresh();
setInterval(self.startDateRefresh, RefreshTime.date);
this.initWeatherModel();
},
render: function() {
var self = this;
var data = {
picto_url : ParameterManager.get('WEATHER_RESOURCE_URL') + ParameterManager.get('WEATHER_PICTO_CODE') + ".png",
date: self.date
}
this.$el.html(this.template({data: data}));
},
initWeatherModel: function() {
var self = this;
var weather_url = ParameterManager.get('WEATHER_URL');
if(weather_url === null) {
this.drupalidModel = new DrupalidModel();
this.drupalidModel.fetch({
success: function(model, response) {
var center_id_num = model.get('center_id_num');
ParameterManager.set('DRUPAL_CENTER_ID_NUM', center_id_num);
ParameterManager.constructWeatherUrl();
self.model = new WeatherModel();
self.listenTo(self.model,'change', self.render);
self.startModelRefresh();
},
error: function() {
console.log("Failed to fetch center id!");
}
})
} else {
this.model = new WeatherModel();
self.listenTo(self.model,'change', self.render);
this.startModelRefresh();
};
},
startModelRefresh: function() {
var self = this;
this.modelRefresh = function() {
self.model.fetch();
}.bind(this);
self.modelRefresh();
setInterval(self.modelRefresh, RefreshTime.weather);
},
stopModelRefresh: function() {
var self = this;
clearInterval( self.modelRefresh );
},
startDateRefresh: function() {
var self = this;
this.date = this.dateParser.classicDate();
this.render();
}
});
return TopbarView;
})
As fbynite suggested, your code which is supposed to clear the interval(s) is not correct, you should pass the interval id to clearInterval.
apart from that, you're not calling stopModelRefresh() at all. You should make sure all external references are properly removed before removing the view. For example I've added a destroy method that clears the interval before removing the view:
var TopbarView = Backbone.View.extend({
el: '#topbar',
template: _.template(TopbarTemplate),
events: {},
initialize: function() {
},
render: function() {
},
modelRefresh: function() {
this.model.fetch();
},
startModelRefresh: function() {
this.modelRefresh();
this.intervalId = setInterval(_.bind(this.modelRefresh,this), RefreshTime.weather);
},
stopModelRefresh: function() {
clearInterval(this.intervalId);
},
destroy: function() {
this.stopModelRefresh();
this.remove();
}
});
Now your GhostviewHunter should call it instead of directly calling remove:
GhostviewHunter.prototype.clean = function() {
_.each(this.currentViews, function(view) {
view.destroy();
});
this.currentViews.length = 0;
}
or you can even override the remove method itself to something like:
remove: function(){
this.stopThisInterval();
this.stopThatInterval();
this.cleanUpSomethingElse();
Backbone.View.prototype.remove.call(this);
}
and have the ghost thingy call remove itself.
Note that you have other interval calling startDateRefresh which you're not even attempting to clear... You should clear all such similarly.
And as a side note, I strongly suggest to stop spamming self = this where it is totally unnecessary for eg:
stopModelRefresh: function() {
var self = this;
clearInterval( self.modelRefresh );
// Why..? Nothing here changes the context?
},
and I also suggest recursively calling modelRefresh once the current fetch succeeds/fails rather than calling it from an interval where you have no guarantee that the previous fetch is complete
Related
This is one of question from a Backbone newbie.
So, I was trying to use this.model.bind and this.model.on('change', this.render), but it could not work for my model. I checked my console & my model function getStoreByName. This is returning an array, but render function is rendering before the fetch array, that's why I want to bind model to view when model change.
Here is how far I have gotten so far.
This is Backbone view:
var storeTemplate = Backbone.View.extend({
initialize: function () {
console.log('view inti is running');
this.template = template;
this.model = new storeModel();
this.model.getStoreByName();
this.stores = this.model.get('stores');
this.model.on('change', this.render);
console.log(this.stores);
console.log('ready to call this.render()');
this.render();
console.log('end to call this.render()');
console.log('view init end');
},
render: function () {
console.log('render is start');
this.logger.log(Logger.LOG_LEVELS.TRACE, "Entering render.");
console.log(this.model.stores);
this.$el.html(_.template(this.template, { stores: this.model.get('stores') }));
return this;
}
});
return storesTemplate;
});
and this is my Backbone Model
var store= Backbone.Model.extend({
initialize: function () {
console.log('init models is running');
this.stores = [];
this.set({ 'stores': this.stores});
console.log(this.get('stores'));
this.service = Service;
},
getStoreByName: function () {
console.log('getting store');
stores = [];
this.service.getStoreByName(function (xml) {
$(xml).find("element").each(
function () {
var store = {
"storeID": $(this).find("ID").text(),
"storeType": $(this).find("Type").text(),
"storeName": $(this).find("Name").text(),
};
if (xml !== null) {
stores.push(store);
}
else {
this.model.set({ stores: [] });
}
}
);
that.set('stores', store)
},
);
},
})
return store;
});
Try this.listenTo(this.model,'change:stores', this.render);
If that doesn't work use promises. Update model like this:
getStoreByName: function() {
var deferred = $.Deferred();
var stores = [];
return this.service.getStoreByName(function(xml) {
if (xml === null) {
return Deferred.resolve([]);
}
$(xml).find("element").each(function() {
var store = {
"storeID": $(this).find("ID").text(),
"storeType": $(this).find("Type").text(),
"storeName": $(this).find("Name").text(),
};
stores.push(store);
});
return Deferred.resolve(stores);
});
this.model.set('stores', stores);
return deferred.promise();
}
and in the view you can do this.model.getStoreByName().then(this.render);
I'm trying to get it so that when my backbone collection is saving if it hits an error I can do something with it. However when the form is saved the Render All Tasks button's click event is always triggered. How can I get it to stop iterating the collection if an error is found? Or otherwise how can I get it to call the fail function?
var EditTaskView = AddTaskView.extend({
template: _.template($("#individualTaskEditView").html()),
events: {
"submit": function (e) {
e.preventDefault();
if (this.model.isValid(true) && this.ScheduleView.isValid() && this.ProviderView.isValid()) {
$.when(this.model.save(), this.FiltersView.saveAll())
.done(function () {
$("#RenderAllTasks").trigger("click");
})
.fail(function (xhr, status, errorMessage) {
var message = JSON.parse(xhr.responseText).Message;
RenderError(message, "#EditTaskDetailsTabError");
});
}
}
}
};
var FieldCollectionAddView = Backbone.View.extend({
tagName: "div",
render: function () {
// iterate the collection
this.collection.each(function (field) {
// render the view and append to the collection
var view = new FieldAddView({ model: field });
var rendered = view.render();
this.$el.append(rendered.el);
}, this);
return this;
},
isValid: function () {
var valid = true;
_.each(this.collection.models, function (model) {
if (!model.isValid(true)) {
valid = false;
}
});
return valid;
},
saveAll: function () {
var errorsFound = false;
_.each(this.collection.models, function (model) {
model.save(null, {
error: function (error) {
//TODO: do soemthing with the error
var message = JSON.parse(el.responseText).Message;
RenderError(message, "#ProviderDetailsTabError");
}
}, { wait: true });
});
return errorsFound;
}
});
var ProviderAddView = Backbone.View.extend({
tagName: "div",
template: _.template($("#providerAddTemplate").html()),
render: function () {
// append the template to the element
this.$el.append(this.template);
//render provider types
this.ProviderTypes = RenderProviderTypes(this.model.attributes.ProviderTypes);
var providerTypesDiv = _.template($("#ProviderTypesTemplate").html());
$("#ProviderTypesDiv", this.$el).html(providerTypesDiv);
$("#ProviderTypesSelectDiv", this.$el).html(this.ProviderTypes.render().el);
$("#ProviderTypes", this.$el).val(this.model.attributes.ProviderType);
// render field collection
var collection = new FieldCollection(this.model.attributes.ProviderFieldList);
var fieldsView = new FieldCollectionAddView({
collection: collection
});
this.FieldsAddView = fieldsView;
// append the fields to the element
$("#fieldsDiv", this.$el).append(fieldsView.render().el);
this.stickit();
return this;
},
events: {
"submit #NewProviderForm": function (e) {
e.preventDefault();
if (this.FieldsAddView.isValid()) {
var fieldsView = this.FieldsAddView;
this.model.save(null, {}, { wait: true })
.success(function (result) {
var filters = new FilterCollection();
$.when(fieldsView.saveAll(),
filters.fetch({
data: $.param({
taskId: result.attributes.TaskId
})
}))
.done(function() {
if (!$("#FiltersForm").html()) {
var view = new FilterCollectionView({ collection: filters });
assign(view, "#FilterDetails");
$("#FiltersForm").append(buttonsTemplate);
$("#FilterDetailsTab").parent("li").removeClass("disabled");
$("#FilterDetailsTab").attr("data-toggle", "tab");
}
$("#FilterDetailsTab").tab("show");
});
})
.error(function (xhr, el, other) {
var message = JSON.parse(el.responseText).Message;
RenderError(message, "#ProviderDetailsTabError");
});
}
}
},
isValid: function () {
return this.model.isValid(true) && this.FieldsAddView.isValid();
},
save: function () {
this.model.save();
this.FieldsAddView.saveAll();
}
});
Ok, so I found how this has to be done. In short the code had to be changed so that it was syncing the entire collection at once using Backbone.sync instead of iterating over it and saving each model individually. Also when you're ready to save the collection you use the $.when function so it completes the whole sync before deciding what to do. Below is the relevant code showing the changes that were made.
var FieldCollection = Backbone.Collection.extend({
url: "/api/Field",
model: FieldModel,
syncAll: function () {
return Backbone.sync("create", this)
.error(function (xhr, el, other) {
var message = JSON.parse(xhr.responseText).Message;
var tab = "";
var activeTab = $("#Tabs li.active a").attr("id");
if (activeTab === "TaskListTab") {
tab = "#EditTaskDetailsTabError";
}
else if (activeTab === "NewTaskTab") {
tab = "#ProviderDetailsTabError";
}
RenderError(message, tab);
});
}
});
var FieldCollectionAddView = Backbone.View.extend({
saveAll: function () {
return this.collection.syncAll();
}
});
var ProviderAddView = Backbone.View.extend({
events: {
"submit #NewProviderForm": function (e) {
e.preventDefault();
if (this.FieldsAddView.isValid()) {
var fieldsView = this.FieldsAddView;
$.when(fieldsView.saveAll())
.done(function() {
// success
});
}
}
}
});
help me understand why the code is not working properly
my code:
var Link = Backbone.Model.extend({
defaults : {
groups: []
}
});
var LinkCollection = Backbone.Collection.extend({
model:Link,
url: 'item.json'
});
var Group = Backbone.Model.extend({
defaults: {
links: []
},
initialize : function() {
this.on("all" , function(event) {
console.log(event);
});
this.on("change:links" , function() {
console.log(this.get("links").length , this.id);
})
},
setLink: function(link) {
var links = this.get("links");
links.push(link);
this.set("links" , links);
this.trigger("change:links" , this);
},
removeLink : function(link) {
var links = this.get("links") , index = links.indexOf(link);
links.splice(index , 1);
this.set("links" , links);
this.trigger("change:links" , this);
}
});
var GroupCollection = Backbone.Collection.extend({
model:Group,
url: 'group.json',
setLinks : function(links) {
var self = this;
this.links = links;
this.links.on('all' , function(event) {
console.log(event);
});
this.links.on('add' , self.setLink , this);
this.links.on('remove' , this.removeLink , this);
this.links.on('reset' , this.resetLinks , this);
},
setLink : function(link) {
var self = this , test = false;
if(self.length ) {
link.get('groups').forEach(function(groupId) {
var group = self.get(groupId);
console.log(group , groupId);
if( group ) {
test = true;
group.setLink(link);
}
});
if(!test) {
self.get("notInGroup").setLink(link);
}
self.get("allObjects").setLink(link);
}
},
resetLinks : function() {
this.links.each(this.setLink);
},
initialize: function() {
var self = this;
this.on('reset' , self.resetLinks , this);
},
removeLink: function(link) {
var self = this;
link.get('groups').forEach(function(groupId) {
var group = self.get(groupId);
if(group) {
group.removeLink(link);
}
})
}
});
var LinkView = Backbone.View.extend({
tagName: 'li',
className : 'list-item',
template: _.template($("#ListView").html()),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this
}
});
var GroupView = Backbone.View.extend({
tagName : 'li',
className: 'group-list ',
template: _.template($("#GroupView").html()),
initialize: function() {
this.model.on('remove', function() {
console.log('remove');
});
this.model.on('reset' , function() {
console.log('reset');
});
this.model.on('destroy' , function() {
console.log('destroy')
});
},
render: function() {
var model = this.model.toJSON();
this.$el.html(this.template(model));
this.renderLinks(this.model);
this.model.on('change:links' , this.renderLinks.bind(this));
return this;
},
renderLinks : function(group) {
var self = this;
self.$el.find("ul").empty();
group.get("links").forEach(function(link) {
var view = new LinkView({model: link});
self.$el.find("ul").append(view.render().el);
});
}
})
var App = Backbone.View.extend({
el: $('#content'),
initialize: function() {
this.links = new LinkCollection();
this.groups = new GroupCollection();
this.groups.setLinks(this.links);
this.groups.bind('add', this.addGroup, this);
this.groups.bind('reset', this.addGroups, this);
this.links.fetch();
this.groups.fetch({reset: true});
return this;
},
render: function() {
this.$el.html($('#GroupListView').html())
},
addGroup: function(model) {
var view = new GroupView({model: model});
this.$("ul.group-list").append(view.render().el);
},
addGroups: function() {
this.groups.each(this.addGroup)
}
})
$(function() {
var app = new App();
app.render();
})
working example http://plnkr.co/edit/40CCIq0jt2AdmGD61uAn
but instead of the expected list of groups I get incorrect group
this list i tried to get:
All Objects
First
Second
Third
Fourth
Fifth
FirstGroup
First
Third
SecondGroup
Third
Fourth
NotInGroup
Second
Fifth
upd: Groups and Links can come at different times, so the order of their receipt is not critical. the problem is that for some reason the models groups added extra value, which should not be there
Screenshot added. Please look at the highlighted change
Already a couple of hours struggle trying to solve this...
Although the model gets fetched correctly and I can verify it as the view gets informed of the model's 'change' event, it just does not render.
At startup, the default model data ('Test Project'), is correctly displayed in the view, but after the model is refreshed, the view is not refreshed.
I tried to show a new view in the layout after model refresh but it did not change much...
Any idea or opinion about this ?
App.Project = function () {
var Project = {};
var ProjectModel = Backbone.Model.extend({
defaults:{
id: 0,
name: "Test Project",
intro: "",
desc: ""
},
initialize: function () {
// Flag fetch status to avoid multiple simultaneous calls
this.loading = false;
var self = this;
App.vent.on("project:display", function (_id) { self.fetchProject(_id); });
},
fetchProject: function (_id) {
if (this.loading)
return true;
this.loading = true;
var self = this;
var id = _id;
this.url = 'data.project_'+id+'.json';
this.fetch({
success: function (_data) {
self.loading = false;
},
error: function () {
self.loading = false;
}
});
}
});
Project.Details = new ProjectModel();
var Layout = Backbone.Marionette.Layout.extend({
template: "#project-layout",
regions: { details: "#project_details" }
});
Project.initializeLayout = function () {
Project.layout = new Layout();
App.content.show(App.Project.layout);
};
App.addInitializer(function () {
App.Project.initializeLayout();
});
Project.display = function () {
App.Project.Views.showDetails(Project.Details);
App.vent.trigger("project:display", 1);
}
return Project;
}();
App.Project.Views = function () {
var Views = {};
var DetailView = Backbone.Marionette.ItemView.extend({
template: "#project-details-template",
tagName: "div",
initialize: function () {
//this.listenTo(this.model, "change", this.render, this);
},
modelEvents: {
'change': "modelChanged"
},
modelChanged: function() {
console.log(this.model);
this.render();
}
});
Views.showDetails = function (_project) {
var projectView = new DetailView({model: _project});
App.Project.layout.details.show(projectView);
};
return Views;
}();
App.ProjectRouting = function () {
var ProjectRouting = {};
ProjectRouting.Router = Backbone.Marionette.AppRouter.extend({
initialize: function (_options) {
this.route('project/', "displayProject", _options.controller.display);
}
});
App.vent.on("project:display", function (_id) {
App.navigate("project/");
});
App.addInitializer(function (_options) {
ProjectRouting.router = new ProjectRouting.Router({
controller: App.Project
});
});
return ProjectRouting;
}();
I have a model which has both navid and subnavid .While destroying a model i need to check in the entire collection , for other models which have navid as same as subnavid of the model i'am trying to delete . Please help me out . Thanks in advance . Heregoes my sample code.
Model:
var Node = Backbone.Model.extend({
defaults: {
NavId: '',
SubNavId: ''.
ItemName:''
} }
Collection:
var NodeCollection = Backbone.Collection.extend({ model:Node }
And i have two view one for the Node(i am building tr) and other for
the collection(I need to build table) var NodeCollectionView =
Backbone.View.extend({
initialize: function (options) {
var self = this; self.collection = new NodeCollection({ NavigationId: options.NavigationId });
self.collection.fetch({
success: function () {
/*I am getting hte proper collection from my restful api and iam able to bind it properly
self.render();
}
});
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.RenderEachNode(item);
}, this);
},
RenderEachNode: function (item) {
var TempJsonNode = item.toJSON();
var self = this;
var nodeView = new NodeView({
tagName: 'tr',
id: 'NavId_' + TempJsonNode.NavItemId,
model: item
});
} });
var ItemTemplate = ""; ItemTemplate += " <td>"; ItemTemplate += " <a><%= ItemName %></a>"; ItemTemplate +=" </td>"; ItemTemplate
+=" <td>"; ItemTemplate +=" <a href='#' original-title='Delete ' class='tip_north Delete'>X</a>"; ItemTemplate +=" </td> ";
var NavigationItemView = Backbone.View.extend({
template: ItemTemplate,
render: function () {
var self = this;
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
},
events: {
"click .Delete": "DeleteBtnClick"
},
DeleteBtnClick: function () {
var self = this;
self.model.destroy({
success: function (status, data) {
var RetData = JSON.parse(data);
if (RetData.Status == 'Success') {
$(self.el).remove()
}
},
error: function () {
alert('Error In Deleting The Record');
}
});
return false;
} });
I am able to build the table properly but while destroying a model , i am not figuring out a way to destroy the dependent models.My Api is restricted in such a way that i cannot get a nested json ( if so i would have done with backbone relation). so i need to figure out some way that the other models and views which has the NavId of the model am deleting.
Please help me out.
How about something like:
var NodeView = Backbone.View.extend({
initialize: function() {
//when the model gets destroyed, remove the view
this.listenTo(this.model, 'destroy', this.remove);
},
//..clip
DeleteBtnClick: function () {
var self = this;
var collection = self.model.collection;
var navId = self.model.get('NavId');
self.model.destroy({
success: function (status, data) {
var RetData = JSON.parse(data);
if (RetData.Status == 'Success') {
//if parent was part of a collection
if (collection) {
//find related models
var related = collection.filter(function (model) {
return model.get('SubNavId') === navId;
});
//call destroy for each related model.
var promises = _.invoke(related, 'destroy');
//optional: if you want to do something when all the children
//are destroyed:
$.when.apply($, promises).then(function () {
console.log('all destroyed');
});
}
}
},
error: function () {
console.log(arguments);
alert('Error In Deleting The Record');
}
});
return false;
}
});
Edit: JSFiddle here