Pagination in Backbone.js - backbone.js

I know that there is a component for this but based on what i see you have to create a new collection with the component extended. Is there another way to do pagination in backbone?
All i need is just a previous and next button limit the items per page to 12. i've been creating it on javascript ( not a good solution for production environment ). Any ideas?

Since Backbone collection has underscore methods extended, you might want to create helper pagination method very easy. I use something like :
var Paginated = Backbone.Collection.extend({
pagination : function(perPage, page) {
page = page-1;
var collection = this;
collection = _(collection.rest(perPage*page));
collection = _(collection.first(perPage));
return collection.map( function(model) { return model.toJSON() } );
}
});
This returns toJSON of your collection, you may play with it in the jsfiddle : http://jsfiddle.net/YHmrp/2/

Related

How to specify filtered Backbone collection for Marionette view

I have a Marionette composite view that displays a collection, which I set in my Application start handler:
App.on('start', function() {
Backbone.history.start({pushState: true});
// I load up this.appsCollection in my before:start handler
var tblView = new this.appsTableView({
collection: this.appsCollection
});
this.regions.main.show(tblView);
});
This works as expected, displaying my entire collection. In my models, I have a state field, and I want to display only models with state 0. I tried:
collection: this.appsCollection.where({state: 0})
but that doesn't work. I actually want to display states in 0 and 1, but I'm trying to just display state in 0 for right now.
What am I missing?
The problem probably resides in that .where() doesn't return a collection, but an array. http://backbonejs.org/#Collection-where This was supposedly to maintain compatibility with underscore.
If you change the line to:
collection: new Backbone.Collection( this.appsCollection.where( { state: 0 } ))
Does that help?
I was able to override the filter method in my Marionette CompositeView:
http://marionettejs.com/docs/v2.4.3/marionette.collectionview.html#collectionviews-filter

backbone.js set in model initialize not effecting models in collection

