Nested collections with Backbone.Marionette - backbone.js

I want to create a calendar which is a day collection, and every day is a collection of appointments. The structure of day object is:
day:{
date:'08-06-2012',
appointment: {
time_begin:'12:55',
time_end: '16:40',
customer: 'Jaime'
}
}
At this moment i have this models and views:
// CALENDAR COLLECTION
App.Calendar.Collection = Backbone.Collection.extend({
// MODEL
model: App.Day.Model
}
When calendar collection fetch data form the server, it gets complete day objects including appointments.
// CALENDAR VIEW
App.Calendar.View = Backbone.Marionette.CompositeView.extend({
// TEMPLATE
template: Handlebars.compile(templates.find('#calendar-template').html()),
// ITEM VIEW
itemView: App.Day.View,
// ITEM VIEW CONTAINER
itemViewContainer: '#calendar-collection-block'
});
// DAY MODEL
App.Day.Model = Backbone.Collection.extend({
// PARSE
parse:function(data){
console.log(data);
return data;
}
});
// DAY VIEW
App.Day.View = Backbone.Marionette.CompositeView.extend({
collection: App.Day.Model,
itemView: App.CalendarAppointment.View, //---->I NEED TO DEFINE THIS, NOT SURE HOW
template: Handlebars.compile(templates.find('#day-template').html())
});
The day model needs to be a collection of appointments and shouldn't be necessary to fetch data from the server because it's inside every day.
How can i do this?

if I understand the question right, you're asking how to get the data from your Day model, Appointment collection, in to the CalendarApointmentView itemView?
Your Day.View can be set up to populate the collection of this composite view, which will then be pushed in to the item views:
// DAY VIEW
App.Day.View = Backbone.Marionette.CompositeView.extend({
collection: App.Day.Model,
itemView: App.CalendarAppointment.View,
template: Handlebars.compile(templates.find('#day-template').html()),
initialize: function(){
// since `this.model` is a `Day` model, it has the data you need
this.collection = this.model.get("CalendarAppointments");
}
});
One thing to note: the this.collection must be a valid Backbone.Collection. If your model stores the appointment data as a simple array, then you need to do this:
var appointments = this.model.get("CalendarAppointments");
this.collection = new AppointmentCollection(appointments);
Hope that helps.

This looks like the perfect usecase for backbone-relational, which would automatically take care of parsing your nested data and creating the nested collection structure.

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.

Marionette collection view does not update when model added

I am new to Marionette. So may be its pretty basic.
I have a collection initialized globally and is empty during creation.
App.Colors= Backbone.Collection.extend({
model : App.Color,
comparator : "price"
});
var appColors = new App.Colors();
If I create a Marionette collection view with this empty collection.
var colorview= new App.ColorView({collection:appColors});
and later on add to the collection appColors.
appColors.add({code:'red'})
Shouldn't the view be updated automatically since it listens to collection.add. I know it will work fine if I type:
colorview.collection.add({code:'red'})
In order for it to work, you must bind the view rendering to the collection add event, so basicly in your view declaration you must have something like this :
App.ColorView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'add', this.render);
...
}
...
});

BB Marionette: best way to update a collection without re-rendering

