getting next and previous model from a collection - backbone.js

I've seen a few different ways to get the next or previous model from a collection, but was wondering if anyone could offer some advice on the way I decided to implement it. My collection is ordered, but the id that i'm sorting on is not guaranteed to be sequential. It's only guaranteed to be unique. Assume that smaller ids are "older" entries to the collection and larger ids are "newer".
MyCollection = Backbone.Collection.extend({
model: MyModel,
initialize:function (){
this.getElement = this._getElement(0);
},
comparator: function(model) {
return model.get("id");
},
_getElement: function (index){
var self = this;
return function (what){
if (what === "next"){
if (index+1 >= self.length) return null;
return self.at(++index);
}
if (what === "prev"){
if (index-1 < 0 ) return null;
return self.at(--index);
}
// what doesn't equal anything useful
return null;
};
}
});
When using getElement, I do things like getElement("next") and getElement("prev") to ask for the next or previous model in my collection. What is returned from getElement is the actual model, not the index. I know about collection.indexOf, but I wanted a way to loop through a collection without first having a model to start from. Is this implementation harder than it needs to be?

I would do something like this. Keep in mind that there isn't any error handling currently so if you are currently at the first model in the collection and try to get the previous you will probably get an error.
MyCollection = Backbone.Collection.extend({
model: MyModel,
initialize:function (){
this.bindAll(this);
this.setElement(this.at(0));
},
comparator: function(model) {
return model.get("id");
},
getElement: function() {
return this.currentElement;
},
setElement: function(model) {
this.currentElement = model;
},
next: function (){
this.setElement(this.at(this.indexOf(this.getElement()) + 1));
return this;
},
prev: function() {
this.setElement(this.at(this.indexOf(this.getElement()) - 1));
return this;
}
});
To progress to the next model collection.next(). To progress to the next model and return it var m = collection.next().getElement();
To explain a little better how next/prev works.
// The current model
this.getElement();
// Index of the current model in the collection
this.indexOf(this.getElement())
// Get the model either one before or one after where the current model is in the collection
this.at(this.indexOf(this.getElement()) + 1)
// Set the new model as the current model
this.setElement(this.at(this.indexOf(this.getElement()) + 1));

I've done this slightly differently in that I'm adding the methods to the model rather than to the collection. That way, I can grab any model, and get the next one in the sequence.
next: function () {
if (this.collection) {
return this.collection.at(this.collection.indexOf(this) + 1);
}
},
prev: function () {
if (this.collection) {
return this.collection.at(this.collection.indexOf(this) - 1);
}
},

Bumping this old thread with a somewhat more generic solution:
Stuff to add to Collection.prototype
current: null,
initialize: function(){
this.setCurrent(0);
// whatever else you want to do here...
},
setCurrent: function(index){
// ensure the requested index exists
if ( index > -1 && index < this.size() )
this.current = this.at(index);
else
// handle error...
},
// unnecessary, but if you want sugar...
prev: function() {
this.setCurrent(this.at(this.current) -1);
},
next: function() {
this.setCurrent(this.at(this.current) +1);
}
you can then use the sugar methods to get the prev/next model like so...
collection.prev();
collection.next();

My Backbone SelectableCollection class:
Backbone.Collection.extend({
selectNext: function () {
if(this.cursor < this.length - 1) {
this.cursor++;
this.selected = this.at(this.cursor);
this.trigger('selected', this.selected);
}
},
selectPrevious: function () {
if(this.cursor > 0) {
this.cursor--;
this.selected = this.at(this.cursor);
this.trigger('selected', this.selected);
}
},
selectById: function (id) {
this.selected = this.get(id);
this.cursor = this.indexOf(this.selected);
this.trigger('selected', this.selected);
},
unselect: function () {
this.cursor = null;
this.selected = null;
this.trigger('selected', null);
}
});

Related

Qooxdoo Remote table getRowCount() return 0

