I added this to my backbone-extend.js file which resides in the same folder as backbone-min.js...
_.extend(Backbone.View.prototype, {
getFormData: function(form) {
var unindexed_array = form.serializeArray();
var indexed_array = {};
$.map(unindexed_array, function(n, i){
indexed_array[n['name']] = n['value'];
});
return indexed_array;
}
});
However, when I call this.getFormData in my view code, I get a method not defined error. What am I missing? Thanks for your help!
Edit: Here is my view. I have to uncomment the getFormData method to make it work. It can't see the getFormData otherwise...
define([
'jquery',
'underscore',
'backbone',
'models/Member',
'text!templates/memberEditTemplate.html'
], function($, _, Backbone, Member, memberEditTemplate) {
var MemberEditView = Backbone.View.extend({
el: $("#page"),
model: 'member',
initialize: function(args) {
this.member = new Member({ id: args.id });
this.member.on('error', this.eventSyncError, this);
this.member.on('sync', this.eventSyncModelLoaded, this);
this.member.fetch();
},
events: {
"click #bttnMemberSave": "bttnClickMemberSave"
},
eventSyncError: function(model,response,options) {
console.log('Sync error='+response.statusText);
$('#server-message').css({'color':'red', 'font-weight':'bold'}).text(response.statusText);
//$('#server-message').text(response.statusText);
},
eventSyncModelLoaded: function(model,response,options) {
this.render();
},
eventSyncModelSaved: function(model,response,options) {
console.log("Member saved!");
$('#server-message').css({'color':'green', 'font-weight':'bold'}).text("Member saved!");
//$('#server-message').text('Member saved!');
var to = setTimeout(function() { Backbone.history.navigate('members', true); }, 2000);
},
bttnClickMemberSave: function() {
var data = this.getFormData($('#member-form').find('form'));
this.member.save(data, { success: this.eventSyncModelSaved });
},
// getFormData: function(form) {
// var unindexed_array = form.serializeArray();
// var indexed_array = {};
// $.map(unindexed_array, function(n, i){
// indexed_array[n['name']] = n['value'];
// });
// return indexed_array;
// },
render: function() {
this.member.toJSON();
var compiledTemplate = _.template( memberEditTemplate, { member: this.member } );
this.$el.html( compiledTemplate );
return this;
}
});
return MemberEditView;
});
Ok, I added backbone-extend.js to the RequireJS required files array in my app.js, now it's working.
Related
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
I am working on a Backbone Application that contains multiple routes. I would like to modify my changePage function and make it take a hash as a parameter and when it is called it changes the page basing on that URL.
This is my router
define(["jquery","backbone",'views/header'], function( $, Backbone, Header) {
var ApplicationRouter = Backbone.Router.extend({
_header: null,
routes: {
"": "login",
"home": "home",
},
initialize: function() {
this.firstPage = true;
this.fromBack = false;
i18n.currentLocal = localStorage.getItem("currentLocal") || 'en';
Backbone.history.start();
},
login: function() {
var self = this;
require(['views/loginPageView'], function(loginView){
self.changePage(new loginView(),true);
});
},
home: function() {
var self = this;
require(['views/homePageView'], function(homeView){
self.changePage(new homeView(),false);
});
},
changePage: function (page,noPanel) {
var deferred = $.Deferred();
$page = $(page.el);
$page.attr('data-role', 'page');
console.log(this.firstPage);
if(this.firstPage){
$('#pageContent').append($page);
page.render();
if(!noPanel){
self._header = new Header({parent: $("#header")});
}
}
else{
$('#pageContent').html($page);
$("#header").empty();
page.render();
}
if (this.firstPage) {
this.firstPage = false;
}
deferred.resolve();
return deferred;
},
});
return ApplicationRouter;
})
This is the First View :
define([ "jquery", "backbone","text!../../pages/login.html"], function($, Backbone,loginTpl) {
var loginPageView = Backbone.View.extend({
events :{
"click #login" : "login",
"change input[type=radio]":"changeLanguage"
},
initialize : function() {
//Some Code
},
render: function() {
this.template = _.template(loginTpl);
$(this.el).html(this.template);
return this;
},
login:function(){
console.log("Login Clicked");
router.navigate('home', {trigger: false, replace: false});
},
changeLanguage:function(){
//console.log("change lang");
if(i18n.currentLocal == 'en'){
i18n.currentLocal='fr';
this.render();
$("#month2").attr('checked',true);
}else
{
if(i18n.currentLocal == 'fr'){
i18n.currentLocal='en';
this.render();
}
}
}
});
return loginPageView;
});
So instead of using router.navigate which caused some problems due to bugs in the router I think, I would like to use router.changePage('home.html');
and based on the URL I load to respective View.
Thank You
I still strugling with memory leak in my app. I wannted to do it without huge changes in code.
var ItemsView = Backbone.View.extend({
id:'products', // If I change it to el: document.getElementById('products') and without passing views into items object, my views are properly rendered but with memory leak
events: { },
initialize: function() {
_.bindAll(this);
this.listenTo(this.collection, 'reset', this.reset);
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'change', this.changeItem);
this.listenTo(this.collection, 'destroy', this.delItem);
this.items = [];
},
reset: function(){ console.log("reset");
this.el.innerHTML = null;
this.render();
},
render: function(){
for(var i=0; i < this.collection.length; i++){
this.renderItem(this.collection.models[i]);
}
},
renderItem: function( model ){
var itemView = new ItemView({ model: model });
itemView.render();
this.items.push(itemView);
jBone(this.el).append(itemView.el);
},
addItem: function(){ console.log("addItem");
this.renderItem();
},
changeItem: function(){ console.log("changeItem"); },
delItem: function(){ console.log("delItem"); },
remove: function() {
_.invoke(this.items, 'remove');
this.items = [];
Backbone.View.prototype.remove.call(this);
}
});
return ItemsView;
This is my Itemsview it is executed when user hit orderview, there is created ItemView for every model in collection:
var ItemView = Backbone.View.extend({
tagName: "li",
className: "productcc",
initialize: function () {
_.bindAll(this, 'addItem', 'removeItem', 'updateItem');
this.listenTo(this.model, 'remove', this.removeItem);
this.listenTo(this.model, 'change', this.updateItem);
},
events: {},
render: function () {
var model = this.model.toJSON();
this.el.innerHTML += '<div class="tabody"><h4 class="tablename">'+model.title+'<h4>'+model.status+'</div>';
return this;
},
addItem: function(){
this.collection.create({"table_no":"demo"});
},
changeItem: function(e){
e.preventDefault();
this.model.save({ table_no: 'demo' });
},
updateItem: function(newstuff){
console.log("updateItem");
console.log(this.el);
},
delItem: function(){
this.model.destroy({ silent: true });
},
removeItem: function(model){
console.log("removeItem");
console.log(model);
var self = this;
self.el.remove();
}
});
return ItemView;
MY ROUTER:
var AppRouter = Backbone.Router.extend({
routes: {
'' : 'home',
'home' : 'home',
'customer/:customer_id': 'showItems'
}
});
var initialize = function(options) {
window.app_router = new AppRouter;
window.socket = io.connect('www.example.com');
this.socketOrdersCollection = new SocketOrdersCollection();
this.ordersView = new OrdersView({ collection: this.socketOrdersCollection });
this.socketOrdersCollection.fetch({ reset: true });
app_router.on('route:home', function() { });
app_router.on('route:showItems', function(customer_id) {
if (this.itemsView) {
this.itemsView.remove();
}
this.socketItemsCollection = new SocketItemsCollection();
this.socketItemsCollection.fetch({ data: { id: customer_id}, reset: true });
this.itemsView = new ItemsView({
collection: this.socketItemsCollection,
model: { tableName: customer_id }
});
});
Backbone.history.start();
};
I have to remove also ItemsView after click to another order...
Thanks for any opinion.
Ok. Let me take a stab at what you're attempting here.
var ItemsView = Backbone.View.extend({
el: document.getElementById('products'),
events: { },
initialize: function() {
// everything you had before
this.items = [];
},
// etc.
renderItem: function( model ){
var itemView = new ItemView({ model: model });
itemView.render();
this.items.push(itemView);
jBone(this.el).append(itemView.el);
},
// etc.
// we're overloading the view's remove method, so we clean up our subviews
remove: function() {
_.invoke(this.items, 'remove');
this.items = [];
Backbone.View.prototype.remove.call(this);
}
});
return ItemsView;
And then in the router:
var initialize = function(options) {
// etc.
app_router.on('route:home', function() { });
app_router.on('route:showItems', function(customer_id) {
if (this.itemsView) {
this.itemsView.remove();
}
// everything else the same
});
Backbone.history.start();
};
So now, your ItemsView will clean up any child items it has, and when you change customers, you'll clean up any ItemsView you have open before generating a new one.
EDIT
I see what you're having a problem with now.
In your route handler, you're going to need to do something along these lines:
app_router.on('route:showItems', function(customer_id) {
// everything you already have
jBone(document.getElementById('container')).append(this.itemsView);
});
I am trying to render a View(customerEditView) using modular BackboneJS but it gives me the uncaught type error.
this is my router.js file:
define([
'jquery',
'underscore',
'backbone',
'views/Customers/CustomerEditView',
'views/Customers/CustomerListView'
], function($, _, Backbone, CustomerEditView, CustomerListView) {
var Router = Backbone.Router.extend({
routes: {
"customers": "customerhome",
"editcustomer/:id": "editcustomer",
"newcustomer": "editcustomer",
},
customerhome: function () {
var customerListView = new CustomerListView();
customerListView.render();
},
editcustomer: function (id) {
var customerEditView = new CustomerEditView();
customerEditView.render({ id: id });
}
});
var initialize = function () {
var router = new Router;
Backbone.history.start();
};
return {
initialize: initialize
};
});
and this is my customerEditView file:
define([
'jquery',
'underscore',
'backbone',
'router',
'models/Customers/Customer',
'helper/Serialize',
'text!template/Customer/CustomerEditTemplate.html'
], function ($, _, Backbone, router, Customer, Router, serializeObject, CustomerEditTemplate) {
var CustomerEditView = Backbone.View.extend({
el: '.page',
events: {
'submit .edit-customer-form': 'saveCustomer',
'click .delete': 'deleteCustomer'
},
saveCustomer: function (ev) {
var customerDetails = $(ev.currentTarget).serializeObject();
var customer = new Customer();
customer.save(customerDetails, {
success: function (customer) {
Backbone.history.navigate('', { trigger: true });
}
});
return false;
},
deleteCustomer: function (ev) {
this.customer.destroy({
success: function () {
console.log('destroyed');
Backbone.history.navigate('', { trigger: true });
}
});
return false;
},
render: function (options) {
var that = this;
if (options.id) {
that.customer = new Customer({ id: options.id });
that.customer.fetch({
success: function (customer) {
var template = _.template(CustomerEditTemplate);
that.$el.html(template({ customer: customer }));
}
});
} else {
var template = _.template(CustomerEditTemplate);
that.$el.html(template({ customer: null }));
}
}
});
return CustomerEditView;
});
I am new to backbone and marionette. Now i m trying to implement paging with compositeview of marionettejs. Below is my code, what happens here that when a new fetch is done through my custom pager, existing data is getting replaced by new set of data instead of appending. Please help me to overcome this! Thanks in advance.
define(['text!/Templates/projects/_GroupItem.html', 'collections/projects/groups'], function (ProjectGroupsTmpl, GroupCollection) {
var GroupItemView = Backbone.Marionette.ItemView.extend({
tagName: 'li',
template: _.template(ProjectGroupsTmpl)
});
var CompositeView = Backbone.Marionette.CompositeView.extend({
template: _.template("<ul id='ulgroups' ></ul>"),
itemView: GroupItemView,
itemViewContainer: '#ulgroups',
initialize: function (params) {
this.isLoading = false;
this.ProjectID = params.id;
this.collection = new GroupCollection();
this.getData();
var self = this;
$(window).scroll(function () {
self.checkScroll();
});
},
getData: function () {
var that = this;
this.isLoading = true;
this.collection.fetch({
data: { ProjectID: this.ProjectID },
success: function (collection, response, options) {
that.isLoading = false;
}
});
},
checkScroll: function () {
var triggerPoint = 100; // 100px from the bottom
if (!this.isLoading && $(window).scrollTop() + $(window).height() + triggerPoint > $(document).height()) {
this.collection.page += 1; // Load next page
this.getData();
}
},
appendHtml: function (collectionView, itemView, index) {
$(this.itemViewContainer).append(itemView.el);
}
});
return CompositeView;
});
I have used backbone.paginator to resolve above issue and it works well. Below are the new code used for that.
Collection:
define([
'jquery',
'underscore',
'backbone',
'helper',
'paginator'
], function ($, _, Backbone) {
var Groups = Backbone.PageableCollection.extend({
url: 'projects/_groups',
mode: "infinite",
state: {
pageSize: null
},
queryParams: {
totalPages: null,
totalRecords: null
}
});
return Groups;
});
Marionette CompositeView:
define(['text!/Templates/projects/_GroupItem.html', 'collections/projects/groups'], function (ProjectGroupsTmpl, GroupCollection) {
var GroupItemView = Backbone.Marionette.ItemView.extend({
tagName: 'li',
template: _.template(ProjectGroupsTmpl)
});
var CompositeView = Backbone.Marionette.CompositeView.extend({
template: _.template("<ul id='ulgroups' ></ul>"),
itemView: GroupItemView,
itemViewContainer: '#ulgroups',
initialize: function (params) {
this.isLoading = false;
this.ProjectID = params.id;
this.grpcollection = new GroupCollection([], {
queryParams: {
ProjectID: params.id
}
});
this.collection = this.grpcollection.fullCollection;
this.getData();
var self = this;
$(window).scroll(function () {
self.checkScroll();
});
},
getData: function () {
var that = this;
this.isLoading = true;
this.grpcollection.fetch({
success: function (collection, response, options) {
if (response.length > 0) {
that.isLoading = false;
}
}
});
},
getNextPage: function () {
var that = this;
this.isLoading = true;
this.grpcollection.getNextPage({
success: function (collection, response, options) {
if (response.length > 0) {
that.isLoading = false;
}
}
});
},
checkScroll: function () {
var triggerPoint = 100; // 100px from the bottom
if (!this.isLoading && $(window).scrollTop() + $(window).height() + triggerPoint > $(document).height()) {
this.getNextPage();
}
},
appendHtml: function (collectionView, itemView, index) {
$(this.itemViewContainer).append(itemView.el);
}
});
return CompositeView;
});
I solved a similar problem recently by creating a temporary collection to hold the models for each paginated request. My setup was slightly different to yours, however, in that I created a Marionette controller to negotiate between the data and the view. A "show" method on the controller handled the initial data request and a "showMore" method handled subsequent requests. Here is basically what I did:
(function ($, _, Backbone, Marionette) {
var carData = [
{
make: 'Audi',
model: 'A4',
year: '1994'
},
{
make: 'BMW',
model: '3 Series',
year: '1975'
},
{
make: 'Chevrolet',
model: 'Cruze',
year: '2008'
},
{
make: 'Daimler',
model: 'Six',
year: '1994'
},
{
make: 'Fiat',
model: '500X',
year: '2015'
},
{
make: 'Honda',
model: 'Civic',
year: '1972'
},
{
make: 'Kia',
model: 'Optima',
year: '2015'
},
{
make: 'Lada',
model: 'Priora',
year: '2007'
},
{
make: 'Mitusbishi',
model: 'Lancer',
year: '1973'
},
{
make: 'Nissan',
model: 'Pathfinder',
year: '1995'
}
];
var Car = Backbone.Model.extend({
defaults: {
make: '',
model: '',
year: ''
}
});
var Cars = Backbone.Collection.extend({
model: Car,
rows: 3,
page: 0
});
var CarView = Marionette.ItemView.extend({
tagName: 'tr',
template: '#row-template'
});
var CarsView = Marionette.CompositeView.extend({
childView: CarView,
childViewContainer: 'tbody',
template: '#table-template',
triggers: {
'click button': 'showMore'
}
});
var CarController = Marionette.Controller.extend({
initialize: function (options) {
this.collection = options.collection;
},
show: function () {
var cars = this.getData(this.collection.page);
var carsView = new CarsView({
collection: new Backbone.Collection(cars)
});
this.listenTo(carsView, 'showMore', this.showMore);
app.carsRegion.show(carsView);
},
showMore: function (options) {
var cars = this.getData(++this.collection.page);
options.collection.add(cars);
},
getData: function (page) {
var rows = this.collection.rows;
var start = page * rows;
var end = start + rows;
return this.collection.slice(start, end);
}
});
var app = new Marionette.Application();
var cars = new Cars(carData);
var carController = new CarController({
collection: cars
});
app.addRegions({
carsRegion: '#cars-region'
});
app.addInitializer(function () {
carController.show();
});
app.start();
}(jQuery, _, Backbone, Marionette));
This is also available as a JSFiddle.