Can a Backbone model contains its own Collection - backbone.js

Can a Backbone model contains a collection of its own type?
component = Backbone.Model.extend({
defaults:{
'name' : 'Name1',
'subcomponents' :Backbone.Collection.extend({
model: component
});
}
});

Of course.
You can have a complex Backbone model graph, that I advise you to maintain with something like Backbone Relational or similar libs.

Related

Backbone Nested Views & Nested Models

I am a newbie to backbone and trying to start using in our projects.
My requirement is I have something like this
var TextFields = Backbone.Model.extend({});
var TextFieldsCollection = Backbone.Collection.extend({});
var Module = Backbone.Model.extend({
defaults: {
fields: new TextFieldsCollection()
});
var ModuleCollection = Backbone.Collection.extend({
model: CTModule
});
Now I have views defined for TextFields & Modules.
If I change a value in TextFields a event gets fired for the model and I am changing the value in the model but the collection is not getting updated.
I tried to trigger backbone events in the child model but at the collection view I am not able to map to the correct model that triggered the change event.
Any comments are helpful. I am not in a position to use more libraries. Can this be done in Backbone?
I do not know what is full code, but if you do it in a fashion that Backbone apps are usually written everything should work.
Quick tutorial:
var TestModel = Backbone.Model.extend({});
var TestModelCollection = Backbone.Collection.extend({ model: TestModel });
// now if you have a view for collection
var TestModelCollectionView = Backbone.View.extend({
initialize: function(opts) {
this.collection = opts.collection;
this.collection.on('change', this.render, this) // so - rerender the whole view
}
});
//execution:
var modelData = { foo: 'bar' };
var collection = new TestModelCollection([modelData]);
var view = new TestModelCollectionView({collection: collection});
view.render();
collection.first().set('your_attr', 'new_value'); // will cause rerender of view
//////
One more thing - if you have collection, which is kept by the model (in your case Modules model, which have collection as one of its attributes), and your view keeps that model, you will have to either bind directly from view to collection via model (this.model.get('fields').on( .... )), or rethrow collection event in your model
I hope I helped at least a bit.
//////
There might be some syntax errors in my code - didn't run it on js fiddle.

Backbone populate Model inside of a Collection

So here is my scenario:
I have a Backbone Collection full of Models. For performance reasons, however, these are not "full" Models. My "full" Models are quite large (imagine each "full" Model has a sub-collection of equally large objects), so when I fetch the Collection from the server I return an array of "partial" Models whose properties are a subset of the "full" model (for example I return only the length of the sub-collection instead of the full sub-collection), just enough to display the Models in a list view to the user.
Now when the user selects an item from the list, I fetch the "full" Model from the server and show a details view of that Model. The problem I have is that now I have two versions of the same Model, one "partial" in the Collection and one "full", and manually having to keep them in sync isn't the right way to do things.
What I'm wondering is if there is an existing pattern (in Backbone or Marionette) for "populating" a "partial" Model into a "full" Model while keeping all of the same references, and for "depopulating" the same Model from a "full" Model into a "partial" Model when we no longer need all of the extra data (i.e. the user navigates to another item in the list).
I have full control over both the front-end and the back-end of my application, and can make changes accordingly if a pattern requires I change what the server returns.
You are representing a single domain object (albeit in two different forms), so you should use a single Model instance to cover both cases.
One fairly clean pattern:
var MyModel = Backbone.Model.extend({
// ... existing code...
inflate: function() {
return $.ajax({
// parameters to fetch the full version
}).then(function(data) {
// process the response - something like this:
this.set({
a: data.a,
b: data.b
}, { silent: true })
this.trigger('inflate')
})
},
deflate: function() {
this.unset('a', { silent: true });
this.unset('b', { silent: true });
// any other cleanup to prevent leaking the large attributes
this.trigger('deflate')
}
})
This pattern uses the custom inflate and deflate events in preference to firing change, because it's semantically more accurate.
You could, of course, DRY up the code by maintaining an array of attribute names that should be in/deflated.
Just like your collection has a URL to the "partial" models, your models should have a URL to the full versions:
var Library = Backbone.Collection.extend({
model: Book,
url: "/books"
});
var Book = Backbone.Model.extend({
url: function () {
return "/books/" + this.get("id");
}
});
When you click your item view use that same model, call a fetch(), and pass it into the detail view.
var BookView = Backbone.View.extend({
tagName: "li",
events: {
"click .details": "openBook"
},
initialize: function() {
// ...
},
openBook: function () {
this.model.fetch();
var bookDetailView = new BookDetailView({ model: this.model });
// Or create the view after successful fetch...
}
// ...
});
var BookDetailView = Backbone.View.extend({});
You won't have two versions of the same model. The model in the collection view will now have all the attributes, but it will only display what is in the template.
As far as "depopulating" it doesn't seem necessary. If the item is clicked again you could even check if the "full" model data is available and lose the extra fetch. If you really want to drop the data, then go ahead and create a method on the model to unset the attributes.

Backbone Marionette : Add a model (and render its view) to a nested Composite View

If you don't want to see the complete code, here is what I am trying to do.
I have multiple pages and each page has multiple tags. There is a composite View called PageManyView for rendering pages which called its childView PageView. Page View is a nested composite view which renders tags, passing this.model.get('tags') as collection.
Now I can easily add a new page by using pages.add(newPage). Here pages is the collection. I am facing problem in adding a new Tag. How can I do that. Please help.
CODE
var PageModel = Backbone.Model.extend({});
var PageCollection = Backbone.Collection.extend({
model: PageModel
});
My JSON at /data endpoint is coming like this
[
{
_id: '1', 'name': '1', info: 'Page 1',
tags: [{name:'main', color:'red'}, {name:'page', color:'blue'}]
},
{
_id: '1', 'name': '2', info: 'Page 2',
tags: [{name:'section', color:'blue'} {name:'about', color:'yellow'}]
}
]
I have created Nested Views in Marionette like this:
TagView = Marionette.ItemView.extend({
template: '#tagOneTemplate'
});
PageView = Marionette.CompositeView.extend({
template: '#pagesTemplate',
childViewContainer: 'div.tags',
childView: EntityViews.TagView,
initialize: function(){
var tags = this.model.get('tags');
this.collection = new Backbone.Collection(tags);
}
});
PageManyView = Marionette.CompositeView.extend({
template: '#pageManyTemplate',
childView: EntityViews.PageView,
childViewContainer: 'div#all-pages'
});
Now here is where i am facing problem. Inside Controller of my application, lets say if I have to add a new page
showPages: function(){
//Getting pages by using jQuery deferred
var view = PageMainView({collection:pages});
view.on("add:page", function(){
var newPage = Page({_id: 3});
pages.add(newPage);
});
}
Now this add function renders the new page automatically.
BUT I AM FACING PROBLEM IN ADDING a NEW TAG. HOW CAN I ADD A NEW TAG?
Finally it worked. Here is what I have done.
Step 1: Get Current model (page) from pages collection.
var currentpage = pages.get(pageid);
Step 2: Use Marionette BabySitter to get the view of the page where I want to insert a new tag.
var v = view.children.findByModel(currentpage);
Step 3: Add tag to v.collection. Since v is the View of the page where I want to insert new tag, v.collection returns the initialised tags collection
v.collection.add(tag);
This works for me. Let me know if I am wrong somewhere or a better way exists. Hope it helps.
this can be done quite easily by shifting around how your collection is being passed in. Instead of setting the collection on initialize in your compositeView, you should pass it in directly during instantiation. This way when you make a change to the collection from within your model, the compositeView will hear the "add" event on collection and add node automagically for you
For example it might look something like this.
PageView = Marionette.CompositeView.extend({
template: '#pagesTemplate',
childViewContainer: 'div.tags',
childView: EntityViews.TagView,
});
new PageView({
model: myModel,
collection: myModel.get("tags")
});
myModel.get("tags").add([{new: "object"}])

Fetch and display rest data with Backbone / Marionette

I'd need some help on fetching data from my server and displaying it using Marionette.
In Angular I'd do this:
index.html:
<body ng-app="app" ng-controller="AppCtrl as app">
<ul>
<li ng-repeat="person in app.people">
{{person.firstName}} {{person.lastName}}
</li>
</ul>
app.js:
var app;
app = angular.module("app", []);
app.controller("AppCtrl", function($http) {
app = this;
$http.get("http://localhost:3000/people").success(function(data) {
app.people = data;
});
});
and my server on my server (express):
var people = [
{
id: 1,
firstName: 'Bob',
lastName: 'Blob',
phoneNumber: '123'
}, {
id: 2,
firstName: 'Valdemar',
lastName: 'Ugh',
phoneNumber: '456'
}
];
app.get('/people', function(req, res) {
res.send(people);
});
Now, how would I get a similar result using Marionette?
I have a model (using coffeescript here in this example):
class Person extends Backbone.Model
I also have a collection:
class People extends Backbone.Collection
model: Person
url: '/people'
Then I'll do this:
people = new People
people.fetch
success: ->
console.log 'works ok'
return people
error: (data) ->
console.log 'no success'
console.log people
view = new Views model: people
What I get in console.log is:
works ok
People {length: 0, models: Array[0], _byId: Object, constructor: function, model: function…}
_byId: Object
length: 2
models: Array[2]
__proto__: ctor
Now, my question is how do I use this? In order to simply list people do I even need this collection or can I do it only with a model? How would I console.log the first name of all the contact on my list? And why does it show length: 0, models: Array[0] and yet there are 2 models??
The hints I can provide are:
Your JSON has a root. Parse the data the same as what you do in Angular.
class People extends Backbone.Collection
# ...
parse: (data) ->
data.people
When you want to show collection, use CollectionView or CompositeView, not View. And use the collection as option, not model.
class PeopleView extends Marionette.CollectionView
peopleView = new PeopleView
collection: people
There are still plenty of things to know about CollectionView and the collection. You can read the doc for details and practice by yourself. Be patient.
This wasn't created specifically as an answer for you, but check out this jsFiddle which is a barebones test of getting data from a REST api and displaying it in Backbone/Marionette - http://jsfiddle.net/tonicboy/5dMjD/.
The key concept you're missing is model events. In your view, you should bind a handler to the "reset" event of your collection. When the collection is fetched, this callback will be used to then render the data to your template. The collection is available from your view as this.collection.toJSON().
Here is the equivalent code from my Fiddle (although it's old code I was playing with while learning Backbone and not how I would do it today). I'll try to update my Fiddle to have more 'best practices' code.
myBook.bind('change', function (model, response) {
var view = new MainView({
el: $("#main"),
model: model
});
this.region.attachView(view);
this.region.show(view);
}, this);

BackboneJS model.url using collection.url

From my understanding the default behavior of Backbone JS Models are to return the Collection's URL, unless the model has a urlRoot specified. I can't seem to get the behavior to work.
From the documentation:
model.url() ... Generates URLs of the form: "[collection.url]/[id]" by default, but you may override by specifying an explicit urlRoot if the model's collection shouldn't be taken into account.
Here is my collection, and model respectively:
var MyCollection = Backbone.Collection.extend({
model: Model,
initialize: function(options){
this.options = options || {};
},
url: function(){
return "/theurl/" + this.options.param;
}
});
return MyCollection;
...
var MyModel = Backbone.Model.extend({
urlRoot: '/theurl',
initialize: function() {
}
});
return MyModel;
When a model is loaded without a collection, it works great and submits to /theurl, but when it's loaded into a collection, all methods submit to /theurl/param/.
If I'm understanding the documentation correctly, the urlRoot of the Model should override this behavior; and even then the models url should be /theurl/param/{MODEL-ID}.
Any ideas on what I'm missing/misunderstanding?
...
PS: The model: Model from the collection is brought in via RequireJS
It will always use the collection's url even if you have urlRoot specified.
The reason for urlRoot is so you can use it in an override, or if the model happens to not be in a collection ( for example maybe it gets removed, but still exists on the client).
So if you want to fetch or save the model and override the url generated by the collection you'll need to pass the urlRoot into these methods explicitly as an option. For example:
yourModel.save({ url: yourModel.urlRoot });
I agree the documentation is confusing and this caught me out recently too.
UrlRoot should be a function and model must have attributeId defined.
If you define your model like this all operation will be working if model is in collection or not.
Backbone add modelId at the end of URL that is returned by urlRoot function.
var MyModel = Backbone.Model.extend({
attributeId: 'myModelId'
urlRoot: function() {
return '/theurl';
},
initialize: function() {
}
defaults: {
myModelId: null
}
});
In the model, try using:
url: function() {
return 'your url goes here';
}

Resources