Backbone collection always with n models - backbone.js

I'm trying make a list with last ten models or less on collection. I have events to add models dynamically with create. This event calls 'add' on collection and with my logic this add correctly one element. But I need add new element, check if collection has more than 10, if it's true remove last model and add new
var model = Backbone.Model.extend({
defaults: function() {
return {id:null}
}
});
var collection = Backbone.Collection.extend({
model:model
});
var view = Backbone.View.extend({
initialize: function(){
var self = this;
this.listenTo(collection, 'add', this.addOne);
this.listenTo(collection, 'reset', this.addAll);
this.listenTo(collection, 'all', this.render);
},
render: function(){
this.$el.html();
return this;
},
addAll: function(){
this.collection.each(this.addOne, this);
},
addOne: function(model){
//this is executed after 'create' but before this I need slice my collection
var view = new view({model:model});
this.$el.prepend(view.render().el);
}
});
thanks

Bind a function in initialize of your Backbone.Collection.extend on the add event in which you're checking the size.
initialize: function() {
this.on('add', function() {
this.checkSize();
});
},
checkSize: function() {
var max = 10;
if (this.length > max) {
this.reset(this.first(max));
}
}

Yoou can do like that:
addOne: function(model){
//this is executed after 'create' but before this I need slice my collection
if !model.get('new_field')
model.set({new_field, ""})
newField = new Date();
while (this.collection.findWhere({new_field:newField})){
newField = new Date();
}
model.set({new_field, newField});
var view = new view({model:model});
this.$el.prepend(view.render().el);
this.checkLength()
},
checkLength: function(){
if (this.collection.length > 10) {
// remove model which gets minimum value by 'new_field'
}
}

Related

Backbone.js collection view render multiple times for single trigger

I have backbone.js collection and collectionview. collection view listening to its collection add event. But when I add new models to it's collection it renders mutiple times for each model.
Please Check the JSFiddle
var ImageCollectioView = Backbone.View.extend({
initialize: function() {
this.collection.bind('add', this.render, this);
},
collection: imgColection,
el: '#cont',
render: function() {
var els = [], self = this;
this.collection.each(function(image){
var imageView = new ImageView({model: image});
self.$el.append(imageView.render().el);
});
return this;
}
});
Your render method renders the entire collection. So after adding a model you should clear the existing item views:
render: function() {
var els = [], self = this;
this.$el.empty();
//------^---- clear existing
this.collection.each(function(image){
var imageView = new ImageView({model: image});
self.$el.append(imageView.render().el);
});
return this;
}
That being said, it's better to add a separate method that just appends single item view rather than rendering the entire collection:
var ImageCollectioView = Backbone.View.extend({
initialize: function() {
this.render();
this.listenTo(this.collection, 'add', this.renderItem);
},
el: '#cont',
render: function() {
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(image) {
var imageView = new ImageView({
model: image
});
this.$el.append(imageView.el);
}
});
Updated Fiddle

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 iterating through JSON results from fetch()

I am very new to Backbone and just trying to figure out the way things work. How do I iterate through the "fetched" results?
(function($) {
console.log('Formcontainer init.');
var Field = Backbone.Model.extend({
defaults: {
id: "0",
memberlist_id: "3",
field_name: "sample-field-name",
},
initialize: function() {
console.log('Field model init.');
}
});
var FieldCollection = Backbone.Collection.extend({
defaults: {
model: Field
},
model: Field,
url: 'http://localhost:8888/getstuff/3.json',
initialize: function() {
console.log('FieldCollection init.');
}
});
var ListView = Backbone.View.extend({
el: '#app-container',
initialize: function() {
_.bindAll(this,"render");
console.log('ListView init.')
this.counter = 0;
this.collection = new FieldCollection();
this.collection.fetch();
//this.collection.bind('reset', function() { console.log('xxx')});
//this.collection.fetch();
this.render();
},
events: {
'click #add': 'addItem'
},
render: function() {
var self = this;
console.log('Render called.');
},
addItem: function() {
this.counter++;
this.$("ul").append("<li>hello world" + this.counter + "</li>");
}
});
var listView = new ListView();
})(jQuery);
I get this in my Firebug console:
Formcontainer init.
ListView init.
FieldCollection init.
GET http://localhost:8888/getstuff/3.json 200 OK
Render called.
Field model init.
Field model init.
Field model init.
Field model init.
Field model init.
It seems that the fetch() was called - since "Field model init." is in the console 5 times. But how to I output that? I would want to append the items in an unordered list.
Thanks!
Bind the reset event to your render function...
this.collection.bind('reset', this.render, this);
This gets triggered after the fetch is complete, so you can create the list...
render: function() {
var self = this;
console.log('Render called.');
this.collection.each(function(i,item) {
this.$el.append("<ul>" + item.get("field_name") + "</ul>");
});
},

Backbone.js reinstantiating collection does not update corresponding view

Here is my Model View and Collection :
window.Report = Backbone.Model.extend({});
window.ReportCollection = Backbone.Collection.extend({
model: Report,
initialize: function(properties){
this.url = properties.url;
}
});
window.ReportCollectionView = Backbone.View.extend({
initialize: function(){
this.collection.reset();
this.render();
},
render: function(){
var self = this;
this.collection.fetch({
success: function(){
self.collection.each(function(model){
//pass model to subview
});
}
}
});
}
});
in the other part of the code I use the instantiate the above objects
var reportCollection = new ReportCollection({url:someURL});
var reportCollectionView = new ReportCollectionView({collection:reportCollection});
'someURL' is a REST based URL that returns JSON list of Objects
So far everything looks good. What I am trying to achieve is:
I must be able to refresh the 'reportCollection' by changing the url and this should trigger an updated 'reportCollectionView'. Thanks for any pointers
I suppose you could add a method to your collection which changes url and forces a fetch:
window.ReportCollection = Backbone.Collection.extend({
//...
changeUrl: function(url) {
this.url = url;
this.fetch();
}
});
and then bind to the "reset" event in your view:
window.ReportCollectionView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
this.collection.on('reset', this.render);
this.collection.reset();
},
//...
});
Then if you do this:
c = new ReportCollection(...);
v = new ReportCollectionView({ collection: c, ... });
You'll get your rendered view and then later you can:
c.changeUrl(...);
to set the new URL and that will trigger a render call on v.

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