Backbone sorting procedure. UI vs Data - backbone.js

My question is about the proper way to show a list of records using backbone. Lets say you have a person model that you want to display to the user and allow them to sort by first name, last name, id....
The first instinct is to just have the view catch the event and re-render based on the users sort option. The problem with this method is that it is U.I. driven and not data driven.
Second thoughts are to set the sorting attributes in the model since the collection does not contain attributes (though that seems would be the best option). This method is at least data driven by setting the sorting attributes but is horribly redundant and if the sorting attributes are not stripped out on save they are sent to the server | local or...
Last thought is probably the correct one. Create a second model that would be a control model used contain sorting/displaying properties. My issues with this method is events and models can get very unruly. If you expand beyond just a person model and make this a fairly large app you have a LOT of models and events and gets hard to manage. The model-1 view has to catch the initial event, then have the collection trigger a custom event then the second model has to catch the custom event and render it.
Sorry for the long post, I am fairly new to backbone js and want to make sure I have the best practice grasp. Thanks in advance for the help. I hope I am at least on the correct track.

I just implemented this last night.
You can set a new collection comparator and then use the sort method on the collection. sort will fire a reset event, which you can use in your view to re-render the list.
Here is my view, which contains a select box that allows the user to choose how to sort the data:
App.HouseListView = Backbone.View.extend({
el: '.house-list',
initialize: function() {
App.houseCollection.bind('reset', this.populateList, this);
},
events: {
'change .sort':'sort',
},
populateList: function(collection) {
this.$('ul').html('');
_.each(collection.models, function(model) {
var view = new App.HouseListElemView({model:model});
this.$('ul').append(view.el);
});
},
sort: function(e) {
var sort_by = $(e.srcElement.selectedOptions[0]).attr('data-sort-by');
App.houseCollection.comparator = function(house) {
return house.get(sort_by);
}
App.houseCollection.sort();
},
});
Hope this helps
EDIT: Implemented #mu is too short's suggestion:
App.Houses = Backbone.Collection.extend({
model: App.House,
url: API_URL,
_sort_by: 'price',
sort_houses_by: function(sort_by) {
this._sort_by = sort_by;
this.sort();
},
comparator: function(house) {
return house.get(this._sort_by);
},
});
App.houseCollection = new App.Houses();
App.HouseListView = Backbone.View.extend({
el: '.house-list',
initialize: function() {
App.houseCollection.bind('reset', this.populateList, this);
},
events: {
'change .sort':'sort',
},
populateList: function(collection) {
this.$('ul').html('');
_.each(collection.models, function(model) {
var view = new App.HouseListElemView({model:model});
this.$('ul').append(view.el);
});
},
sort: function(e) {
var sort_by = $(e.srcElement.selectedOptions[0]).data('sort-by');
App.houseCollection.sort_houses_by(sort_by);
},
});

Related

BackboneJs - Retain events on a collection inside a model when model changes

