Backbone - use different views or templates for same collection - backbone.js

The base model of my application has a status attribute.
Let's assume, for simplicity, that status might be either pending or deleted.
I have an upper menu with these two status values, when you click one of them you see all objects with this status (I use router to trigger a filter).
My problem is that I need to draw a different template for each status.
deleted object has delete forever and recover buttons
pending object has delete, edit and some other buttons (also some textarea...)
I wonder what would be the best solution for this problem.
I thought of creating a different view for each status, but then I don't know how to deal with it in the collection level.
I also thought of creating different templates and deal with it in the model-view level, but again - I have no idea whether it is possible and if yes - how.
Finally, I can solve it with same template and view, hiding what is not necessary inside the view, but then the code becomes quite ugly in my point of view.
Ideas?? Thanks!

If you want to create a different view for each status, you do it this way :
Router {
clickDeletedMenu : {
var collection = new MyCollection();
var deletedView = new DeletedView({ model : collection });
collection.fetch({ status : 'deleted' }); // filter deleted objects
},
clickPendingMenu : {
var collection = new MyCollection();
var pendingView = new PendingView({ model : collection });
collection.fetch({ status : 'pending' }); // filter deleted objects
},
}
If you want to create differents templates, you do it this way :
View {
render : {
if (this.model.status == 'deleted') {
// render deleted template
} else {
// render pending template
}
}
}
Finally, in my point of view, you can use the same template and view, and hiding what is not necessary inside the template not the view.
nb : the code is used just to illustrate the idea, it's not going to execute :)

Related

Marionette Regions and routing

I'm using a LayoutView to display a collection in table form. When a user clicks on a tr I swap the CompositeView for an ItemView that shows the details using the same region. It all works except the functionality of the back button is broken. Is there a way to trap the back event and switch views?
Or should I use two Views and pass the model id and then refetch the model? The problem with that though is the extra request and I lose the filter and sort values of the table unless I use local storage.
Including more code would be better, but in any case I will try to give some guidance for your problem.
To avoid fetching the data twice, you can keep a common object in a "parent" component, for example in the Router.
var theObject;
var router = Marionette.AppRouter.extend({
routes: {
"routeA/:id": "goToRouteA",
"routeB/:id": "goToRouteB"
},
goToRouteA: function(id) {
MyRegion.show(new myLayout({
model: this._getCommonObject(id)
}));
},
goToRouteB: function(id) {
MyRegion.show(new myLayout({
model: this._getCommonObject(id)
}));
},
/*Return the common object for the views*/
_getCommonObject: function(id) {
theObject = (theObject && theObject.get('id') == id) ? theObject : MyApp.request('getTheObject');
return theObject;
}
});
In this way, you can keep the reference to the same object without loosing information.
You just have to make sure to delete the object when it is not needed to avoid keeping old information, for example on the Region close event.

Marionette.js - hijacking CompositeView functions to create streaming pagination