I have a very simple page that shows a collection in a table. Above it theres a search field where the user enters the first name of users.
When the user types I want to filter the list down.
Edit: I have updated the code to show how the current compositeView works. My aim is to integrate a searchView that can _.filter the collection and hopefully just update the collection table.
define([
'marionette',
'text!app/views/templates/user/list.html',
'app/collections/users',
'app/views/user/row'
],
function (Marionette, Template, Users, User) {
"use strict"
return Backbone.Marionette.CompositeView.extend({
template: Template,
itemView: User,
itemViewContainer: "tbody",
initialize: function() {
this.collection = new Users()
this.collection.fetch()
}
})
})
Divide your template in a few small templates, this increases performance at the client side, you don't have problems with overriden form elements and you have more reuseable code.
But be aware of too much separation, cause more templates means more views and more code/logic.
You don't seem to be making use of CollectionView as well as you could be. If I were you I would separate the concerns between the search box and the search results. Have them as separate views so that when one needs to rerender, it doesn't effect the other.
This code probably won't work straight away as I haven't tested it. But hopefully it gives you some clue as to what ItemView, CollectionView, and Layout are and how they can help you remove some of that boiler plate code
//one of these will be rendered out for each search result.
var SearchResult = Backbone.Marionette.ItemView.extend({
template: "#someTemplateRepresentingEachSearchResult"
)};
//This collectionview will render out a SearchResult for every model in it's collection
var SearchResultsView = Backbone.Marionette.CollectionView.extend{
itemView: SearchResult
});
//This layout will set everything up
var SearchWindow = Backbone.Marionette.Layout.extend({
template: "#someTemplateWithASearchBoxAndEmptyResultsRegionContainer",
regions:{
resultsRegion: "#resultsRegion"
},
initialize: function(){
this.foundUsers = new Users();
this.allUsers = new Users();
this.allUsers.fetch({
//snip...
});
events: {
'keyup #search-users-entry': 'onSearchUsers'
},
onSearchUsers: function(e){
var searchTerm = ($(e.currentTarget).val()).toLowerCase()
var results = this.allUsers.filter(function(user){
var firstName = user.attributes.firstname.toLowerCase();
return firstName.match(new RegExp(searchTerm))
});
this.foundUsers.set(results); //the collectionview will update with the collection
},
onRender: function(){
this.resultsRegion.show(new SearchResultsView({
collection: this.foundUsers
});
}
});
I think the most important thing for you to take note of is how CollectionView leverages the Backbone.Collection that you provide it. CollectionView will render out an itemView (of the class/type you give it) for each model that is in it's collection. If the Collection changes then the CollectionView will also change. You will notice that in the method onSearchUsers all you need to do is update that collection (using set). The CollectionView will be listening to that collection and update itself accordingly

low coupling: add a model to a collection of a different view

i'm building a Backbone/Marionette application to list different sets of cards. the layout has an ItemView on the left side including an input field to add a new set and a CompositeView on the right side to list the card sets.
Cards.module("Set.SideBar", function(SideBar, App) {
SideBar.SideBarView = Backbone.Marionette.ItemView.extend({
template: "#set-sideBar",
className: "well sidebar-nav",
ui: {
saveBtn: "a.saveSet",
setName: "input[type=text]"
},
events: {
"click .saveSet": "saveSet"
},
saveSet: function(ev) {
ev.preventDefault();
var newSetName = this.ui.setName.val().trim();
var newSet = new Cards.Entities.Set({ name: newSetName });
newSet.save();
// How to add the model to the collection?
}
});
});
i'm looking for the best way to add the newSet to the collection of the CompositeView below. is there any clean low coupling solution to deal with that? i'm quite new to backbone.js and can't imagine that this is something totally unordinary, but somehow i'm not able to find an answer to my question in the regarding docs - or just dont understand them.
Cards.module('Set.List', function(List, App) {
List.SetItemView = Backbone.Marionette.ItemView.extend({
tagName: "tr",
template: "#set-list-item"
});
List.SetView = Backbone.Marionette.CompositeView.extend({
tagName: "table",
className: "table table-bordered table-striped table-hover",
template: "#set-list",
itemView: List.SetItemView,
itemViewContainer: "tbody",
modelEvents: {
"change": "modelChanged"
},
initialize: function() {
this.collection.fetch();
}
});
});
thanks in advance for your help!
how i'm doing it now:
thanks for both answers, they were guiding me in the right direction. the collection.create hint was also very useful and solved another problem i was facing!
inside a Marionette.Controller i do something like this and simply share the collection reference:
var setLayout = new Cards.Set.Layout();
Cards.mainRegion.show(setLayout);
var sets = new Cards.Entities.SetCollection();
var listView = new Cards.Set.List.SetView({ collection: sets });
setLayout.listRegion.show(listView);
var sideBarView = new Cards.Set.SideBar.SideBarView({ collection: sets });
setLayout.sideBarRegion.show(sideBarView);
and the new model is simply added by collection.create instead of .save() and .add().
Backbone.Collection.add can be used to add a model to an existing backbone collection.
http://backbonejs.org/#Collection-add
Also, look in to Collection.Create - http://backbonejs.org/#Collection-create
If your model is being persisted, then immediately added to the collection, you can skip your model.save() then collection.add() and just use collection.create(model)
Edit: And as already mentioned, make the collection instance visible from the sidebar view
To keep views decoupled, you can raise events from one view that other view(s) can listen to and handle however they please.

Resources