qx.Class.define("webApp.backendjs.tables.RegionesModel", {
extend: qx.ui.table.model.Remote,
members: {
_loadRowCount: function () {
var params = {};
params.action = "getCount";
var rpc = new qx.io.remote.Rpc("http://qx.alpali.cl/svc/svc.php");
rpc.setProtocol("2.0");
rpc.setCrossDomain(true);
rpc.callAsync(qx.lang.Function.bind(this._onRowCountCompleted, this), "regiones.regiones.getNominaRegiones", params);
},
_onRowCountCompleted: function (result, exc) {
if (result !== null) {
this._onRowCountLoaded(result.count);
}
},
_loadRowData: function (firstRow, lastRow) {
var params = {};
params.action = "getData";
var rpc = new qx.io.remote.Rpc("http://qx.alpali.cl/svc/svc.php");
rpc.setProtocol("2.0");
rpc.setCrossDomain(true);
rpc.callAsync(qx.lang.Function.bind(this._onLoadRowDataCompleted, this), "regiones.regiones.getNominaRegiones", params);
},
_onLoadRowDataCompleted: function (result, exc) {
if (result !== null) {
this._onRowDataLoaded(result);
}
}
}
});
var RTRegionesModel = new webApp.backendjs.tables.RegionesModel();
RTRegionesModel.setColumns(["ID", "Cè´¸digo", "Nombre"], ["id", "region_id", "region_nombre"]);
var TableRegiones = new qx.ui.table.Table(RTRegionesModel);
TableRegiones.setTableModel(RTRegionesModel);
// THIS don't work, return 0
TableRegiones.addListener('appear', function () {
console.log("RTRegionesModel.getRowCount(): %s", RTRegionesModel.getRowCount());
}, RTRegionesModel);
// THIS don't work, return 0
TableRegiones.addListener('appear', function () {
console.log("RTRegionesModel.getRowCount(): %s", RTRegionesModel.getRowCount());
}, this);
this.getRoot().add(TableRegiones);
var button1 = new qx.ui.form.Button("How many record...", "icon/22/apps/internet-web-browser.png");
this.getRoot().add(button1,{right:50,top:50});
// this is ok, return teh value
button1.addListener("execute", function(e) {
console.log("RTRegionesModel.getRowCount(): %s", RTRegionesModel.getRowCount());
});
url for testing playground
i need the valor when remote table is loaded
what is the problem..???
thank.
PD: sorry for my bad and ugly english, my native language is spanish (chile), my best friend in this moment is googol
At the time that you are looking for the row count with your "THIS don't work" comment, the row count is not yet available because the network operation to retrieve the row count from the server has not yet been issued.
You probably want to be listening for the model's dataChanged event which is fired when a row count is loaded, or when the model data changes, such as this:
TableRegiones.getTableModel().addListener(
'dataChanged',
function ()
{
console.log(
"dataChanged: RTRegionesModel.getRowCount(): %s",
RTRegionesModel.getRowCount());
},
RTRegionesModel);

Angular 1.5, cloning scope to keep track of changing

I'm trying to set a dictionary of changes in order to track any modifcation to the scope when I execute the take snapshot function, however, for some reason, it starts working at the third try, other cases, my current value is sync with the previous, any idea why?
My saving factory looks like :
.factory('AuthoringState', function() {
var track = 0;
var _pool = {};
return {
addChange : function(data) {
track++;
var temp = _.cloneDeep(data);
_pool[track] = temp;
temp['track'] = track;
return _pool[track];
},
undo : function(checklist) {
if(checklist['track'] === 1) {
return checklist;
}
return _pool[checklist['track'] - 1];
},
back : function(checklist) {
if(checklist['track'] === track) {
return checklist;
}
return _pool[checklist['track'] + 1];
}
}
})
and my controller like this:
.controller('Sample', function($scope, AuthoringState) {
var tasks = {
tasks:[
{value: 1, text: 'I\'m a task'},
{value: 2, text: 'I\'m another task'}]
};
var vm = this;
/**Init**/
vm.checklist = new CheckList(tasks);
vm.checklist = AuthoringState.addChange(vm.checklist);
/** Methods**/
$scope.snapshoot = function() {
vm.checklist = AuthoringState.addChange(vm.checklist);
}
$scope.undo = function() {
vm.checklist = AuthoringState.undo(vm.checklist);
}
$scope.back = function() {
vm.checklist = AuthoringState.back(vm.checklist);
}
$scope.check = function() {
console.log($scope.checklist);
}
});
Here is a jsbin to see it working.
By returning the snapshot objects themselves from your service and binding them to the template, you are allowing the user's changes to modify the snapshots. It sounds like you want them to be immutable instead, so a snapshot doesn't change after it's saved.
Instead, clone the snapshots when you restore them and return the clones, leaving the snapshots untouchable:
undo : function(checklist) {
if(checklist.track === 1) {
return checklist;
}
return _.cloneDeep(_pool[checklist.track - 1]);
},
back : function(checklist) {
if(checklist.track === track) {
return checklist;
}
return _.cloneDeep(_pool[checklist.track + 1]);
}
Also, when saving a new snapshot, don't replace the current checklist object on the scope with the snapshot from the pool (which makes the snapshot itself editable), all you need to do is update its track property:
addChange : function(data) {
track++;
var temp = _.cloneDeep(data);
_pool[track] = temp;
temp.track = track;
data.track = track;
},
And in the controller:
vm.checklist = new CheckList(tasks);
AuthoringState.addChange(vm.checklist);
$scope.snapshoot = function() {
AuthoringState.addChange(vm.checklist);
}
Combining these changes, we get this working version: http://jsbin.com/juwimepofi/1/edit?js,output