I am creating a streaming paginated list of views. We start the app with an empty collection and add items to the collection at regular intervals. When the size of the collection passes a page_size attribute then the rest of the models should not get rendered, but the compositeView should add page numbers to click on.
I am planning on creating a render function for my compositeView that only renders items based on the current page# and page size by have a function in my collection that returns a list of models like this:
get_page_results: function(page_number){
var all_models = this.models;
var models_start = page_number * this.page_size;
var models_end = models_start + this.page_size;
//return array of results for that page
return all_models.slice(models_start,models_end);
}
My question is, should I even be using Marionette's composite view for this? It seems like im overwriting most of the functionality of Marionette's collectionView to get what I want.
Every time the number of items in my collection changes two things need to be updated:
The itemViews in the collection view
The page numbers at the bottom of the composite view
My strong recommendation is not to do this in the view layer. You're going to add a ton of code to your views, and you're going to end up duplicating a lot of this code between multiple views (one for displaying the data, one for page list and counts, one for ...).
Instead, use a decorator pattern to build a collection that knows how to handle this. I do this for filtering, sorting and paging data, and it works very well.
For example, here's how I set up filtering (running in a JSFdiddle here: http://jsfiddle.net/derickbailey/vm7wK/)
function FilteredCollection(original){
var filtered = Object.create(original);
filtered.filter = function(criteria){
var items;
if (criteria){
items = original.where(criteria);
} else {
items = original.models;
}
filtered.reset(items);
};
return filtered;
}
var stuff = new Backbone.Collection();
var filtered = FilteredCollection(stuff);
var view = Backbone.View.extend({
initialize: function(){
this.collection.on("reset", this.render, this);
},
render: function(){
var result = this.collection.map(function(item){ return item.get("foo"); });
this.$el.html(result.join(","));
}
});
In your case, you won't be doing filtering like this... but the idea for paging and streaming would be the same.
You would track what page # you are on in your "PagingCollection", and then when your original collection is reset or has new data added to it, the PagingCollection functions would re-calculate which data needs to be in the final pagedCollection instance, and reset that collection with the data you need.
Something like this (though this is untested and incomplete. you'll need to fill in some detail and flesh it out for your app's needs)
function PagingCollection(original){
var paged = Object.create(original);
paged.currentPage = 0;
paged.totalPages = 0;
paged.pageSize = 0;
paged.setPageSize = function(size){
paged.pageSize = size;
};
original.on("reset", function(){
paged.currentPage = 0;
paged.totalPages = original.length / paged.pageSize;
// get the models you need from "original" and then
// call paged.reset(models) with that list
});
original.on("add", function(){
paged.currentPage = 0;
paged.totalPages = original.length / paged.pageSize;
// get the models you need from "original" and then
// call paged.reset(models) with that list
});
return paged;
}
Once you have the collection decorated with the paging info, you pass the paged collection to your CollectionView or CompositeView instance. These will properly render the models that are found in the collection that you pass to it.
Regarding CollectionView vs CompositeView ... a CompositeView is a CollectionView (it extends directly from it) that allows you to render a model / template around the collection. That's the majority difference... they both deal with collections, and render a list of views from that collection.
We have built a set of mixins for bakcbone.marionette that you may find usefull (https://github.com/g00fy-/marionette.generic/)
You could use PaginatedMixin, that allows a Backbone collection to be paginated + a PrefetchMixin, so you don't have to pass a prefetched collection to a view.
the only code you would have to do is:
YourListView = Generic.ListView.mixin(PaginatedMixin,LoadingMixin,PrefetchMixin).extend({
paginateBy:10,
template:"#your-list-template",
itemViewOptions:{template:"#your-itemview-template"},
fetchPage:function(page){
this.page = page;
return this.collection.refetch({data:{page:page}}); // your code here
},
hasNextPage:function(){
return true; // your code here
},
});
For a working example see https://github.com/g00fy-/stack.reader/blob/master/js/views.js

How can I persist custom attributes over a collection fetch

I have a an "Asset" backbone model that has a custom attribute called "selected". Its custom in the sense that it is not part of the object on the server side. I use to represent which of the list of assets the user has currently selected.
var Asset = Backbone.Model.extend({
defaults: {
selected: false
},
idAttribute: "AssetId"
});
This model is part of a backbone collection that I fetch periodically to get any changes from the server.
The problem I have is that every time I fetch the collection, the collection is doing a reset (I can tell by the listening for the reset event) and hence the value of the selected attribute is wiped out by the data coming in from the ajax request.
The backbone.js documentation seems to suggest that there is a intelligent merge that will solve this problem. I believe I'm doing this in my fetch methods
allAssets.fetch({ update: true ,cache: false});
And I have also set the "idAttribute" field in the model so that the ids of the object coming in can be compared with the objects in the collection.
The way I have solved this is by writing my own Parse method in my collection object
parse: function (response) {
// ensure that the value of the "selected" for any of the models
// is persisted into the model in the new collection
this.each(function(ass) {
if (ass.get("selected")) {
var newSelectedAsset = _.find(response, function(num) { return num.AssetId == ass.get("AssetId"); });
newSelectedAsset.selected = true;
}
});
return response;
}
Is there a better way to do this?
Collection.update (introduced in Backbone 0.9.9) does indeed try to merge existing models, but does so by merging all set attributes in the new model into the old model. If you check Backbone source code, you'll see
if (existing || this._byCid[model.cid]) {
if (options && options.merge && existing) {
existing.set(model.attributes, options);
needsSort = sort;
}
models.splice(i, 1);
continue;
}
All attributes, including defaults, are set, that's why your selected attribute is reset to false. Removing the default value for selected will work as intended: compare http://jsfiddle.net/nikoshr/s5ZXN/ to http://jsfiddle.net/nikoshr/s5ZXN/3/
That said, I wouldn't rely on a model property to store my app state, I would rather move it to a controller somewhere else.

Backbone.js firing Collection change event multiple times

In one of by Backbone.js views I am updating the attribute "read" of the current model (instance of Message) by using this.model.set( { read: true } );. I verified that this command is only executed once (I know about "ghost events"). As you can see below I configured the Collection to fire an update event in which the whole Collection gets saved into a variable.
Unfortunately the saveToVar function gets called 3 times instead of one! Also, the first time saveToVar is called, this correctly consists of all the collection's models, whilst the 2nd and 3rd time this only has one model, namely the one I did the update on.
I tracked everything down piece by piece but I have no clue why this happens.
window.Message = Backbone.Model.extend({
});
window.MessageCollection = Backbone.Collection.extend({
model: Message,
initialize: function()
{
this.on("change", this.saveToVar);
},
saveToVar: function(e)
{
App.Data.Messages = this.toJSON();
return;
}
});
In your jsfiddle, you're doing this:
App.Collections.message = new MessageCollection([ ... ]);
var elements = App.Collections.message.where({ id: 4 });
var item = new MessageCollection(elements);
Your where call will return models that are in the message collection, not copies of those models but exactly the same model objects that are in message. Now you have two references to your id: 4 model:
The original one buried inside App.Collections.message.
The one in elements[0].
Both of those references are pointing at the same object. Then you add elements to another MessageCollection. Now you have something like this:
App.Collections.message.models[3] item.models[0]
| |
+--> [{id: 4}] <--+
Both of those collections will be notified about change events on the id: 4 model since collections listen to all events on their members:
Any event that is triggered on a model in a collection will also be triggered on the collection directly, for convenience.
And your collection listens for "change" events in itself:
initialize: function()
{
this.on("change", this.saveToVar);
}
So when you do this:
this.model.set({ read: true });
in your view, both collections will be notified since that model happens to be in both collections.
If we alter your event handler to look like this:
saveToVar: function() {
console.log(_(this.models).pluck('cid'));
}
then you'll see that the same cid (a unique identifier that Backbone generates) appears in both collections. You can also attach a random number to each collection and see what you get in saveToVar: http://jsfiddle.net/ambiguous/mJvJJ/1/
You probably shouldn't have one model in two collections. You probably shouldn't have two copies of the same model kicking around either so cloning elements[0] before creating item might not be a good idea either. You might need to reconsider your architecture.

merge backbone collection with server response

TL;DR: If I'm polling the entire collection of models from the server, how can I merge the changed attributes into each model and add/remove added/removed models from the collection?
In my backbone app, I am polling for the entire collection of models. I have a Backbone.Collection that I am basically calling reset on each time I get the array of models, so:
myCollection.reset(server_response);
The only problem with this is that it gets rid of the old models, kind of eliminating the benefit of events on a model. This is reset's purpose of course, but what I want to do is only modify the changed attributes of the models, and remove models not in the response, and add models that were in the response but not the collection.
Essentially I want a sort of merge of the data.
For models that are in the response and in the collection already, I believe I can just do model.set(attributes) and it takes care of seting only the ones that actually changed, triggering the change events in the process. This is great.
But how do I handle the cases where the models were in the response but not in the collection already, and vice versa, not in the response but in the collection?
My Proposed Solution
I don't know if backbone already has a way of doing this, and I may be overcomplicating which is why I'm asking, but I was thinking then of creating a method on my collection which gets passed the server_response.
It would get all of the id attributes of the server_response, and all of the id attributes of the models already in the collection.
The difference of the id's in response - collection would = added models, and vice versa would be removed models. Add and remove those models respectively from the collection.
The intersection of both sets of id's would be the modified models, so iterate through these id's and simply do a collection.get(id).set(attributes).
In pseudocoffeescript:
merge: (server_response) =>
response_ids = _.pluck(server_response, 'id')
collection_ids = #pluck('id')
added = _.difference(response_ids, collection_ids)
for add in added
#add(_.find(server_response, (model) ->
return model.id == add
))
removed = _.difference(collection_ids, response_ids)
for remove in removed
#remove(#get(remove))
changed = _.intersection(response_ids, collection_ids)
for change in changed
#get(change).set(_.find(server_response, (model) ->
return model.id == change
))
This technique is useful sometimes. We extend Collection with the following method. This should do what you're looking for. It's not in coffee, but you could easily port it. Enjoy!
// Take an array of raw objects
// If the ID matches a model in the collection, set that model
// If the ID is not found in the collection, add it
// If a model in the collection is no longer available, remove it
freshen: function (objects) {
var model;
// Mark all for removal
this.each(function (m) {
m._remove = true;
});
// Apply each object
_(objects).each(function (attrs) {
model = this.get(attrs.id);
if (model) {
model.set(attrs); // existing model
delete model._remove
} else {
this.add(attrs); // new model
}
}, this);
// Now check for any that are still marked for removal
var toRemove = this.filter(function (m) {
return m._remove;
})
_(toRemove).each(function (m) {
this.remove(m);
}, this);
this.trigger('freshen', this);
}
This just went into Backbone master 3 days ago:
https://github.com/documentcloud/backbone/pull/1220

Resources