While performing a fetch() on my backbone collection, and instantiating models as children of that collection, I want to add one more piece of information to each model.
I thought that I could do this using set in the model initialize. (My assumption is that fetch() is instantiating a new model for each object passed into it. And therefore as each initialize occurs the extra piece of data would be set.
To illustrate my problem I've pasted in four snippets, first from my collection class. Second the initialize function in my model class. Third, two functions that I use in the initialize function to get the needed information from the flickr api. Fourth, and finally, the app.js which performs the fetch().
First the collection class:
var ArmorApp = ArmorApp || {};
ArmorApp.ArmorCollection = Backbone.Collection.extend({
model: ArmorApp.singleArmor,
url: "https://spreadsheets.google.com/feeds/list/1SjHIBLTFb1XrlrpHxZ4SLE9lEJf4NyDVnKnbVejlL4w/1/public/values?alt=json",
//comparator: "Century",
parse: function(data){
var armorarray = [];
var entryarray = data.feed.entry;
for (var x in entryarray){
armorarray.push({"id": entryarray[x].gsx$id.$t,
"Filename": entryarray[x].gsx$filename.$t,
"Century": entryarray[x].gsx$century.$t,
"Date": entryarray[x].gsx$date.$t,
"Country": entryarray[x].gsx$country.$t,
"City": entryarray[x].gsx$city.$t,
"Type": entryarray[x].gsx$type.$t,
"Maker": entryarray[x].gsx$maker.$t,
"Recepient": entryarray[x].gsx$recipient.$t,
"Flickrid": entryarray[x].gsx$flickrid.$t,
"FlickrUrl": "", //entryarray[x].gsx$flickrurl.$t,
"FlickrUrlBig": ""//entryarray[x].gsx$flickrurlbig.$t,
});
}
return armorarray;
}
});
Second, the initialization in my model.
initialize: function(){
//console.log("A model instance named " + this.get("Filename"));
item = this;
var flickrapi = "https://api.flickr.com/services/rest/?&method=flickr.photos.getSizes&api_key=<my_apikey>&photo_id=" + this.get("Flickrid") + "&format=json&jsoncallback=?";
sources = getFlickrSources(flickrapi);
sources.then(function(data){
sourceArray = parseFlickrResponse(data);
FlickrSmall = sourceArray[0].FlickrSmall;
console.log (FlickrSmall);
item.set("FlickrUrl", FlickrSmall);
console.log(item);
});
Notice here how I'm getting the "Flickrid" and using to get one more piece of information and then trying to add it back into the model with item.set("FlickrUrl", FlickerSmall);
console.log confirms that the property "FlickrUrl" has been set to the desired value.
Third, these are the functions my model uses to get the information it needs for the flicker api.
var getFlickrSources = function(flickrapi){
flickrResponse = $.ajax({
url: flickrapi,
// The name of the callback parameter, as specified by the YQL service
jsonp: "callback",
// Tell jQuery we're expecting JSONP
dataType: "jsonp"})
return flickrResponse;
}
var parseFlickrResponse = function(data){
flickrSourceArray = []
if (data.stat == "ok"){
sizeArray = data.sizes.size;
for (var y in sizeArray){
if (sizeArray[y].label == "Small"){
flickrSourceArray.push({"FlickrSmall": sizeArray[y].source});
}
else if (sizeArray[y].label == "Large"){
flickrSourceArray.push({"FlickrLarge": sizeArray[y].source});
}
}
}
return flickrSourceArray
}
But, fourth, when I try to perform the fetch and render the collection, I only get objects in my collection without the FlickrUrl property set.
//create an array of models and then pass them in collection creation method
var armorGroup = new ArmorApp.ArmorCollection();
armorGroup.fetch().then(function(){
console.log(armorGroup.toJSON());
var armorGroupView = new ArmorApp.allArmorView({collection: armorGroup});
$("#allArmor").html(armorGroupView.render().el);
});
var armorRouter = new ArmorApp.Router();
Backbone.history.start();
The console.log in this last snippet prints out all the objects/models supposedly instantiated through the fetch. But none of them include the extra property that should have been set during the initialization.
Any ideas what is happening?
What is this function ? getFlickrSources(flickrapi)
Why are you using this.get in the initialize function. Honestly it looks over-complicated for what you are trying to do.
If you want to set some parameter when you instantiate your model then do this var model = new Model({ param:"someparam", url:"someurl",wtv:"somewtv"});
If the point is to update your model just write an update function in your model something like update: function (newparam) { this.set;... etc and call it when you need it.
If I read you well you just want to set some params when your model is instantiated, so just use what I specified above. Here is some more doc : http://backbonejs.org/#Model-constructor
I hope it helps.
edit:
Put your call outside your model, you shouldn't (imo) make call inside your model this way it seems kinda dirty.
Sources.then(function(flickrdata) {
var mymodel = new Model({flicker:flickrdata.wtv});
});
It would be cleaner in my opinion.

Is it good practice to manage view instantiation in a router?

So this is my first Backbone project and I'm wondering if I'm doing things in the best way. My app basically has two states, one of them displays a search box and the other displays a search box with a table under it. My router has routes for searching and for the initial landing page with just the search view. When the user types in the query the router navigates to the search route and the table view is added to the page. This is my router:
app.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search: function(address, age, rad){
app.statusView || (app.statusView = new app.StatusView());
app.searchView || (app.searchView = new app.SearchView());
app.trigger('status:loading');
app.Practices.fetch({
reset: false,
success: function() {
app.searchView.setElement($('#search-box')).render();
var searchQuery = new app.SearchQueryModel({age: age, coords: address.split(","), radius: rad});
if (!app.tableView){
app.tableView = new app.TableView({model: searchQuery});
} else {
app.tableView.model = searchQuery;
app.tableView.refresh();
};
}
});
app.trigger('status:clear');
},
index: function() {
app.statusView = new app.StatusView();
app.searchView = new app.SearchView();
app.footerView = new app.FooterView();
app.searchView.setElement($('#search-box')).render();
}
});
As you can see my views are instantiated in the index route and then the same views are used when you search, unless the user is going directly to the search page in which case the views are instantiated there. I'd be surprised if this wasn't very sub-optimal because it seems clumsy to be checking if the view already exists in the search route. Is there a better way of doing things?
Lets say its not bad, but there is one better approach.
As for now you router is in charge of hook-up URL with app astatus and also for view and model control. The second may be detached from Router, so you will need Controller abstraction, but Backbone does not provide Controller "from the box".
But this is not the problem, you can use plugin or take a look at Controller realization in Marionette.js
The main idea here is to split responsibilities between app part correctly:
1) Router - keeps routes and hook up URL with controller action
2) Controller - manage views and models (create, delete, fetch and so on)
3) View - listen to model and DOM events and render data
4) Model - provide actual data and work with data.
First of all welcome to Backbone. It is a lovely framework which can allow you to make things as beautiful or ugly as you'ld like. Your question is about where view instantiation should be, in terms of good practices. Of course it seems sort of wrong to do it there as it violates the Law of Demeter by handling both url routing and view instantiation.
But the views have to be run from somewhere right? If not the router then where?
So I have two responses:
If your app is simple and you just want to play with backbone then you're probably going to be fine. A lot of people let single page app frameworks complicate otherwise simple apps. I'm not trying to be lazy, but where you have it now is the natural beginner's choice in Backbone. If this is your case then stop here.
If you want to use the full power of backbone to custom make a framework then read on.
So my setup is designed to be able to start a new project using some boilerplate functions and create only a few classes which are specific to the new app. Route handling and all of that kind of thing seems low-level enough to me that it should be just part of some configuration that I don't want to look at often. The upshot is that my router looks like this:
define([
'autorouter'
], function(AutoRouter){
var AppRouter = AutoRouter.extend({
autoRoutes: {
":page" : "routeDirect",
":page/:object" : "routeDirect",
":page/:object/:action" : "routeDirect",
"": "routeDirect"
}
});
return AppRouter;
});
Then for each new project I have a file where I keep the non-default routes, for instance:
define(function(require){
return {
"schedule" : require('screens/schedule')
, "logout" : require('screens/logout')
, "login" : require('screens/login')
, "create" : require('screens/create')
, "upload" : require('screens/upload')
, "select" : require('screens/selection')
, "inventory" : require('screens/inventory')
, "describe" : require('screens/description')
}
});
I put each screen into it's own file (using requirejs for the multi-file dependency management). The extra variables get passed through to the screen.
Each screen is the brain for a particular user experience and is responsible for loading views and maybe handling some events while that screen is active.
If that seems like an interesting setup then here is how I did it:
For the router itself I use a boilerplate class which I borrowed from Derick Bailey with some slight modifications:
define([
'jquery', 'underscore', 'backbone'],
function($, _, Backbone) {
var AutoRouter = Backbone.Router.extend({
constructor: function(options){
Backbone.Router.prototype.constructor.call(this, options);
var that = this;
that.app = options.app;
if (this.autoRoutes){
that.processAutoRoutes(options.app, that.autoRoutes);
}
},
processAutoRoutes: function(app, autoRoutes){
var method, methodName;
var route, routesLength;
var routes = [];
var router = this;
for(route in autoRoutes){
routes.unshift([route, autoRoutes[route]]);
}
routesLength = routes.length;
for (var i = 0; i < routesLength; i++){
route = routes[i][0];
methodName = routes[i][1];
method = app[methodName];
router.route(route, methodName, method);
}
}
});
return AutoRouter;
});
I never have to look at it, but I do need to pass it an app instance. For example:
this.appRouter = new AppRouter({app : this});
Finally my route direction function:
define(function(require){
var pathParser = function(path){
return Array.prototype.slice.call(path);
}
var pathApply = function(path, routes, context){
var pathArray = pathParser(path);
var primary = pathArray[0];
if (routes.hasOwnProperty(primary)){
routes[primary].apply(context, pathArray.slice(1));
} else {
routes["default"].apply(context, pathArray.slice(1));
}
}
return function(path){
//NOTE PLEASE that this references AutoRouter
//Which has an app property
var oApp = this.app;
var pathRoutes = _.extend(require('urls'), {
"default" : require('screens/default')
});
pathApply(arguments, pathRoutes, oApp);
};
});
So, did I make things better? Well if you're doing something very simple with just a screen or two, then you certainly don't want to build this sort of setup from scratch. But if you're like me, and you want to be able to quickly produce new projects then having some boilerplate like the two classes above allows for one JSON object to tell the app which routes I should send to which screens. Then I can have all of the logic in the appropriate places, allowing separation of concerns. Which is why I think Backbone is so pleasant.
My understanding of your problem is that you are triggering a route each time you are hitting search.
If this is how you are doing it, then use view events hash (used to capture and handle events that happen in a view) for search.Don't use routes. Define an events hash in the view and have a callback to handle the search.
var myAppEventBus = _.extend({},Backbone.Events);
var myAppController = {
function : search(options) {
// create an instance of the collection and do a fetch call passing the
// search parameters to it.
var searchResultsCollection = new SearchResultsCollection();
// pass search criteria, the success and error callbacks to the fetch
// method.
var that = this;
searchResultsCollection.fetch(
{
data:that.options,
success : function() {
// Pass the fetched collection object in the trigger call so that
// it can be
// received at the event handler call back
var options = {
"searchResultsCollection" : that.searchResultsCollection;
};
myAppEventBus.trigger("search_event_triggered",options);
},
error : function() {
// do the error handling here.
}
}
);
}
};
// Application Router.
var MyAppRouter = Backbone.Router.extend({
routes : {
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search : function(searchParams) {
// Fetch the query parameters and pass it to the view.
var routeSearchExists = false;
var searchOptions = {};
var options = {};
if(searchParams) {
routeSearchExists = true;
// If search params exist split and set them accordingly in
// the searchOptions object.
options.searchOptions = searchOptions;
}
// Create and render the search view. Pass the searchOptions
var searchView = new SearchView(options);
searchView.render();
// Create and render an instance of the search results view.
var searchResultsView = new SearchResultsView();
searchResultsView.render();
// If there are search parameters from the route, then do a search.
if(routeSearchExists) {
searchView.search();
}
}
});
// The main view that contains the search component and a container(eg: div)
// for the search results.
var SearchView = Backbone.View.extend({
el : "#root_container",
searchOptions : null,
initialize : function(options) {
// Intialize data required for rendering the view here.
// When the user searches for data thru routes, it comes down in the
// options hash which can then be passed on to the controller.
if(options.searchOptions) {
this.searchOptions = options.searchOptions;
}
},
events : {
"search #search_lnk":"initSearch"
},
initSearch : function(event) {
event.preventDefault();
var searchOptions = {};
// Fetch the search fields from the form and build the search options.
myAppController.search(searchOptions);
},
search : function() {
if(this.searchOptions) {
myAppController.search(searchOptions);
}
}
});
// The view to display the search results.
var SearchResultsView = Backbone.View.extend({
searchResultsCollection : null;
initialize : function(options) {
// Handling the triggered search event.
myAppEventBus.on("search_event_triggered",this.render,this);
},
render : function(options) {
//search results collection is passed as a property in options object.
if(options.searchResultsCollection)
//Render your view.
else
// Do it the default way of rendering.
}
});
SearchView is the root view that contains the search component and a container like div to hold the search results.
SearchResultsView displays the result of a search.
When search option is clicked, the event callback (initSearch) gets the entered search data.
The search method on myAppController object is invoked and the search query is passed.
An instance of the search collection is created and fetch is invoked passing it the search query and also the success and error callback.
On success, a custom backbone event is triggered along with the fetched collection.
The callback(render method in SearchResultsView) for this event is invoked.
The callback renders the results of the search.
When loading in the router an instance for both the views can be created(the results view will be empty) and attached to the dom.
If you wish to search by multiple query strings at the url then I would suggest you to use the following route.
search?*queryString.
In the route callback make a call to a utility function the splits the querystring and returns you a search object and pass on the search string to the view.

Multiple collections tied to one base collection with filters and eventing

I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?
I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);

