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

*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)
});

Related

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.

Initializing nested collections with Backbone

I am trying to nest a Collection View into a Model View.
In order to do so, I used Backbone's Marionnette Composite View and followed that tutorial
At the end he initializes the nested collection view like this:
MyApp.addInitializer(function(options){
var heroes = new Heroes(options.heroes);
// each hero's villains must be a backbone collection
// we initialize them here
heroes.each(function(hero){
var villains = hero.get('villains');
var villainCollection = new Villains(villains);
hero.set('villains', villainCollection);
});
// edited for brevity
});
How would you go doing the same without using the addInitalizer from Marionette?
In my project I am fectching data from the server. And when I try doing something like:
App.candidatures = new App.Collections.Candidatures;
App.candidatures.fetch({reset: true}).done(function() {
App.candidatures.each(function(candidature) {
var contacts = candidature.get('contacts');
var contactCollection = new App.Collections.Contacts(contacts);
candidature.set('contacts', contactCollection);
});
new App.Views.App({collection: App.candidatures});
});
I get an "indefined options" coming from the collection:
App.Collections.Contacts = Backbone.Collection.extend({
model: App.Models.Contact,
initialize:function(models, options) {
this.candidature = options.candidature;
},
url:function() {
return this.candidature.url() + "/contacts";
}
)};
That's because when you're creating the contactCollection, you're not providing a candidatures collections in an options object. You do need to modify your contact collection initialization code to something like:
initialize:function(models, options) {
this.candidature = options && options.candidature;
}
That way the candidature attribute will be set to the provided value (and if not provided, it will be undefined).
Then, you still need to provide the info when you're instanciating the collection:
App.candidatures.each(function(candidature) {
var contacts = candidature.get('contacts');
var contactCollection = new App.Collections.Contacts(contacts, {
candidature: candidature
});
candidature.set('contacts', contactCollection);
});
P.S.: I hope you found my blog post useful!

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
);

Backbone.js error - Uncaught TypeError: Object [object Object] has no method 'set'

My Code:
I am new to Backbone.js and trying to build an app with Backbone.js and PHP. When I am trying to call add in the router, I am getting error:
Uncaught TypeError: Object [object Object] has no method 'set'.
Please help me to find my mistake.
Thanks.
// Models
window.Users = Backbone.Model.extend({
urlRoot:"./bb-api/users",
defaults:{
"id":null,
"name":"",
"email":"",
"designation":""
}
});
window.UsersCollection = Backbone.Collection.extend({
model:Users,
url:"./bb-api/users"
});
// Views
window.AddUserView = Backbone.View.extend({
template:_.template($('#new-user-tpl').html()),
initialize:function(){
this.model.bind("click", this.render, this);
},
render:function(){
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events:{
"click .add":"saveUser"
},
saveUser:function(){ alert('saveUser');
this.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(this.model.isNew()){
this.model.create(this.model);
}
return false;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"welcome",
"users":"list",
"users/:id":"userDetails",
"add":"addUser"
},
addUser:function(){
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
$('#content').html(this.addUserView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
As suggested in the comments, the problem starts here here:
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
and finishes here:
saveUser:function(){ alert('saveUser');
this.model.set({
By passing a collection in place of a model you create confusion, and as a result later in the saveUser function you try to call a Backbone.Model method (set) on a Backbone.Collection instance.
Note: As of version 1.0.0 Backbone.Collection now has a set method. In previous versions, such as the one used by the question's author, that method was instead called update.
There are several steps you can take to clarify this code. For starters, I would rename your model and collection classes so that it's clear that the model is the singular form and the collection is the plural form:
window.Users => window.User
window.UsersCollection => window.Users
Next, I would create a new User model, instead of a Users collection, and pass that to your view:
this.addUserModel = new User();
this.addUserView = new AddUserView({model:this.addUserModel});
Finally, I'd remove these lines:
if(this.model.isNew()){
this.model.create(this.model);
}
For one thing, the model will always be new (as you just created it before passing it in), but more importantly you don't need to call the Collection's create method because that method creates a new model, when you already have one created. Perhaps what you should add instead is :
this.model.save();
if your intent is to save the model to your server.
Since you already specified a urlRoot for the model, that should be all you need to create a new model, pass it to your view, have your view fill in its attributes based on DOM elements, and finally save that model's attributes to your server.
I think you are facing problem with object scope. When event fired it send to event object to that function. Just try this it may work
Declare global variable with the current view inside the initialize
initialize : function(){ self = this; }
then change this to self,
saveUser:function(){ alert('saveUser');
self.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(self.model.isNew()){
self.model.create(this.model);
}
return false;
}

Pagination in 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/

Resources