Backbone : Make collection from slice of another collection - javascript

I'd like to have a paginated view of a collection. I tried using Backbone.Paginator but I just couldn't make it work.
I figured I'd do the pagination myself and I thought it would be a good idea to have my full collection, and then send the view a small collection of the big one, and do this every time someone clicks on 'next'.
I tried doing this but it doesn't work :
var purchaseCollection = new purchaseItemCollection({url:endpoints.purchases});
purchaseCollection.fetch();
var purchaseRangeCollection = new Backbone.Collection(purchaseCollection.models),
purchaseView = new purchaseItemCollectionView({collection:purchaseRangeCollection});
My second collection is only made of one model when it should have severals.
I'm wondering if this is even the best way to do it.
Any advice on how to split a collection into collections, or how to do it in another way would really be appreciated!

You could use a custom object to control a collection representing the list of models currently selected.
For example,
var Slicer = function(opts) {
opts || (opts = {});
// your big collection
this.collection = opts.collection || new Backbone.Collection();
// a collection filled with the currently selected models
this.sliced = new Backbone.Collection();
};
_.extend(Slicer.prototype, Backbone.Events, {
// a method to slice the original collection and fill the container
slice: function(begin, end) {
var models = this.collection.models.slice(begin, end);
this.sliced.reset(models);
// triggers a custom event for convenience
this.trigger('sliced', this.sliced);
}
});
You would then create an instance of this controller and either listen to the custom event sliced on this object or on a reset event on the sliced collection to update your view:
var controller = new Slicer({
collection: purchaseCollection
});
controller.on('sliced', function(c) {
console.log('sliced', c.pluck('id'));
});
controller.sliced.on('reset', function(c) {
console.log('reset', c.pluck('id'));
});
And a demo to play with http://jsfiddle.net/nikoshr/zjgkF/
Note that you also have to take into account the asynchronous nature of fetch, you can't immediately work on the models. In this setup, you would do something like
var purchaseCollection = new purchaseItemCollection(
[], // you have to pass an array
{url:endpoints.purchases} // and then your options
);
purchaseCollection.fetch().then(function() {
// do what you want
// for example
controller.slice(0, 10);
});

You can define the model of your full collection as another independent collection.
Then after fetch(), you will get your collections as the model of your full one.
var PurchaseCollection = Backbone.Collection.extend({
model: Backbone.Collection
})
var purchaseCollection = new PurchaseCollection({url:endpoints.purchases});
purchaseCollection.fetch()
purchaseCollection.each(function (purchaseItem, index) {
//the purchaseItem is what you want...
//do anything...
});
If you want a demo, click here.

