Retrieve Backbone Collection from Model and keep it as a Collection - javascript

I'm currently fooling around with backbone.js and came across some wierd behaviour trying to create some relationships between models and collections.
So here is my Model/Collection
Element = Backbone.Model.extend({
name: 'element'
});
Elements = Backbone.Collection.extend({
name: 'elements',
model: Element
});
Application = Backbone.Model.extend({
name: 'app',
initialize: function() {
this.elements = new Elements(this.get('elements'));
}
});
When I retrieve the elements via application.get('elements') I get a 'false' asking if this new Object is an instanceof Backbone.Collection.
var gotElements = application.get('elements');
var isCollection = gotElements instanceof Backbone.Collection;
So am I doing something wrong, or do I have to create a new Collection-Instance and fill it up with the Collection I receive from the application?

In your initialize function doing this.elements sets a property called 'elements' directly on your model (so model.elements will be your Backbone collection). The problem is when you try to retrieve this.elements by calling application.get('elements'), you will see it returns undefined (which is why it is not an instance of a Backbone collection).
In order for a model attribute to be retrievable using model.get it needs be set with model.set(attribute). If you examine the model in the console you will see the difference. model.set(myData) adds your data to the model's attributes hash. model.myData = myData adds a 'myData' property directly to the model.
To get this to work you can do the following:
Element = Backbone.Model.extend({
name: 'element'
});
Elements = Backbone.Collection.extend({
name: 'elements',
model: Element
});
Application = Backbone.Model.extend({
name: 'app',
elements: null
});
var application = new Application({
elements: new Elements()
});
var myElements = application.get('elements');
myElements should now be an empty Backbone Collection

Instead of putting your collection into another model, you should put it in the according view:
var ListView = new Backbone.View.extend({
initialize:function(){
this.Elements= new Elements();
// more stuff to go
},
render:function(){
_.forEach(this.Elements, function(){
//Do rendering stuff of indivdual elements and get HTML
});
//put HTML in the DOM
}
});
There is no need of a model containing a collection containing several models.
Edit: And instead of putting your app into a Model, you should put it into a view.

Related

Backbone js passing arguments

I'm new at reading Backbone js and I have some serious problems with passing arguments in Backbone js.
var Song = Backbone.Model.extend();
var Songs = Backbone.Collection.extend({
model: Song
});
var SongView = Backbone.View.extend({
el: "li",
render: function() {
this.$el.html(this.model.get("title"));
return this;
}
});
var SongsView = Backbone.View.extend({
el: "ul",
initialize: function() {
this.model.on("add", this.onSongAdded, this);
},
onSongAdded: function(song) { // when object is added to a collection add event is triggerd
// the handler for this event get an argument which is the object that was just added
//in this case it refers to a song model so we simply pass it to our songView which is responsible for rendering a song an then we use jquery append method
// to append it to our list
var songView = new SongView({
model: Song
});
this.$el.append(songView.render().$el);
},
render: function() {
var self = this;
this.model.each(function(song) { //
var songView = new SongView({
model: Song
});
self.$el.append(songView.render().$el);
});
}
});
var songs = new Songs([
new Song({
title: "1"
}),
new Song({
title: "2"
}),
new Song({
title: "3"
})
]);
var song_1 = new Song({
title: "hello"
});
var songsView = new SongsView({
el: "#songs",
model: Songs
});
songsView.render();
as you can see I have this function: onSongAdded
we have some built-in events such as add that get 3 arguments like this:
add(collection, model , options)
how can I use these arguments in my code?
can you help me?
el option is for pointing the view to an element already existing in DOM. Your item views should be creating new <li> elements so you should be using tagName option instead.
In your collection view constructor you've defined el option and you're passing a different el option while instantiating it. If #songs is a <uL> in DOM, then no use in defining el: "ul", in constructor.
Also, there is no need to manually instantiate models, you can just pass objects into collections and collection will do it internally. And don't pass collection as model, pass it as collection.

el is not populated correctly in short example

