Browsable Support for Backbone.Collections
Browsing means paging, sorting and filtering through a collection of items. These can be large and thus the Backbone.Collection might contain only a part of it in memory at a time. The Backbone.View showing the collection is supposed to use an extended interface to get the right "window" on the full collection.
Paging can be discreet, where the window is limited by a starting item index and an item count. The view usually presents a pagination control to set up the browsing state, dealing either in items or pages - "batches" of items with the same size.
Paging can also be continuous, which starts at the beginning and continues by loading a specified item count at a time and appending the items to the growing in-memory collection. The view usually calls the next fetch when the scrollbar hits the item threshold, performing an apparent "infinite scrolling".
The collection browsing support provided by the modules below works like this:
- Set up the browsing state
- Listen for the standard Backbone Collection and Model events to get notified when the (part of the) collection has been fetched
- Fetch the collection; the specified part will be placed in the collection
- Adjust the browsing state and repeat the step 3
This browsing support is based on the standard fetch
method and the
standard events (add
, remove
, reset
, sync
). Depending on the server
capabilities, the fetched items may or may not be up-to-date when the fetch
finishes. The reload: true
option can be passed to the fetch
call if
fresh data are needed and the possible performance penalty is acceptable.
Samples of discreet fetching; if your collection uses concept of pages, you have to convert them to item indexes for the collection interface:
// Fetch the first page of 10 items
collection.setSkip(0, false);
collection.setTop(10, false);
collection.fetch();
// Fetch only the second page of 10 items
collection.setSkip(10, false);
collection.fetch();
// Fetch only another page of 10 items
collection.setSkip(collection.skipCount + 10, false);
collection.fetch();
// Fetch only another page of top items
collection.setSkip(collection.skipCount + collection.topCount, false);
collection.fetch();
// Check if more pages are available for fetching
if (collection.length < collection.totalCount) {
...
}
// Reload the collection and start browsing from the beginning
collection.setSkip(0, false);
collection.fetch({reload: true});
Samples of continuous fetching:
// Fetch the first 10 items
collection.setSkip(0, false);
collection.setTop(10, false);
collection.fetch();
// Fetch and append the second 10 items
collection.setSkip(10, false);
collection.fetch({
remove: false,
merge: false
});
// Fetch and append another 10 items
collection.setSkip(collection.skipCount + 10, false);
collection.fetch({
remove: false,
merge: false
});
// Fetch and append another top items
collection.setSkip(collection.skipCount + collection.topCount, false);
collection.fetch({
remove: false,
merge: false
});
// Check if more items are available for fetching
if (collection.length < collection.totalCount) {
...
}
// Reload the collection and start from the first 10 items again
collection.setSkip(0, false);
collection.fetch({reload: true});
Browsing Support Modules
nuc/models/browsable/browsable.mixin : extends the interface of Backbone.Collection with properties and methods supporting the browsing functionality
nuc/models/browsable/client-side.mixin : overrides the interface of Backbone.Collection, which can be fetched only completely, to support the browsing functionality on the client side
nuc/models/browsable/v1.request.mixin
: provides serialization of the URL parameters for the browsing functionality
using the concepts of the /api/v1/nodes/:id/nodes
nuc/models/browsable/v1.response.mixin
: provides deserialization of the collection state and collection items
using the concepts of the /api/v1/nodes/:id/nodes
nuc/models/browsable/v2.response.mixin
: provides deserialization of the collection state and collection items
using the concepts of the /api/v2/members/favorites
Examples
Collection of node (container) children returned by /api/v1/nodes/:id/nodes
,
which supports paging, sorting and filtering:
var NodeChildrenCollection = Backbone.Collection.extend(_.defaults({
model: NodeModel,
constructor: function NodeChildrenCollection(models, options) {
Backbone.Collection.prototype.constructor.apply(this, arguments);
this.makeNodeResource(options)
.makeBrowsable(options)
.makeBrowsableV1Request(options)
.makeBrowsableV1Response(options);
},
url: function () {
var query = this.getBrowsableUrlQuery();
return Url.combine(this.node.urlBase(),
query ? '/nodes?' + query : '/nodes');
},
parse: function (response, options) {
this.parseBrowsedState(response, options);
return response.data;
}
}, NodeResourceModel(Backbone.Collection)));
BrowsableMixin.mixin(NodeChildrenCollection.prototype);
BrowsableV1RequestMixin.mixin(NodeChildrenCollection.prototype);
BrowsableV1ResponseMixin.mixin(NodeChildrenCollection.prototype);
Collection of favourite nodes returned by /api/v2/members/favorites
,
which does not support paging, sorting and filtering:
var FavoritesCollection = Backbone.Collection.extend(_.defaults({
model: NodeModel,
constructor: function FavoritesCollection(models, options) {
Backbone.Collection.prototype.constructor.apply(this, arguments);
this.makeConnectable(options)
.makeFetchable(options)
.makeClientSideBrowsable(options);
},
url: function () {
var url = this.connector.connection.url.replace('/v1', '/v2');
return Url.combine(url, 'members/favorites');
},
parse: function (response, options) {
return response.results;
}
}, ConnectableModel(Backbone.Collection), FetchableModel(Backbone.Collection)));
ClientSideBrowsableMixin.mixin(NodeChildrenCollection.prototype);
BrowsableV2ResponseMixin.mixin(NodeChildrenCollection.prototype);
Continuous paging through favorites by "batches" of 10 items and logging the current collection length:
var connector = ...,
favorites = new FavoriteNodeCollection(undefined, {
connector: connector,
top: 10
});
// The top parameter can be set by `favorites.setTop(10, false)` too
favorites
.fetch()
.then(fetchNext);
function fetchNext() {
console.log(favorites.length);
if (favorites.length < favorites.totalCount) {
return favorites
.fetch({
remove: false,
merge: false
})
.then(fetchNext);
}
}
Loading 10 children from the position 20 in their parent container and logging the actual collection length:
var node = ...,
children = new NodeChildrenCollection(undefined, {
node: node,
skip: 20,
top: 10
});
// The skip and top parameters can be set by `children.setSkip(20, false)`
// and `children.setTop(10, false)` too
children
.fetch()
.done(function () {
console.log(children.length);
});
// Other "window" to the collection can be fetched by adjusting the limit
// using `children.setSkip(..., false)` and `children.setTop(..., false)`
// and repeating the `fetch` call.
Getting the most recent 5 documents from a folder, which means filtering the node (container) children by type including only documents and e-mails, sorting them by the last modification time in the descending direction (and by name, ascending if multiple were modified at the same time), limiting them just to the first 5 and logging their names:
var node = ...,
children = new NodeChildrenCollection(undefined, {
node: node,
filter: {
type: [144, 748]
},
orderBy: 'modify_date desc, name'
top: 5
});
// The parameters can be set by `setFilter, `setOrder` and `setTop` too
children
.fetch()
.done(function () {
console.log(children.pluck('name').join(', '));
});
// Another 5 items can be fetched by `fetch({remove: false, merge: false})`. The
// item count to fetch can be adjusted by `children.setTop(..., false)` before it.
Remarks
Collections extended by either BrowsableMixin
or ClientSideBrowsableMixin
offer the same interface for both discreet and continuous paging. You can use them
interchangingly without knowing which one you have.
ClientSideBrowsableMixin
fetches the complete collection to an internal
buffer at first, then it populates the collection according to the browsing
state properties. Following fetches are served from the internal buffer.
BrowsableMixin
needs to be supported by the REST API on the server side.
If you need to refresh the collection from the server independently on the
client-side and server-side collection, pass the reload: true
option to the
fetch
method. You usually start from the beginning too, by calling
setSkip(0, false)
right before the fetch({reload: true})
.