Backbone collection is not rendering - backbone.js

I have problem with render of collection. Its simple model with title and boolean 'completed', when you click on list item it's changing to completed value (true/false). Value is changed ( I know it because when I refresh page, in initialize after fetch() I have collection.pluck, where order made by comparator is correct), but view looks all the time the same.
In collection I have comparator which works like I described upper, after collection.fetch() I have pluck, and pluck gives me well sorted list (but in view I see bad, default order). I dont know how to refresh collection to be well sorted.
Collection is just:
var TodolistCollection = Backbone.Collection.extend({
model: TodoModel,
localStorage: new Store('todos-backbone'),
// Sort todos
comparator: function(todo) {
return todo.get('completed');
}
});
Model is:
var TodoModel = Backbone.Model.extend({
defaults: {
title: '',
completed: false
},
// Toggle completed state of todo item
toggle: function(){
this.save({
completed: !this.get('completed')
});
}
});
return TodoModel;
Single todoView is:
var TodoView = Backbone.View.extend({
tagName: 'li',
template: JST['app/scripts/templates/todoView.ejs'],
events: {
'click .js-complete': 'toggleCompleted'
},
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html( this.template( this.model.toJSON() ));
this.$el.toggleClass( 'l-completed', this.model.get('completed') );
return this;
},
// Toggle the `"completed"` state of the model.
toggleCompleted: function() {
this.model.toggle();
}
and app View:
var ApplicationView = Backbone.View.extend({
el: $('.container'),
template: JST['app/scripts/templates/application.ejs'],
events: {
'click #add' : 'createOnEnter',
'keypress #addTodo' : 'createOnEnter'
},
// aliasy do DOMu,
// nasluchiwanie w kolekcji czy zaszlo jakies zdarzenie, jesli tak, to wykonuje funkcje
initialize: function() {
this.$input = this.$('.js-input');
this.listenTo(todoList, 'add', this.addOne);
this.listenTo(todoList, 'reset', this.addAll);
this.listenTo(todoList, 'all', this.render);
todoList.fetch();
console.log(todoList.pluck('title'));
},
render: function() {
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
title: this.$input.val().trim(),
completed: false
};
},
// Tworzy nowy model dzieki newAtributes() do localStorage
addTodo: function ( e ) {
e.preventDefault();
if( e.which !== Common.ENTER_KEY || !this.$input.val().trim() ){
return;
}
todoList.create( this.newAttributes() );
this.$input.val('');
},
// Tworzy model i dopisuje go do listy
addOne: function( todo ){
var view = new todoView({ model: todo });
$('.js-todolist').append( view.render().el );
},
// Tworzy nowego todo gdy nacisniemy enter
createOnEnter: function( e ) {
if( e.which !== Common.ENTER_KEY || !this.$input.val().trim() ){
return;
}
todoList.create( this.newAttributes() );
this.$input.val('');
},
// Przy rerenderze, dodaj wszystkie pozycje
addAll: function() {
this.$('.js-todolist').html('');
todoList.each(this.addOne, this);
}
});
return ApplicationView;
When I change listenTo render like that: this.listenTo(todoList, 'all', function(){console.log('whateva')}); I can see that on my click 'all' is triggering (even three times per one click ;s ).
Its hard for me to put it on jsfiddle, but here's git link with all files: https://github.com/ozeczek/ozeczek/tree/master/bb-todo-yo/app/scripts