How do you properly display a Backbone marionette collection view based on a model javascript array property?

*UPDATE: See final answer code in the last code block below.*
Currently I am having an issue displaying a collection in a collection view. The collection is a property of an existing model like so (pseudo code)
ApplicationVersion { Id: 1, VersionName: "", ApplicationCategories[] }
So essentially ApplicationVersion has a property called ApplicationCategories that is a javascript array. Currently when I render the collection view associated with ApplicationCategories nothing is rendered. If I debug in Chrome's javascript debugger it appears that the categories have not been populated yet (so I assume ApplicationVersion has not been fetched yet). Here is my code as it stands currently
ApplicationCategory Model, Collection, and Views
ApplicationModule.ApplicationCategory = Backbone.Model.extend({
urlRoot:"/applicationcategories"
});
ApplicationModule.ApplicationCategories = Recruit.Collection.extend({
url:"/applicationcategories",
model:ApplicationModule.ApplicationCategory,
initialize: function(){
/*
* By default backbone does not bind the collection change event to the comparator
* for performance reasons. I am choosing to not preoptimize though and do the
* binding. This may need to change later if performance becomes an issue.
* See https://github.com/documentcloud/backbone/issues/689
*
* Note also this is only nescessary for the default sort. By using the
* SortableCollectionMixin in other sorting methods, we do the binding
* there as well.
*/
this.on("change", this.sort);
},
comparator: function(applicationCategory) {
return applicationCategory.get("order");
},
byName: function() {
return this.sortedBy(function(applicationCategory) {
return applicationCategory.get("name");
});
}
});
_.extend(ApplicationModule.ApplicationCategories.prototype, SortableCollectionMixin);
ApplicationModule.ApplicationCategoryView = Recruit.ItemView.extend({
template:"application/applicationcategory-view-template"
});
ApplicationModule.ApplicationCategoriesView = Recruit.CollectionView.extend({
itemView:ApplicationModule.ApplicationCategoryView
});
ApplicationCategory template
<section id="<%=name%>">
<%=order%>
</section>
ApplicationVersion Model, Collection, and Views
ApplicationModule.ApplicationVersion = Backbone.Model.extend({
urlRoot:"/applicationversions"
});
ApplicationModule.ApplicationVersions = Recruit.Collection.extend({
url:"/applicationversions",
model:ApplicationModule.ApplicationVersion
});
ApplicationModule.ApplicationVersionLayout = Recruit.Layout.extend({
template:"application/applicationversion-view-template",
regions: {
applicationVersionHeader: "#applicationVersionHeader",
applicationVersionCategories: "#applicationVersionCategories",
applicationVersionFooter: "#applicationVersionFooter"
}
});
ApplicationModule.ApplicationVersionController = {
showApplicationVersion: function (applicationVersionId) {
ApplicationModule.applicationVersion = new ApplicationModule.ApplicationVersion({id : applicationVersionId});
var applicationVersionLayout = new Recruit.ApplicationModule.ApplicationVersionLayout({
model:ApplicationModule.applicationVersion
});
ApplicationModule.applicationVersion.fetch({success: function(){
var applicationVersionCategories = new Recruit.ApplicationModule.ApplicationCategoriesView({
collection: ApplicationModule.applicationVersion.application_categories
});
applicationVersionLayout.applicationVersionCategories.show(applicationVersionCategories);
}});
// Fake server responds to the request
ApplicationModule.server.respond();
Recruit.layout.main.show(applicationVersionLayout);
}
};
Here is my ApplicationVersion template
<section id="applicationVersionOuterSection">
<header id="applicationVersionHeader">
Your Application Header <%= id %>
</header>
<section id="applicationVersionCategories">
</section>
<footer id="applicationVersionFooter">
Your footer
</footer>
One thing to note I am currently using Sinon to mock my server response, but I don't think this is causing the issues as it is responding with the information as I expect looking through the javascript debugger (and like I said it is displaying ApplicationVersion id correctly). I can provide this code as well if it helps
It is currently displaying the application version id (id in the template), so I know it is fetching the data correctly for normal properties, it just is not rendering my ApplicationCategories javascript array property.
So ultimately I am binding to the success of the fetch for ApplicationVersion, then setting up the view for the ApplicationCategories. Since this isn't working like I expect I am wondering if there is a better way to create this collection view?
Thanks for any help
UPDATE: Working code example that Derek Bailey lead me too.
ApplicationModule.ApplicationVersionController = {
showApplicationVersion: function (applicationVersionId) {
ApplicationModule.applicationVersion = new ApplicationModule.ApplicationVersion({id : applicationVersionId});
var applicationVersionLayout = new Recruit.ApplicationModule.ApplicationVersionLayout({
model:ApplicationModule.applicationVersion
});
ApplicationModule.applicationVersion.fetch();
// Fake server responds to the request
ApplicationModule.server.respond();
Recruit.layout.main.show(applicationVersionLayout);
var applicationVersionCategories = new Recruit.ApplicationModule.ApplicationCategoriesView({
collection: new Backbone.Collection(ApplicationModule.applicationVersion.get('application_categories'))
});
applicationVersionLayout.applicationVersionCategories.show(applicationVersionCategories);
}
};
Marionette's CollectionView requires a valid Backbone.Collection, not a simple array. You need to create a Backbone.Collection from your array when passing it to the view:
new MyView({
collection: new Backbone.Collection(MyModel.Something.ArrayOfThings)
});

Resources