Nodejs async data duplication

I'm having some problems with one async process on nodejs.
I'm getting some data from a remote JSON and adding it in my array, this JSON have some duplicated values, and I need check if it already exists on my array before add it to avoid data duplication.
My problem is when I start the loop between the JSON values, the loop call the next value before the latest one be process be finished, so, my array is filled with duplicated data instead of maintain only one item per type.
Look my current code:
BookRegistration.prototype.process_new_books_list = function(data, callback) {
var i = 0,
self = this;
_.each(data, function(book) {
i++;
console.log('\n\n ------------------------------------------------------------ \n\n');
console.log('BOOK: ' + book.volumeInfo.title);
self.process_author(book, function() { console.log('in author'); });
console.log('\n\n ------------------------------------------------------------');
if(i == data.length) callback();
})
}
BookRegistration.prototype.process_author = function(book, callback) {
if(book.volumeInfo.authors) {
var author = { name: book.volumeInfo.authors[0].toLowerCase() };
if(!this.in_array(this.authors, author)) {
this.authors.push(author);
callback();
}
}
}
BookRegistration.prototype.in_array = function(list, obj) {
for(i in list) { if(list[i] === obj) return true; }
return false;
}
The result is:
[{name: author1 }, {name: author2}, {name: author1}]
And I need:
[{name: author1 }, {name: author2}]
UPDATED:
The solution suggested by #Zub works fine with arrays, but not with sequelize and mysql database.
When I try to save my authors list on the database, the data is duplicated, because the system started to save another array element before finish to save the last one.
What is the correct pattern on this case?
My code using database is:
BookRegistration.prototype.process_author = function(book, callback) {
if(book.volumeInfo.authors) {
var author = { name: book.volumeInfo.authors[0].toLowerCase() };
var self = this;
models.Author.count({ where: { name: book.volumeInfo.authors[0].toLowerCase() }}).success(function(count) {
if(count < 1) {
models.Author.create(author).success(function(author) {
console.log('SALVANDO AUTHOR');
self.process_publisher({ book:book, author:author }, callback);
});
} else {
models.Author.find({where: { name: book.volumeInfo.authors[0].toLowerCase() }}).success(function(author) {
console.log('FIND AUTHOR');
self.process_publisher({ book:book, author:author }, callback);
});
}
});
// if(!this.in_array(this.authors, 'name', author)) {
// this.authors.push(author);
// console.log('AQUI NO AUTHOR');
// this.process_publisher(book, callback);
// }
}
}
How can I avoid data duplication in an async process?
This is because you are comparing different objects and result is always false.
Just for experiment type in the console:
var obj1 = {a:1};
var obj2 = {a:1};
obj1 == obj2; //false
When comparing objects (as well as arrays) it only results true when obj1 links to obj2:
var obj1 = {a:1};
var obj2 = obj1;
obj1 == obj2; //true
Since you create new author objects in each process_author call you always get false when comparing.
In your case the solution would be to compare name property for each book:
BookRegistration.prototype.in_array = function(list, obj) {
for(i in list) { if(list[i].name === obj.name) return true; }
return false;
}
EDIT (related to your comment question):
I would rewrite process_new_books_list method as follows:
BookRegistration.prototype.process_new_books_list = function(data, callback) {
var i = 0,
self = this;
(function nextBook() {
var book = data[i];
if (!book) {
callback();
return;
}
self.process_author(book, function() {
i++;
nextBook();
});
})();
}
In this case next process_author is being called not immediately (like with _.each), but after callback is executed, so you have consequence in your program.
Not sure is this works though.
Sorry for my English, I'm not a native English speaker