i've a Collection inside a Model as illustrated below:
var itemModel = Backbone.Model.extend({
defaults:{
name:"",
brand:"",
priceCollection:[]
}
})
There are change listeners attached to the itemModel and also change listeners attached to collection as
this.listenTo(itemModel.get('priceCollection'),'change',this.dosomething) in a view.
The problem is that the change listeners on the collection work fine as long as the parent model hasn't changed , if the model is given a set of new attributes via itemModel.set(newattributes) the event bound on itemModel.get('priceCollection') is lost.
How do i retain this event? or should i rebind this event everytime the Model is change? or Should i move the listener on the collection from the view to the Model and trigger a custom Backbone event?
It should be noted that this model is singleton
Keep in mind that Backbone assumes a Collection and Model should be mapped 1:1 to a server side resource. It makes clear assumtions on the API layout and data structures - refer to Model.url, Model.urlRoot and Collection.url.
Proposal
You said the model is a singleton. In this case I'd suggest to maintain the model and collection separately.
Since a SomeModel is not accompanied by a certain collection SomeCollection which have a tight relationship it's not necessary to relate them on an attribute level. The effort needed to establish event listeners and sync the data is only at one place.
// some controller (app main)
var model = new SomeSingletonModel();
var collection = new SomeSingletonCollection();
var view = new SomeView({
model: model,
collection: collection
});
Probably the resource that is mapped to SomeSingletonModel will deliver an array.
What are the benefits of using a collection as model attribute (that's what model.get("name") is) over using a plain array? Syncing and change events. Both are probably only necessary when a View updates the Collection's Models. When the View only renders, a Collection does not provide any benefit in many cases.
If the data of that array needs to be updated, using a Collection is probably the right choice because of Backbone's synching mechanisms.
But how to keep the collection sync with the model (you ask)?
Your controller needs to listen to the model and update the collection on sync and reset:
model.on("sync reset", function() {
// "priceCollection" is a model attribute
collection.reset(model.get("priceCollection"));
// optionally unset "priceCollection" on the model
this.unset("priceCollection", { silent: true });
});
This will initialize the collection.
Any change to the Collection's Models will then only be part of the Collection's or Model's syncing mechanisms.
Foreword
Also see my other answer which is probably the better choice when the model is a singleton.
Note the very first statement on Backbone's assumptions on the API design on that answer.
Proposals using a coupling between Model and Collection
Note: if necessary, in all these implementations the Collection's url or Model's (the Collection's Model) url/rootUrl may get (re-)defined upon sync to control the syncing.
Update internal reference on change/sync/reset
This implementation removes the model attribute and updates an object attribute with its data.
The object attribute is one Collection instance that is only reset, not recreated, upon model change.
var CustomModel = Backbone.Model.extend({
defaults: {
// defaults go here - "children" may be contained here
},
// implement constructor to act before the parent constructor is able to
// call set() (L402 in v1.3.0) with the initial values
// See https://github.com/jashkenas/backbone/blob/1.3.0/backbone.js#L402
constructor: function() {
// create children collection as object attribute - replaces model attr.
this.children = new Backbone.Collection();
// listen to changing events to catch away that attribute an update the
// object attribute
this.listenTo(this, "change:children sync reset", this.onChangeColl);
// apply original constructor
Backbone.Model.apply(this, arguments);
},
onChangeColl: function() {
// check for presence since syncing will trigger "set" and then "sync",
// the latter would then empty the collection again after it has been updated
if (this.has("children")) {
// update "children" on syncing/resetting - this will trigger "reset"
this.children.reset(this.get("children"));
// remove implicitly created model attribute
// use silent to prevent endless loop due to change upon change event
this.unset("children", { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({a: 8, children: [{ x: 50 }, { x: 89 }]});
c.url = "/dummy"
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
listening to collection events is easier to implement since there's one instance through model lifetime
Contra
code is more complex
collection data is not part syncing without further implementations
Replace on change/sync/reset
This implementation intercepts model attribute changes and replaces its data with a Collection instance that has been initialized (reset) with the raw data.
var CustomModel = Backbone.Model.extend({
defaults: {
// this is optional
children: new Backbone.Collection()
},
initialize: function() {
// listen to model attribute changing events to swap the raw data with a
// collection instance
this.listenTo(this, "change:children sync reset", this.onChangeColl);
},
onChangeColl: function() {
if (this.has("children")) {
// use silent to prevent endless loop due to change upon change event
this.set("children", new Backbone.Collection(this.get("children")), { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({ a: 8, children: [{ x: 50 }, { x: 89 }] });
c.url = "/dummy";
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
straightforward implementation
Contra
data included in sync, excluding it takes more effort
listening to the Collection impractical: since all consumers would need to unbind/bind
Note: depending on your requirements and API design you may not want children being synced to the server automatically. In this case this solution is limited. You could overwrite toJSON() of the Model but this may limit its usage for other parts of the application (like feeding the data into a view).
Inverse relation: Collection has a Model
Maybe your primary data is actually the Collection. So decorating a Collection with additional data is another approach. This implementation provides one model along the Collection that will be updated upon collection sync.
This implementation is only best suited for fetch of collection data along with attributes (e.g. fetching directory contents with attributes of the directory itself).
var CustomCollection = Backbone.Collection.extend({
initialize: function() {
// maintain decorative attributes of this collection
this.attrs = new Backbone.Model();
},
parse: function(data, opts) {
// remove "children" before setting the remainder to the Model
this.attrs.set(_.omit(data, "children"));
// return the collection content only
return data.children;
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomCollection({ a: 1, b: 2, children: [{ x: 2 }, { x: 3 }] }, { parse: true });
c.reset({ a: 9, b: 11, children: [{ x: 5 }, { x: 10 }] } , { parse: true });
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, b: 124, children: [{ x: 42 }, { x: 47 }] }) } }
c.fetch();
Pro
straight forward implementation
delegating model events is easier
Contra
collection data is not part syncing without further implementations
requires parse() to be implemented which
-- in turn requires parse: true to always be passed to reset() and set() and
-- requires parse() to be called with the collection as scope (this) (this could be circumvented by defining parse within initialize bound to this using `bind()ยด)

Collection fetch returns one model but response has all models

I'm new to Backbone and on fetching a collection, I can see the server return all 15 collections. The fetch success returns all 15 models in the response object but the collection object has only the last of the 15 models.
var BracketModel = Backbone.Model.extend({
defaults: {
id: '',
name: '',
title: ''
},
urlRoot: 'http://test.com/bracket/rest.php',
.....
}),
var BracketsCollection = Backbone.Collection.extend({
url: 'http://test.com/bracket/rest.php?op=list',
model: BracketModel,
}),
bracketCollection.fetch({
success: function (collection, response) {
// Collection.models only has one model, response has 15
var bracketsView = new BracketsView({collection: collection});
},
Try
var bracketsView = new BracketsView({collection: response});
Or
var bracketsView = new BracketsView({collection: collection.toJSON()});
I haven't tested it now, but if I remember well, both are equivalent.
The first parameter returns the collection object, which gives you access to different collection attributes. The second parameter returns 'an array containing the attributes hash of each model in the collection', which is likely the thing you are looking for.
The pattern that I usually go with for passing a collection to a view goes like this:
var bracketCollection = new BracketsCollection();
var view = new brackatsView({collection: bracketCollection});
brackCollection.fetch();
Then inside of your view's initialization method do this:
this.listenTo(this.collection, 'sync', this.render);
What this all is doing is creating your collection and your view, and then when you create the view you are telling it about the collection. Calling fetch on the collection is an asynchronous event that will fire a 'sync' even when it is done. The view will listen for this sync event, and when it happens will call the render function.

Copying events to new instance

I have a collection of Tasks. I have added some filter methods to the collection declaration to return a subset of the collection in a new instance. What I want to do is create a model in the original collection and have the add (and any other) event filter down to any new instances of the Tasks collection so my view partials can update accordingly.
Here is a JSBin demonstrating my problem. Notice that when I add a new model to the original collection on the last line the list doesn't update because, obviously, the filter methods return new instances of that collection (which seems clean to me) therefore any listeners aren't fired - the view is using a different collection to the one that has had an item added to it.
How can I filter a collection semantically and cleanly, but keep any events bound to the set being filtered bound on the subset of models returned?
For example, the add event for notAsManyTasks isn't fired for the below code (the alert() never shows):
// Task model
var Task = Backbone.Model.extend();
// Tasks collection
var TasksCollection = Backbone.Collection.extend({
model: Task,
byProject: function(projectId) {
var matches = this.filter(function(task) {
return task.get('projectId') == projectId;
});
return new TasksCollection(matches);
},
complete: function(state) {
return new TasksCollection(this.where({ complete: state }));
}
});
// Example collection
var lotsOfTasks = new TasksCollection([
{ id: 1, projectId: 1, complete: false },
{ id: 2, projectId: 1, complete: true },
{ id: 3, projectId: 2, complete: false },
{ id: 4, projectId: 2, complete: true }
]);
var notAsManyTasks = lotsOfTasks.byProject(1);
notAsManyTasks.on('add', function() {
alert("Added");
});
// Does not fire `add` event on `notAsManyTasks` which is my problem, however
// it _does_ fire on `lotsOfTasks`, as it should
lotsOfTasks.add({ id: 5, projectId: 1, complete: false });
How can I get round this problem? Ideally I don't want to store the collection's original state, return a subset, then restore all models again.
Without doing some complicated things, you should be able to do something along those lines:
mySubset.listenTo(myCollection, 'all', function() {
this.trigger.apply(this, arguments);
});
You listen to all the events of your collection, and echo them with your subset. Though I guess that's only half a solution because your subset wouldn't be updated and you'd do the filter in the listener for all your subsets.
Another solution to do the job only once would be for your collection to listen to its own add event, filter the new model, and trigger a custom event so that only the subset(s) that actually need(s) to do something do something.

backbone.js a url property must be defined

I'm trying to use Backbone.localStorage as a backend to an app.
I wrote a Model called Era, and a corresponding EraView. Pressing enter in the EraView causes the model to be saved, and this is where I get the error:
Uncaught Error: A "url" property or function must be specified
urlError backbone.js:1509
_.extend.url backbone.js:515
_.result underscore-min.js:1060
Backbone.sync backbone.js:1410
Backbone.sync backbone.localStorage.js:188
_.extend.sync backbone.js:276
_.extend.save backbone.js:476
karass.EraView.Backbone.View.extend.close era.js:61
karass.EraView.Backbone.View.extend.updateOnEnter era.js:75
Here is the code to the EraView
var karass = karass || {};
// Era Item View
// the DOM element for an era item
karass.EraView = Backbone.View.extend({
tagName: 'li',
className: 'era',
template: _.template( $('#era-template').html() ),
// The DOM events specified to an item
events: {
'dblclick .edit-input': 'edit',
'keypress .edit-input': 'updateOnEnter',
//'blur .edit': 'close',
},
// The EraView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between an era and a EraView in this karass,
// we set a direct reference on the model for convenience.
initialize: function(){
_.bindAll(this);
this.model.on('change', this.render, this);
},
// Re-renders the era item to the current state of the model and
// updates the reference to the era's edit input within the view
render: function(){
this.$el.html( this.template(this.model.attributes));
this.$era_start = this.$('.era-start');
this.$era_end = this.$('.era-end');
this.$era_start.attr('disabled', true);
this.$era_end.attr('disabled', true);
return this;
},
// Switch this view into editing mode, displaying the input field
edit: function(){
this.$('.edit-input').removeAttr('disabled');
this.$el.addClass('editing');
this.$('.edit-input').addClass('editing');
},
// Close the editing mode, saving changes to the era
close: function(){
this.$('.edit-input').attr('disabled', true);
var start = this.$era_start.val().trim();
var end = this.$era_end.val().trim();
if(start && end){
this.model.save({from: start, until: end});
}
this.$el.removeClass('editing');
this.$('.edit-input').removeClass('editing');
this.trigger('close');
},
updateOnEnter: function(e){
if(e.which !== ENTER_KEY && (!this.$era_start.val().trim() || !this.$era_end.val().trim())){
return;
}
this.close();
}
});
And this is the code for the era model:
var karass = karass || {};
karass.Era = Backbone.Model.extend({
defaults: {
from: '',
until: ''
},
});
I thought I didn't need a url while using localStorage.
Edit: I forgot to mention that while this behavior occurs in the Era/EraView itself, it also occurs in the Name model, which extends Era. Name in turn belongs in a Names collection. I don't know if this makes a difference, but I figured I add it.
Edit 2: The Names collection looks like this:
karass.Names = History.extend({
model: karass.Name,
localStorage: new Backbone.LocalStorage('karass-names'),
});
Edit 3: I posted all the code on jsfiddle: http://jsfiddle.net/herrturtur/CRv6h/
You don't need an url while using localStorage. But you need to set the localStorage property on your model or on your collection (if you set the localStorage on a collection the models inside the collection will "inherit" this setting):
karass.Era = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("EraCollection"),
// the EraCollection should be an unique name within your app.
defaults: {
from: '',
until: ''
},
});
If you don't setup the localStorage property the plugin falls back to the default ajax sync so you get the uri missing exception.

How to store a reference to a Collection in a Model in Backbone.js?

When creating new collection (Choices) I want to set a property on it (ex: _question) which links back to the containing Model (MultipleChoiceQuestion)
This took me quite a bit of time to figure out, so in case somebody in the future has this problems...here's the code I ended up writing.
I discovered, unlike Model, Collection's initialize() function accepts 2 parameters. The first is models (which is a list of models you can initialize the collection with). The second is options (what you want). For a while my Collection started out with 1 model inside and I couldn't figure out why. Turns out I was passing my options into the models field.
THE CONTAINING MODEL:
m.MultipleChoiceQuestion = Backbone.Model.extend({
initialize: function(){
//NULL as first parameter because we don't want models
this.choices = new c.Choices(null, {
_question: this //this is referring to current question
}); //choices Collection is this
}
});
THE COLLECTION
c.Choices = Backbone.Collection.extend({
initialize: function(models, options){
this._question = options._question;
},
model: m.Choice
});
I actually found that although my 1st answer technically works, there's a plugin that makes care of storing collections in models (and creating appropriate One->Many, One->One and Many->One relationships
https://github.com/PaulUithol/Backbone-relational
Using that plugin you store the parent question as an ATTRIBUTE
m.MultipleChoiceQuestion = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'choices', //says to store the collection in the choices attribute
relatedModel: m.Choice, //knows we are talking about the choices models
includeInJSON: true, //when we do toJSON() on the Question we want to show serialize the choices fully
reverseRelation: {
key: 'question', //in the CHOICE object there will be an attribute called question which contains a reference to the question
includeInJSON: false //when we do .toJSON on a CHOICE we don't want to show the question object (or maybe we want to show the ID property in which case we set it to "id")
}
}],
coolFunction: function () {
this.get('choices').each(function(choice){
choice.doSomethingChoicey();
});
}
});
So now if we are in the choices model we can fully reference anything in the parent question:
m.Choice = m.RelationalModel.extend({
coolFunction: function(){
this.get('question').doSomemethingQuestiony();
var question_numer = this.get('question').get('question_number');
}
});

Resources