Just remember that collection constructor has two attributes (http://backbonejs.org/#Collection-constructor). The first are the models and the second are the options like url etc.

Related

Elegant way to append several views to a region in Marionette

Is there a more elegant way than the one below to append another view to a region? I'd like to append as many chat windows when a button is clicked, and destroy it when a button within the chat window is clicked.
The one below requires to keep track of an index per element:
var AppLayoutView = Backbone.Marionette.LayoutView.extend({
template: "#layout-view-template",
regions: {
wrapperChat : '#wrapper-chat'
}
appendView: function ( incremennt, newView ){
this.$el.append( '<div id="view'+increment+'" >' ) ;
this.regionManager.addRegion( 'view'+increment , '#view'+increment )
this['view'+increment].show ( newView ) ;
}
});
// ChatView
var ChatView = Marionette.ItemView.extend({
template: "#chat-template"
});
// Layout
var LayoutView = new AppLayoutView();
LayoutView.render();
// Append View
LayoutView.wrapper.appendView(++index, new ChatView());
Regions are designed to show a single View. Marionette's abstraction for repeating views is CollectionView, which renders an ItemView for each Model in a Collection.
You add or remove Models from the Collection; Marionette handles the view updates for you.
If your ChatView already has a model, use that. If not, you could add a trivial model to abstract away the index variable.
// Collection for the Chat models.
// If you already have Chat Collection/Models, use that.
// If not, create a simple Collection and Models to hold view state, e.g.:
var chats = new Backbone.Collection();
// CollectionView "subclass"
var ChatCollectionView = Marionette.CollectionView.extend({
itemView: ChatView
})
// Add a single ChatCollectionView to your region
var chatsView = new ChatCollectionView({ collection: chats });
LayoutView.getRegion('wrapperChat').show();
// To add a ChatView, add a Model to the Collection
var nextChatId = 0;
chart.addChat(new Backbone.Model({ id: nextChatId++ }));
// To remove a chat, remove its model
chart.remove(0);

Marionette LayoutView regions share CollectionView

I have a LayoutView that consists of two regions. These two regions share the same collection/collection-view, the only difference being the API endpoint that the collection calls to.
initialize: function () {
// setup collection for scheduled mailings
this._scheduledView = new MailingsCollectionView({
collection: new MailingsCollection()
});
this._scheduledView.collection.url = '/api/mailings?is_scheduled=true&mailing_types=m';
// setup collection for sent mailings
this._sentView = new MailingsCollectionView({
collection: new MailingsCollection()
});
this._sentView.collection.url = '/api/mailings?mailing_statuses=c&mailing_types=m';
this.listenTo(this._scheduledView.collection, 'change:checked', this.setMailing)
},
Instead of writing the this.listenTo() line for each region, how can I listenTo the shared collection at one time?
There is no way to to listenTo multiple objects, however you could break some logic out to make it just as clean as a single listenTo call.
initialize: function()
{
this._scheduledView = this.makeCollectionView('/api/mailings?is_scheduled=true&mailing_types=m');
this._sentView = this.makeCollectionView('/api/mailings?mailing_statuses=c&mailing_types=m');
},
makeCollectionView: function(url)
{
var collection = new MailingsCollection({url: url}),
collectionView = new MailingsCollectionView({collection: collection});
this.listenTo(collection, 'change:checked', this.setMailing);
return collectionView;
}

Meteor, One to Many Relationship & add field only to client side collection in Publish?

Can anyone see what may be wrong in this code, basically I want to check if a post has been shared by the current logged in user AND add a temporary field to the client side collection: isCurrentUserShared.
This works the 1st time when loading a new page and populating from existing Shares, or when adding OR removing a record to the Shares collection ONLY the very 1st time once the page is loaded.
1) isSharedByMe only changes state 1 time, then the callbacks still get called as per console.log, but isSharedByMe doesn't get updated in Posts collection after the 1st time I add or remove a record. It works the 1st time.
2) Why do the callbacks get called twice in a row, i.e. adding 1 record to Sharescollection triggers 2 calls, as show by console.log.
Meteor.publish('posts', function() {
var self = this;
var mySharedHandle;
function checkSharedBy(IN_postId) {
mySharedHandle = Shares.find( { postId: IN_postId, userId: self.userId }).observeChanges({
added: function(id) {
console.log(" ...INSIDE checkSharedBy(); ADDED: IN_postId = " + IN_postId );
self.added('posts', IN_postId, { isSharedByMe: true });
},
removed: function(id) {
console.log(" ...INSIDE checkSharedBy(); REMOVED: IN_postId = " + IN_postId );
self.changed('posts', IN_postId, { isSharedByMe: false });
}
});
}
var handle = Posts.find().observeChanges({
added: function(id, fields) {
checkSharedBy(id);
self.added('posts', id, fields);
},
// This callback never gets run, even when checkSharedBy() changes field isSharedByMe.
changed: function(id, fields) {
self.changed('posts', id, fields);
},
removed: function(id) {
self.removed('posts', id);
}
});
// Stop observing cursor when client unsubscribes
self.onStop(function() {
handle.stop();
mySharedHandle.stop();
});
self.ready();
});
Personally, I'd go about this a very different way, by using the $in operator, and keeping an array of postIds or shareIds in the records.
http://docs.mongodb.org/manual/reference/operator/query/in/
I find publish functions work the best when they're kept simple, like the following.
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('sharedPosts', function(postId) {
var postRecord = Posts.findOne({_id: postId});
return Shares.find{{_id: $in: postRecord.shares_array });
});
I am not sure how far this gets you towards solving your actual problems but I will start with a few oddities in your code and the questions you ask.
1) You ask about a Phrases collection but the publish function would never publish anything to that collection as all added calls send to minimongo collection named 'posts'.
2) You ask about a 'Reposts' collection but none of the code uses that name either so it is not clear what you are referring to. Each element added to the 'Posts' collection though will create a new observer on the 'Shares' collection since it calls checkSharedId(). Each observer will try to add and change docs in the client's 'posts' collection.
3) Related to point 2, mySharedHandle.stop() will only stop the last observer created by checkSharedId() because the handle is overwritten every time checkSharedId() is run.
4) If your observer of 'Shares' finds a doc with IN_postId it tries to send a doc with that _id to the minimongo 'posts' collection. IN_postId is passed from your find on the 'Posts' collection with its observer also trying to send a different doc to the client's 'posts' collection. Which doc do you want on the client with that _id? Some of the errors you are seeing may be caused by Meteor's attempts to ignore duplicate added requests.
From all this I think you might be better breaking this into two publish functions, one for 'Posts' and one for 'Shares', to take advantage of meteors default behaviour publishing cursors. Any join could then be done on the client when necessary. For example:
//on server
Meteor.publish('posts', function(){
return Posts.find();
});
Meteor.publish('shares', function(){
return Shares.find( {userId: this.userId }, {fields: {postId: 1}} );
});
//on client - uses _.pluck from underscore package
Meteor.subscribe( 'posts' );
Meteor.subscribe( 'shares');
Template.post.isSharedByMe = function(){ //create the field isSharedByMe for a template to use
var share = Shares.findOne( {postId: this._id} );
return share && true;
};
Alternate method joining in publish with observeChanges. Untested code and it is not clear to me that it has much advantage over the simpler method above. So until the above breaks or becomes a performance bottleneck I would do it as above.
Meteor.publish("posts", function(){
var self = this;
var sharesHandle;
var publishedPosts = [];
var initialising = true; //avoid starting and stopping Shares observer during initial publish
//observer to watch published posts for changes in the Shares userId field
var startSharesObserver = function(){
var handle = Shares.find( {postId: {$in: publishedPosts}, userId === self.userId }).observeChanges({
//other observer should have correctly set the initial value of isSharedByMe just before this observer starts.
//removing this will send changes to all posts found every time a new posts is added or removed in the Posts collection
//underscore in the name means this is undocumented and likely to break or be removed at some point
_suppress_initial: true,
//other observer manages which posts are on client so this observer is only managing changes in the isSharedByMe field
added: function( id ){
self.changed( "posts", id, {isSharedByMe: true} );
},
removed: function( id ){
self.changed( "posts", id, {isSharedByMe: false} );
}
});
return handle;
};
//observer to send initial data and always initiate new published post with the correct isSharedByMe field.
//observer also maintains publishedPosts array so Shares observer is always watching the correct set of posts.
//Shares observer starts and stops each time the publishedPosts array changes
var postsHandle = Posts.find({}).observeChanges({
added: function(id, doc){
if ( sharesHandle )
sharesHandle.stop();
var shared = Shares.findOne( {postId: id});
doc.isSharedByMe = shared && shared.userId === self.userId;
self.added( "posts", id, doc);
publishedPosts.push( id );
if (! initialising)
sharesHandle = startSharesObserver();
},
removed: function(id){
if ( sharesHandle )
sharesHandle.stop();
publishedPosts.splice( publishedPosts.indexOf( id ), 1);
self.removed( "posts", id );
if (! initialising)
sharesHandle = startSharesObserver();
},
changed: function(id, doc){
self.changed( "posts", id, doc);
}
});
if ( initialising )
sharesHandle = startSharesObserver();
initialising = false;
self.ready();
self.onStop( function(){
postsHandle.stop();
sharesHandle.stop();
});
});
myPosts is a cursor, so when you invoke forEach on it, it cycles through the results, adding the field that you want but ending up at the end of the results list. Thus, when you return myPosts, there's nothing left to cycle through, so fetch() would yield an empty array.
You should be able to correct this by just adding myPosts.cursor_pos = 0; before you return, thereby returning the cursor to the beginning of the results.