Fetch a Backbone.Collection made up of other collections

So I have a few types of data:
Post
Project
Event
And each of those data models have their own collection and a route to view them:
/posts => app.postsCollection
/projects => app.projectsCollection
/events => app.eventsCollection
Now I want to add another route:
/ => app.everythingCollection
How can I create a collection which displays an aggregate of the other three collections, but without fetching all the post project and event data again?
Similarly, calling everythingCollection.fetch() would fill the postsCollection, projectsCollection and eventsCollection so that their data was available when they were rendered independently.
The whole point being never to download the same data twice.
Your app.everythingCollection doesn't have to be a really backbone collection. All it needs is just access and fetch to other collections.
You can inherit the Backbone.Events to gain all the events facilities also.
var fetchedRecords = {posts: 0, projects: 0, events: 0};
var Everything = function () {}
_.extend(Everything.prototype, Backbone.Events, {
fetch: function (option) {
that = this;
this.count = 0;
option.success = function () {that.doneFetch(arguments)};
if (fetchRecords.posts == 0) {
option.fetchedName = "posts";
app.postsCollection.fetch(option);
this.count ++;
}
if (fetchRecords.projects == 0) {
option.fetchedName = "projects";
app.projectsCollection.fetch(option);
this.count ++;
}
if (fetchRecords.events == 0) {
option.fetchedName = "events";
app.eventsCollection.fetch(option);
this.count ++;
}
},
donefetch: function (collection, response, options) {
if (this.count <=0) return;
this.count --;
if (this.count == 0) {
if (options.reset) this.trigger("reset");
}
fetchedRecords[options.fetchedName] ++;
},
posts: function () {return app.postsCollection},
projects: function () {return app.projectsCollection},
events: function () {return app.eventsCollection}
});
app.everythingCollection = new Everything;
everythingView.listenOn app.everythingCollection, "reset", everythingView.render;
app.everythingCollection.fetch({reset: true});
You will need to increment fetchedRecrods count to prevent fetch multiple times.
Something like this. Code is untested. But idea is the same.
var EverythingCollection = Backbone.Model.extend({
customFetch: function (){
var collections = [app.postsCollection, app.projectsCollection, app.eventsCollection],
index = -1,
collection,
that = this;
this.reset(); //clear everything collection.
//this function check collections one by one whether they have data or not. If collection don't have any data, go and fetch it.
function checkCollection() {
if (index >= collections.length) { //at this point all collections have data.
fillEverything();
return;
}
index = index + 1;
collection = collections[index];
if (collection && collection.models.length === 0) { //if no data in collection.
collection.fetch({success: function () {
checkCollection();
}});
} else { //if collection have data already, go to next collection.
checkCollection();
}
}
function fillEverything() {
collections.forEach(function (collection) {
if (collection) {
that.add(collection.models); //refer this http://backbonejs.org/#Collection-add
}
});
}
}
});
use like below.
app.everythingCollection = new EverythingCollection();
app.everythingCollection.customFetch();
for other collections, check models length before fetch data. Something like below.
if (app.postsCollection.models.length === 0) {
app.postsCollection.fetch();
}
Store all necessary collections in an array or object at app startup, attach an event listener to each of them listening for the first reset event and remember the ones you fetched in a second array. If the route where you need all collections is used you can fetch the ones not found in the array for the already fetched collections:
(untested, but it will give you the idea of how i suppose to do it)
var allCollections = [app.postsCollection, app.projectsCollection, app.eventsCollection];
var fetchedCollections = [];
$.each(allCollection, function(i, coll){
coll.once("reset", function(){
fetchedCollections.push(coll);
})
});
var fetchAll = function(){
$.each(allCollections, function(i, coll){
if( $.inArray(coll, fetchedCollections ) == -1 ){
coll.fetch();
}
});
}
Do this in your everythingCollection and you have the everythingCollection.fetchAll() functionality you need. You could also override the fetch function of the everythingCollection to first fetch all other collections:
fetch(options){
this.fetchAll();
return Backbone.Collection.prototype.fetch.call(this, options);
}
It sounds like braddunbar's supermodel or benvinegar's backbone.uniquemodel might address your problem
It's also worth checking out Soundcloud's article (see Sharing Models Between Views) on building Soundcloud next. They have a similar approach to the above two plugins in solving this problem.