In app view initialize I've changed todoList.fetch() to todoList.fetch({reset:true});
Second problem was to show in browser right order of todos, I've added to initialize:
this.listenTo(todoList, 'change', this.walcze);
this.listenTo(todoList, 'remove', this.walcze);
and walcze body function is:
walcze: function(){
todoList.sort();
this.$('.js-todolist').html('');
todoList.each(this.addOne, this);
}
Now every time Todo paremeter complete is changed, Im sorting list (comparator by itself isn't), clearing div with list, and rewriting whole list. I think it is not the best way of doing it, but it works.

Related

Adding new Item to collection on 'add' triggers only once

I am pretty new to backbone,so probably it is stupid bug.
When I press send button(#send_email_button) , one email is rendered as it should,
but when i press it again, no more emails added.
the only logs i got is:(after second+ push)
:
console.log('adding to collection');
console.log('about to exit');
in other words it does not even enters add handler in collection.
Can someone explain why and how to fix this?
Many thanks!
EDIT: If i delete 1 email that rendered , and press send again, new email added correctly.
Here relevant code:
$(document).ready(function() {
//email model
var EmailModel = Backbone.Model.extend({
defaults: {
id: '',
email_from: '',
email_recipient: '',
email_subject: '',
email_data: '',
is_email_read: '',
email_date: ''
}
});
//email collection
var email_collection = Backbone.Collection.extend({
model: EmailModel,
url: '/fetch_received_emails'
});
var email_collection = new email_collection();
var EmailView = Backbone.View.extend({
model: new EmailModel(),
tagName:'li',
events: {
"click #email_template_view" : "click_email"
},
initialize: function() {
console.log('initializing email view');
this.template = _.template($('#email_template').html());
console.log('finish initializing email view');
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
click_email: function() {
this.model.is_email_read = true;
$('#toggle_part_email').toggleClass('no_display');
},
});
var CollectionView = Backbone.View.extend({
model: email_collection,
el: $('#emails_class'),
initialize: function() {
console.log('init to collection view');
this.model.fetch();
this.render();
this.model.on('change', this.render, this);
this.model.on('add', this.render, this);
this.model.on('remove', this.render, this);
},
render: function(){
console.log('rendering collection');
var that = this,
i;
that.$el.html('');
emails = this.model.toArray();
for (i in emails){
console.log(' printing emails');
console.log(emails[i]);
var new_email_view = new EmailView( {model : emails[i]});
that.$el.append(new_email_view.render().$el);
}
console.log('about to exit collection view');
return this;
}
});
$('#send_email_button').click(function(event){
// event.preventDefault();
var sending_date= new Date();
sending_date = sending_date.toDateString()
//new email to ajax
console.log('adding to collection');
email_collection.add(new EmailModel({
'email_from':$('#email_from').val(),
'email_recipient' :$('#email_recipient').val(),
'email_subject': $('#email_subject').val(),
'email_data':$('#email_data').val(),
'is_email_read':false,
'email_date': sending_date
}));
console.log('about to exit');
return false;
});
//create singelton for the collection view
var c = new CollectionView();
});
Why don't you try to use the click event as the other one? Inside the collectionView use events again.
events: {
"click #send_email_button" : "AnyNameThatYouWant"
},
AnyNameThatYouWant: function() {
//Do all the things
},
Try this.

Multiple Views and Sub Views with 1 collection in Backbone

I have issue in rendering Shopping Bag Views using Backbone for my website.
I am using 1 collection for all Bag Views (“Quick_View” of items list & “Normal_View” of Items list). Also I created “Item_View”, which is being used to render each item in both the Views.
It is a SPA (Single Page Application) and “Quick_View” is initiated and rendered for all Backbone routes and hidden by default. Whenever user clicks on “Quick_View” link from any page it is showing. There is no route defined for it.
The “Normal_View”, can be accessed using Checkout button given in “Quick_View”. It is bind with “domain/checkout” route.
When I access “Normal_View” from “Quick_View” check button; it works fine and both (Quick and Normal) views are in Sync. Means, when we add, delete, update any item in any of the View, both Views are getting updated accordingly.
But when I access “domain/checkout” route directly in a new browser, both views are getting rendered fine, but they are not in sync. Means, change in 1 view does not update another view.
The reason, I tracked is, when I access “Normal_View” through “Quick_View”, model for each item in both the Views having same CID, so the both Views are in sync, if there is any change in a model from any of the View.
And, when I access “Normal_View” directly, model for each item in both the views are not having same CID, so they do not work as expected.
There are few more points to consider:
Collection is firing reset event twice for “Quick_View” and each item
in “Quick_View” is rendering twice.
When, I access “Normal_View” (in either way), “Quick_View” is again getting rendered but once “Normal_View” rendering is over.
// Main View
var mainView = Backbone.View.extend({
el: 'body',
template: {
header: Handlebars.compile(headerTemplate),
mainNav: Handlebars.compile(mainNavtemplate),
footer: Handlebars.compile(footerTemplate)
},
initialize: function() {
_.bindAll();
AW.collection.bag = new bagCollection();
//AW.collection.bag.fetch({reset:true});
},
render: function() {
this.$el.html(this.template());
this.loadSubView('bagQV');
},
loadSubView: function(subView) {
switch(subView) {
case 'home' :
if(!AW.view.home) AW.view.home = new homepageView();
AW.view.home.render();
break;
case 'bagQV' :
if(!AW.view.bagQV) AW.view.bagQV = new bagQuickView({collection: AW.collection.bag});
//AW.view.bagQV.render();
break;
case 'checkout' :
if(!AW.view.checkout) AW.view.checkout = new checkoutView({collection: AW.collection.bag});
AW.view.checkout.render();
break;
}
}
});
// Single Item View
var bagItemView = Backbone.View.extend({
tagName: 'tr',
template: Handlebars.compile(bagItemTemplate),
initialize: function() {
_.bindAll(this);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'remove', this.removeItem);
$(document).on('keyup', this.listenKeyboard);
},
events: {
'click .qtyInput .button' : 'updateItem',
'click .controls a.remove' : 'removeModel'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.attr('data-id',this.model.cid);
return this;
},
updateItem: function(e) {
e.preventDefault();
e.stopPropagation();
var newQty = this.$el.find('.qtyInput input').val();
var newAmt = newQty * parseFloat(this.model.get('prodRate').replace('$',''));
this.model.set({prodQty: newQty, amount: '$' + newAmt});
this.cancelEdit(e);
},
removeModel: function(e) {
e.preventDefault();
e.stopPropagation();
if(AW.collection.bag) AW.collection.bag.remove(this.model);
},
removeItem: function() {
this.$el.remove();
}
});
// Bag Quick View
var bagQuickView = Backbone.View.extend({
tagName: 'div',
id: 'myBagQV',
template: Handlebars.compile(bagQuickViewTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
if($('#myBagQV').length == 0) {
this.$el.html(this.template());
$('body').append(this.el);
}
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
$('#itemsInBag table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
$('#itemsInBag table tbody').html('');
this.collection.each(this.addItem, this);
}
},
});
// Normal Bag View
var bagView = Backbone.View.extend({
tagName: 'div',
id: 'myBag',
template: Handlebars.compile(checkoutBagTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.$el.html(this.template());
$('#checkoutContainer #details').append(this.el);
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
this.$el.find('table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
this.$el.find('table tbody').html('');
this.collection.each(this.addItem, this);
}
}
});
Looking for you help.
Thank you in advance
Cheers,
Vikram

Calling destroy on collection

I am doing a sample application similar to the Backbone-Todo. But when I am invoking destroy on collection it's giving error:
Uncaught TypeError: Cannot read property 'destroy' of undefined
How can I solve this problem. Please suggest.
Following is my method code:
$(function(){
var Todo = Backbone.Model.extend({
defaults: function() {
return {
title: "empty todo...",
order: Todos.nextOrder(),
done: false
};
}
});
var TodoList = Backbone.Collection.extend({
model : Todo,
localStorage: new Backbone.LocalStorage("todos-backbone"),
done: function() {
return this.where({done: true});
},
remaining: function() {
return this.without.apply(this, this.done());
},
nextOrder: function() {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
comparator: 'order'
});
var TodoView = Backbone.View.extend({
tagName: "li",
template: _.template($('#item-template').html()),
events: {
"click a.destroy" : "clear"
},
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
clear: function(){
this.model.destroy();
}
});
var AppView = Backbone.View.extend({
el: $("#todoapp"),
statsTemplate: _.template($('#stats-template').html()),
events: {
"keypress #new-todo": "createOnEnter",
"click #remove-all": "clearCompleted"
},
initialize: function() {
this.input = this.$("#new-todo");
this.main = $('#main');
this.footer = this.$('footer');
this.listenTo(Todos, 'add', this.addOne);
this.listenTo(Todos, 'all', this.render);
Todos.fetch();
},
render: function() {
var done = Todos.done().length;
var remaining = Todos.remaining().length;
if (Todos.length) {
this.main.show();
this.footer.show();
this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
} else {
this.main.hide();
this.footer.hide();
}
},
createOnEnter: function(e){
if(e.keyCode != 13) return;
if (!this.input.val()) return;
Todos.create({
title: this.input.val()
})
this.input.val('');
},
addOne: function(todo){
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
},
clearCompleted: function(){
_.invoke(Todos, 'destroy');
return false;
}
});
for this answer I assume Todos is an instance of TodoList. I also assume that your error is fired by this function in your AppView
clearCompleted: function(){
_.invoke(Todos, 'destroy');
return false;
}
In there you're trying to treat your Backbone.js Collection instance like what it is, a collection eg a list. But Backbone collections are not simply lists, they are objects that have the property models which is a list that contains all your models. So trying to use underscore's invoke (which works on lists) on an object is bound to cause errors.
But don't worry, Backbone neatly implements many Underscore methods for its Model and Collection, including invoke. This means you can invoke destroy for each model in a collection like this
SomeCollection.invoke('destroy');
Hope this helps!

backbone.js: add item to collection right after current item

I'm trying to create a invoice rows list with backbone and one of the buttons/events is that when You click "add" on an existing row, it should add a new row right below the one that's "add" button was clicked.
First, I am not sure if I should handle this event in the InvoiceItemView or InvoiceItemListView, and second, I am not sure how to do this. Can anybody help me?
I tried this:
$(function(){
// Invoice row
var InvoiceItem = Backbone.Model.extend({
// Set default values
defaults : {
name: "",
quantity: 1,
price: "",
total: ""
},
// Ensure each item has at least a quantity of one
initialize: function() {
this.set({ "quantity" : this.defaults.quantity });
},
// Delete item (row) from invoice
clear: function() {
this.destroy();
}
});
// Collection of Invoice Items ( rows )
var InvoiceItemList = Backbone.Collection.extend({
// Colelction's model
model: InvoiceItem,
// Use localStorage to save data
localStorage: new Store("invoices-backbone"),
// Calculate invoice totals
calculate_totals: function() {
alert('arvutus');
},
// Generate next item order number
nextOrder: function() {
if (!this.length) return 1;
return this.last().get('order') + 1;
}
});
// Create a global colelction of Invoice Rows
var InvoiceItems = new InvoiceItemList;
// Invoice Item View
var InvoiceItemView = Backbone.View.extend({
// it's a tr tag
tagName: 'tr',
// Cache the template
template: _.template($('#invoiceitem-template').html()),
events: {
"click .remove" : "clear",
"click .add" : "addAfter"
},
initialize: function() {
_.bindAll(this, 'render', 'addAfter', 'remove');
this.model.bind('change', this.render);
this.model.bind('destroy', this.remove);
this.render();
},
// Render the contents
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
$(this.el).attr('id', 'item-' + this.model.get('order'));
return this;
},
// Remove and destroy the item
clear: function() {
this.model.clear();
},
// Add new item
addAfter: function(ItemId) {
var view = new InvoiceItemView({model: InvoiceItem});
this.$("tbody tr#item"+ItemId).after(view.render().el);
}
});
// Invoice Item List View
InvoiceItemListView = Backbone.View.extend({
// Bind to existing DOM element
el: $("#items"),
// Template for displaying totals
totalsTemplate: _.template($('#totals-template').html()),
// Kick-off (load from localStorage as well)
initialize: function() {
_.bindAll(this, 'addOne', 'render');
InvoiceItems.bind('add', this.addOne);
InvoiceItems.fetch();
InvoiceItems.create(this.newAttributes());
InvoiceItems.create(this.newAttributes());
InvoiceItems.create(this.newAttributes());
this.render();
},
// Re-render the totals
render: function() {
this.$('#totals').html(this.totalsTemplate({
total: this.total
}));
},
// Generate the attributes for a new Invoice Item.
newAttributes: function() {
return {
name: '',
price: '',
total: '',
order: InvoiceItems.nextOrder()
};
},
// Add a single invoice item to the list
addOne: function(InvoiceItem) {
var view = new InvoiceItemView({model: InvoiceItem});
this.$("tbody").append(view.render().el);
}
});
var iView = new InvoiceItemListView;
});
But I am getting an error: this.model.toJSON is not a function
The problem is where you create your new InvoiceItemView. You have to create a new instance of InvoiceItem
// Add new item
addAfter: function(ItemId) {
var view = new InvoiceItemView({model: new InvoiceItem()});
this.$("tbody tr#item"+ItemId).after(view.render().el);
}

Backbone.js - Correct way of filtering and displaying collection data in a view

I have got a huge list of tasks loaded on the start.
I want to show them depending on selected list / inbox, so that there won't be additional loadings for each list.
window.Task = Backbone.Model.extend({});
window.TasksCollection = Backbone.Collection.extend({
model: Task,
url: '/api/tasks',
inbox: function() {
return this.filter(function(task) {
return task.get('list') == null;
});
},
list: function(id) {
return this.filter(function(task) {
return task.get('list') == id;
});
}
});
window.tasks = new TasksCollection;
window.TaskView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#item-template').html()),
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
this.setContent();
return this;
},
});
window.TasksView = Backbone.View.extend({
el: '#todo-list',
collection: tasks,
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
var t = this;
$(t.el).html('');
this.collection.each(function(task) {
var view = new TaskView({ model:task });
$(t.el).append( view.render().el );
});
return this;
},
});
window.Nicetask = Backbone.Router.extend({
routes: {
'': 'inbox',
'/inbox': 'inbox',
'/list/:id': 'list',
},
initialize: function() {
_.bindAll(this, 'inbox', 'list');
window.tasksView = new TasksView;
},
inbox: function() {
tasks.reset( tasks.inbox() );
},
list: function(id) {
tasks.reset( tasks.list(id) );
}
});
This code works, but the reset() function removes other tasks in actual list from tasks collection. And on another route, tasks collection is empty.
Is there any reasonable way to achieve this? thanks for any idea.
ps: backbone novice
UPDATE
Thx to #sled and #ibjhb for comments, here is snippet of working solution.
window.TasksView = Backbone.View.extend({
el: '#todo-list',
collection: Backbone.Collection.extend(),
initialize: function() {
_.bindAll(this, 'render', 'addOne', 'addAll');
this.collection.bind('add', this.addOne);
this.collection.bind('reset', this.render);
},
render: function(data) {
$(this.el).html('');
_.each(data, function(task) {
this.addOne(task);
}, this);
return this;
},
addOne: function(task) {
var view = new TaskView({ model:task });
$(this.el).append( view.render().el );
},
});
window.Nicetask = Backbone.Router.extend({
routes: {
'': 'inbox',
'/inbox': 'inbox',
'/today': 'today',
'/list/:id': 'list',
},
initialize: function() {
_.bindAll(this, 'inbox', 'today');
window.tasksView = new TasksView;
window.menuView = new MenuListView;
tasks.fetch();
},
inbox: function() {
tasksView.render( tasks.inbox() );
},
today: function() {
tasksView.render( tasks.today() );
},
list: function(id) {
tasksView.render( tasks.list(id) );
}
});
I think you need to use another collection. For example, in your inbox, do this:
inbox: function(){
currentCollection = new TasksCollection(tasks.inbox());
}
I haven't tested this but when you do a .reset(); you are removing all your models and loading the ones passed in.
#sled there's typos in the code you posted, see comments inline. Did you post this as a project somewhere?
// add models
add: function(models, options) {
// TYPO: next line was missing, so single models not handled.
models = _.isArray(models) ? models.slice() : [models];
var self = this;
models = _.filter(models, this.filter);
// return if no models exist
// TYPO: returned undefined, so was not chainable
if(models.length == 0) { return this; }
// actually add the models to the superset
this.superset.add(models, options);
return this;
},
// remove models
remove: function(models, options) {
// TYPO: next line was missing, so single models not handled.
models = _.isArray(models) ? models.slice() : [models];
// remove model from superset
this.superset.remove(_.filter(_.filter(models, function(cm) {
// TYPO: not 'm != null', causes error to be thrown
return cm != null;
}), this.filter), options);
// TYPO: missing return so not chainable
return this;
},
one quick amendment to you solution, you are using
$(this.el).html('');
My understanding is your the views and related event bindings will still exist in the browser memory, so you ideally need to use view.remove() on the TaskView to correctly clear the event bindings as well as the html.
This is a slightly different take on the answer as I have been looking for a solution to a similar problem, hope this may be of help to others.
My problem: - to filter a complete collection by attributes of the model. eg. a user clicks the models view, gets a list of (some of) the attributes, selecting an attribute filters the collection to only show ones with the same value.
The route I am taking is by calling a method on the collection from the view, in my case the view is specific to a model so:
this.model.collection.myFilter(attr,val);
where attr is an attribute of the model associated with the collection, then in the filter something like
myFilter: function(attr, val){
var groupByAttr = this.groupBy(function(article){
var res = (val === undefined)? true : (article.get(attr) == val);
article.set({selected:res});
return res;
});
return groupByAttr;
}
I have used ._groupBy as this returns 2 arrays (positive / negative) that may be of use. By setting the mode attribute "selected", and binding to this in the model view I can easily toggle a class which shows or hides the view.
if(val === undefined) is added as a simple way of clearing a filter by calling the same method without a value.

Resources