Setting and Initializing multiple knockout view models

I am trying to create and initialize some sort of master view model that contains common view models that might be run on every page and page specific models that are appended on page load.
var MasterViewModel = {
commonViewModel1 : CommonViewModel1(),
commonViewModel2 : CommonViewModel1()
};
var commonInit = function() {
// Populate View Model Data
MasterViewModel.commonViewModel1 = initCommonViewModel1();
MasterViewModel.commonViewModel2 = initCommonViewModel2();
// Apply common view model bindings
ko.applyBindings(MasterViewModel);
};
var pageSpecificInit = function() {
// Populate Specific View Model Data
MasterViewModel.pageViewModel1 = initPageViewModel1();
MasterViewModel.pageViewModel2 = initPageViewModel2();
// Apply Page Specific Bindings
ko.applyBindings(MasterViewModel);
};
$(function() {
commonInit();
pageSpecificInit();
});
this is a crude example of what I am trying to do in the real application this is all namespaced and in separate files so that only page specific code is run. What is the best practice for doing this I somewhat based the above on http://www.knockmeout.net/2012/05/quick-tip-skip-binding.html but when I do it in the application I get something like "cannot bind to pageViewModel1 undefined" should I setup my MasterViewModel differently to be more like
var MasterViewModel = {
commonViewModel1 : CommonViewModel1(),
commonViewModel2 : CommonViewModel1(),
pageViewModels : {}
};
var commonInit = function() {
// Populate View Model Data
MasterViewModel.commonViewModel1 = initCommonViewModel1();
MasterViewModel.commonViewModel2 = initCommonViewModel2();
// Apply common view model bindings
ko.applyBindings(MasterViewModel);
};
var pageSpecificInit = function() {
// Populate Specific View Model Data
MasterViewModel.pageViewModels.pageViewModel1 = initPageViewModel1();
MasterViewModel.pageViewModels.pageViewModel2 = initPageViewModel2();
// Apply Page Specific Bindings
ko.applyBindings(MasterViewModel.pageViewModels);
};
$(function() {
commonInit();
pageSpecificInit();
});
Your second example is more correct, but shouldn't you be binding the page-specific view models to a specific html element that you've surrounded with the stop binding comment?
ko.applyBindings(MasterViewModel.pageViewModels, $('#pageElement')[0]);
However, if you want to have nicely decoupled objects that can talk to each other, then you might want to look at Knockout Postbox