How to show data stored in local storage in my table

I am creating a contact Manager using backbone.js,this is my code
$(document).ready(function() {
var Contact=Backbone.Model.extend({
defaults: {
fname : '',
lname : '',
phoneno : ''
}
});
var ContactList=Backbone.Collection.extend({
model : Contact,
localStorage: new Store("ContactList-backbone")
});
var ContactView=Backbone.View.extend({
el : $('div#contactmanager'),
events: {
'click #additems' : 'add'
},
initialize: function() {
this.render();
this.collection = new ContactList();
},
add : function() {
s1=$('#fname').val();
s2=$('#lname').val();
s3=$('#phoneno').val();
if(s1 =="" || s2=="" || s3=="")
{
alert("Enter values in Textfield");
}
else
{
$('#tlist').append("<tr><td>"+s1+"</td><td>"+s2+"</td><td>"+s3+"</td> </tr>");
cont=new Contact({fname:s1,lname:s2,phoneno:s3});
this.collection.add(cont);
cont.save();
}
},
render : function() {
$(this.el).append("<label><b>First Name</b></label><input id= 'fname' type='text' placeholder='Write ur first name'></input>");
$(this.el).append("<br><label><b>Last Name</b></label><input id= 'lname' type='text' placeholder='Write ur last name'></input>");
$(this.el).append("<br><label><b>Phone Number</b></label><input id= 'phoneno' type='text' placeholder='Write ur phone number'></input>");
$(this.el).append("<br><button id='additems'>ADD</button>");
var showdata=localStorage.getItem('ContactList-backbone',this.model);
console.log(showdata,"showdata");
}
return this;
},
});
var contactManager=new ContactView();
});
This is how I used localstorage
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
function guid() {
return (S4());
};
var Store = function(name)
{
this.name = name;
var store = localStorage.getItem(this.name);
this.data = (store && JSON.parse(store)) || {};
};
_.extend(Store.prototype,
{
save: function() {
localStorage.setItem(this.name, JSON.stringify(this.data));
},
create: function(model) {
if (!model.id) model.id = model.attributes.id = guid();
this.data[model.id] = model;
this.save();
return model;
},
Backbone.sync = function(method, model, options) {
var resp;
var store = model.localStorage || model.collection.localStorage;
switch (method) {
case "create": resp = store.create(model); break;
//I am using only create
}
if (resp) {
options.success(resp);
}
else {
options.error("Record not found");
}
};
The data is getting stored in local storage.
But I can't figure out how to show this data in my table when the page is reloded.
For eg: Iwant to show first name,lname and phone no in table ;
I am new to backbone so plz do help me
Basically you will want to bind the add event in your collection which gets will get called for each item that is being added to the collection and then in the function your binding it to add the code to add the rows to your table. Also you will want to remove the code that is in your current add method that adds the row since it will now be generated when the item gets added to your collection.
Using your code as a base something along the lines of
var ContactView=Backbone.View.extend({
el : $('div#contactmanager'),
events: {
'click #additems' : 'add'
},
initialize: function() {
this.render();
this.collection = new ContactList();
this.collection.bind('add', this.addContact, this);
},
addContact: function(contact) {
//this will get called when reading from local storage as well as when you just add a
//model to the collection
$('#table').append($('<tr><td>' + contect.get('name') + </td></tr>'));
}
Another point being that you have already have underscore.js on your page (since its a requirement for backbone.js) you may want to consider moving your code to generate html to a underscore.js template.
http://documentcloud.github.com/underscore/#template
since you're only using create, you're never going to hit read. Replace your switch statement with by adding a read method
switch (method)
{
case "read":
resp = model.id != undefined ? store.find(model) : store.findAll();
break;
case "create":
resp = store.create(model);
break;
}

Resources