According to backbonejs.org
All views have the el property at all times. Also, to link a view to an already existing element id, you should pass it in when instantiating the view.
In this small example below, I pass in the already existing id in as a string. When I log it to the console later it is logged incorrectly as an empty div. Was I suppose to pass in the actual element and not a string that specified the element id?
Either, way I find it strange that logging the id, logs an actual empty div with that id.
/***************************************************************************************************
*/
var ModelMediaPane = Backbone.Model.extend({
defaults: {
visible: false,
}
});
var model_media_pane = new ModelMediaPane();
var ViewMediaPane = Backbone.View.extend({
});
var view_media_pane = new ViewMediaPane({
model: model_media_pane,
id: 'mi_holder'
});
console.log(model_media_pane.get('visible'));
console.log(view_media_pane.id);
You don't pass an existing id like that, you do it like this:
var view_media_pane = new ViewMediaPane({
model: model_media_pane,
el: '#mi_holder'
});
Otherwise you're just setting the id to the created element.

Is it an anti-pattern to instantiate models in views in Backbone.js?

When developing Backbone applications, I often find myself instantiating models in views when dealing with nested data. Here's some example data:
{
name: Alfred,
age 27,
skills: [
{
name: 'Web development',
level: 'Mediocre'
},
{
name: 'Eating pizza',
level: 'Expert'
]
}
Say I have some view PersonView that takes a person object PersonModel as its model (where Alfred would be an instance). And let's say I want to render the person's skills as subviews. At this point I create a new view and a new model for dealing with the nested skills data. And this is where I suspect I'm doing something wrong, or at least something suboptimal.
var SkillModel = Backbone.Model.extend();
var SkillView = Backbone.View.extend({
tagName: 'li',
initialize: function() {
this.render();
},
render: function() {
this.$el.html(someTemplate(this.model));
}
});
var PersonView = Backbone.View.extend({
el: ...
initialize: ...
renderSkills: function() {
_.each(this.model.get('skills'), function(skill) {
var skillModel = new SkillModel(skill);
var skillView = new SkillView({ model: skillModel });
self.$el.append(skillView.el);
})
}
});
Should I really be doing this or is there some better pattern, that does not involve third-party libraries, that solves the problem? I feel like I'm not really decoupling the views and models in the best way. If you want a concrete question it's: Is this an anti-pattern?
Views are for view logic, Models for data logic.
Consider to have a Model like this:
var SkillModel = Backbone.Model.extend({
});
var SkillsCollection = Backbone.Collection.extend({
model : SkillModel
});
var PersonModel = Backbone.Model.extend({
/*
** Your current model stuff
*/
//new functionality
skills : null,
initialize : function(){
//set a property into the model that stores a collection for the skills
this.skills = new SkillsCollection;
//listen to the model attribute skills to change
this.on('change:skills', this.setSkills);
},
setSkills : function(){
//set the skills of the model into skills collection
this.skills.reset( this.get('skills') );
}
});
So in your view renderSkills would be something like this:
var PersonView = Backbone.View.extend({
renderSkills: function() {
//this.model.skills <- collection of skills
_.each(this.model.skills, function(skill) {
var skillView = new SkillView({ model: skillModel });
self.$el.append(skillView.el);
})
}
});
I did my pseudo code trying to adapt to your sample code. But if you get the point, basically you could have a nested model or collection into your Model, so in the view there is not needed data interaction / set, everything is in the model. Also I answer as your requested, handling things without a third party. However don't mind taking a look to the source of this kind of plugins also:
https://github.com/afeld/backbone-nested
Update: per comments I provided a setup of the Model example:
var m = new PersonModel();
//nested collection is empty
console.log(m.skills.length);
m.set({skills : [{skill1:true}, {skill1:true}]});
//nested collection now contains two children
console.log(m.skills.length);
As you can see the Model usage is as "always" just a set of attributes, and the model is the one handling it. Your view should use skills as how views use any other collection. You could do a .each iterations or worth better to listen for events like add, reset, etc. on that collection(but that is other topic out of the scope of this question).
Example:
http://jsfiddle.net/9nF7R/24/

backbone views not working with fetched json

I'm having issues syncing JSON data received from the server with my views after a fetch.
I do not have a collection of "mainmodel", because I'm only working with one "mainmodel" at a time but numerous "mymodel", anyhow, the structure follows:
var mymodel = Backbone.Model.extend({
defaults: {k1:"",
k2:"",
k3:""}
});
var collection = Backbone.Collection.extend({model:mymodel,});
var mainmodel = Backbone.Model.extend({
defaults: {v1:"",
v2:"",
v3:"",
v4:new collection()
});
I create the nested views for "mymodel" from a render function in a parent view. This works..., only when I'm working with a new model.
// My ParentView render function
render: function() {
for (var i = 0; i < this.model.v4.length;i++) {
var view = new MyModelView({model:this.model.v4.at(i)});
this.$el.append($(view.render().el));
}
this.$el.append(this.template(this.model.toJSON()));
return this;
},
// The MyModelView render function below
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
Now, the above works if I open my application and create models from there. However, If I open my app and supply an id, I make a fetch to the server, retrieve the data, and create a new ParentView I end up getting an error that says "this.model.v4.at not a function". Ugh.
So now, if I change the FIRST render function to be, changing the at(i) to [i]
var view = new MyModelView({model:this.model.v4[i]});
And change the second render function, removing toJSON, to be:
this.$el.html(this.template(this.model));
It renders. But I still can't move around views without errors. No surprise.
I have used console.log(JSON.stringify(this.model)); as they arrive into the parentView and MyModelView. The JSON returned looks like this, whether fetched or created.
{"v1":"val1",
"v2":"val2,
"v3":"val3",
"v4":[{"k1":"key1","k2":"key2","k3","key"}, { ... }, { ... }]
}
The JSON data structures appear to be identical. I thought the JSON format was incorrect, so I tried using JSON.parse before handing the model to the view, but that didn't work. Maybe I'm way off, but I originally thought I had a JSON formatting issue, but now I don't know. The server is returning content as 'application/json'.
Edit: The JSON values for v1,v2,v3 render correctly.
Any ideas?
You have two problems: one you know about and one you don't.
The problem you know about is that your mainmodel won't automatically convert your v4 JSON to a collection so you end up with an array where you're expecting a collection. You can fix this by adding a parse to your mainmodel:
parse: function(response) {
if(response.v4)
response.v4 = new collection(response.v4);
return response;
}
The problem you don't know about is that your defaults in mainmodel has a hidden reference sharing problem:
var mainmodel = Backbone.Model.extend({
defaults: {
//...
v4: new collection()
}
});
Anything you define in the Backbone.Model.extend object ends up on your model's prototype so the entire defaults object is shared by all instances of your model. Also, Backbone will do a shallow copy of defaults into your new models. So if you m1 = new mainmodel() and m2 = new mainmodel(), then m1 and m2 will have exactly the same v4 attribute. You can solve this by using a function for defaults:
var mainmodel = Backbone.Model.extend({
defaults: function() {
return {
v1: '',
v2: '',
v3: '',
v4: new collection()
};
}
});

Backbone.js render collection in View

I have a Backbone Collection that I'm trying to render in the View. The JSON data seems correct, however I can't access the values from within the view.
Here's the basic collection:
define(['backbone', 'BaseModel'], function(Backbone, BaseModel) {
var BaseCollection = Backbone.Collection.extend({
model: BaseModel,
url: "/collection/get?json=true",
initialize: function() {}
});
return BaseCollection;
});
Here's the View:
define(['backbone', 'BaseCollection'], function(Backbone, BaseCollection) {
var BaseView = Backbone.View.extend({
el: $('#baseContainer'),
template: _.template($('#baseTemplate').html()),
initialize: function() {
_.bindAll(this);
this.collection = new BaseCollection();
this.collection.bind('all', this.render, this);
this.collection.fetch();
},
render: function() {
//This returns 3 objects which is correct based on the JSON data being returned from the server
console.log(this.collection.toJSON());
var html = this.template(this.collection.toJSON());
this.$el.html(html);
return this;
},
});
return BaseView;
});
I think I need to iterate through this.render for each model within the collection. But, I'm not sure, because it shouldn't 'render' until it completes all iterations.
Any suggestions would be great! Thank you!
You need to give your template access to the models via name. When you do this:
var html = this.template(this.collection.toJSON());
You end up passing an array to the template function, which normally expects a context object (name/value pairs). Try this:
var html = this.template({collection: this.collection});
Then in your template you can iterate through them using the collection.each iterator function or any of the underscore utility methods for iteration/filtering/map/etc. I also recommend NOT using toJSON when giving your template access to the collection as it makes your data dumber and harder to work with. toJSON is best left for when you are making HTTP requests.

Categories