When do I need a model in backbone.js?

I'm new to Backbone.js, and someone who comes out of the 'standard' model of JS development I'm a little unsure of how to work with the models (or when).
Views seem pretty obvious as it emulates the typical 'listen for event and do something' method that most JS dev's are familiar with.
I built a simple Todo list app and so far haven't seen a need for the model aspect so I'm curious if someone can give me some insight as to how I might apply it to this application, or if it's something that comes into play if I were working with more complex data.
Here's the JS:
Todos = (function(){
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
}
});
var TodoView = Backbone.View.extend({
el: $('#todos'),
newitem: $('#new-item input'),
noitems: $('#no-items'),
initialize: function(){
this.el = $(this.el);
},
events: {
'submit #new-item': 'addItem',
'click .remove-item': 'removeItem'
},
template: $('#item-template').html(),
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
return this;
},
removeItem: function(e){
$(e.target).parent('.item-wrap').remove();
}
});
self = {};
self.start = function(){
new TodoView();
};
return self;
});
$(function(){
new Todos(jQuery).start();
});
Which is running here: http://sandbox.fluidbyte.org/bb-todo
Model and Collection are needed when you have to persist the changes to the server.
Example:
var todo = new TodoModel();
creates a new model. When you have to save the save the changes, call
todo.save();
You can also pass success and error callbacks to save . Save is a wrapper around the ajax function provided by jQuery.
How to use a model in your app.
Add a url field to your model
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
},
url: {
"http://localhost";
}
});
Create model and save it.
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
var todo = new TodoModel({'content':this.newitem.val()});
todo.save();
return this;
},
Make sure your server is running and set the url is set correctly.
Learning Resources:
Check out the annotated source code of Backbone for an excellent
explanation of how things fall into place behind the scenes.
This Quora question has links to many good resources and sample apps.
The model is going to be useful if you ever want to save anything on the server side. Backbone's model is built around a RESTful endpoint. So if for example you set URL root to lists and then store the list information in the model, the model save and fetch methods will let you save/receive JSON describing the mode to/from the server at the lists/<id> endpoint. IE:
ToDoListModel = Backbone.model.extend( {
urlRoot : "lists/" } );
// Once saved, lives at lists/5
list = new ToDoListModel({id: 5, list: ["Take out trash", "Feed Dog"] });
list.save();
So you can use this to interact with data that persists on the server via a RESTful interface. see this tutorial for more.
I disagree with the idea that model is needed only to persist changes (and I am including LocalStorage here, not only the server).
It is nice to have representation of models and collections so that you have object to work with and not only Views. In your example you are only adding and removing divs (html) from the page, which is something you can do normally with jQuery. Having a Model created and added to a Collection everytime you do "add" and maybe removed when you clear it will allow you some nice things, like for example sorting (alphabetically), or filtering (if you want to implement the concept of "complete" to-do).
In your code, for example:
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
complete: false
}
});
var Todos = Backbone.Collection.extend({
model: TodoModel
})
In the View (irrelevant code is skipped):
// snip....
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
var newTodo = new TodoModel({ content: this.newitem.val() });
this.collection.add(newTodo); // you get the collection property from free from the initializer in Backbone
this.el.append(templ({model: newTodo})); // change the template here of course to use model
this.newitem.val('').focus();
return this;
},
Initialize like this:
self.start = function(){
new TodoView(new Todos());
};
Now you have a backing Collection and you can do all sort of stuff, like filtering. Let's say you have a button for filtering done todos, you hook this handler:
_filterDone: function (ev) {
filtered = this.collection.where({ complete: true });
this.el.html(''); // empty the collection container, I used "el" but you know where you are rendering your todos
_.each(filtered, function (todo) {
this.el.append(templ({model: todo})); // it's as easy as that! :)
});
}
Beware that emptying the container is probably not the best thing if you have events hooked to the inner views but as a starter this works ok.
You may need a hook for setting a todo done. Create a button or checkbox and maybe a function like this:
_setDone: function (ev) {
// you will need to scope properly or "this" here will refer to the element clicked!
todo = this.collection.get($(ev.currentTarget).attr('todo_id')); // if you had the accuracy to put the id of the todo somewhere within the template
todo.set('complete', true);
// some code here to re-render the list
// or remove the todo single view and re-render it
// in the simplest for just redrawr everything
this.el.html('');
_.each(this.collection, function (todo) {
this.el.append(templ({model: todo}));
});
}
The code above would not have been so easy without Models and Collections and as you can see it does not relate in any way with the server.

Categories

Resources