I am using the Backbone-nested plugin, declaring my Models as Backbone.NestedModel.extend() : https://github.com/afeld/backbone-nested
This allows me to retrieve data as such: this.model.get('tasks.title')
The problem I am having is rendering each of the nested items. I can output 1 using this.model.get('tasks[0].title').
However, if I write a loop to iterate over each of the nested, when return this; is called after the render() method for the $el to be appended back to the Collection $el, it fails.
Question
How can I write an efficient way for the nested elements to be rendered to the collection view?
Collection View
App.Views.Tasks = Backbone.View.extend({
el: '#taskList',
initialize: function() {
Event.on('tasks:show', this.show, this);
this.collection.on('add', this.addOne, this);
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(project) {
console.log(project.toJSON());
var taskView = new App.Views.Task({ model: project });
this.$el.append(taskView.render().el);
},
show: function(id) {
var project = this.collection.get(id);
var taskView = new App.Views.Task({ model: project });
this.$el.html(taskView.render().el);
}
});
The data (A project)
[
{
"_id": "51497f8dc5c3e3ec28ce4571",
"name": "First Project",
"tasks": [
{
"title": "Second",
"content": "Lots of content...",
"deadline": "1-1-2011",
"status": "in-progress",
"_id": "51497f8d726694b230000002"
},
{
"title": "Third",
"content": "Lots of content...",
"deadline": "1-1-2011",
"status": "in-progress",
"_id": "51497fa18aeb50c630000002"
}
]
}
]
I seem to come across this question several times about displaying a nested layout.
There is a JavaScript library that sits on top of Backbone.js that reduces the amount of boiler plate code you have to write and they have a CompositeView that works best with nested collections. I've included the documentation and an article that discusses using it.
Marionette CompositeView documentation
Marionette CompositeView Article
A similar question that I answered earlier
Hope this helps.
Related
This is my view (html):
<my-directive collection="currentData"></my-directive>
and this is the data structure:
$scope.currentData = [...
{
"name": "lala Page",
"icon": "icon-docs",
"sub_data": [
{
"name": "1-article",
"href": "/1-article",
"icon": "fa fa-pencil"
},
{
"name": "2-article",
"href": "/2-article",
"icon": "fa fa-pencil"
},
{
"name": "3-article",
"href": "/3-article",
"icon": "fa fa-pencil"
}
...
]...
}]
Inside my-directive there are bind-once elements (on sub_data).
If the all array change - the view is changed,
but when I change the sub_data, the view don't updated.
Any idea, how to make the collection be affected by a changes in sub_data?
(I do want to use as less watchers as possible)
Edit
Adding the my-directive code:
angular.module('my_module',[]).directive('myDirective', function(){
return {
scope:{
collection: '='
},
replace: true,
template: ['<li my-directive-item ng-repeat="item in collection" raw-model="{{::item}}">',
'</li>'].join('')
};
});
I don't have a watch on collection, only the code above. I meant angular doesn't update the collection binding unless I change the array itself, and I want it to update the view when i change a sub property of an item in the collection.
Ok, I solved it. I'm not proud of the solution but it works:
Inside the controller:
$scope.updateArray = function() {
...
// Do stuff with tempData
...
// Trick for updating the view - because of the bind-once sub items
$scope.currentData = [];
$timeout(function(){
$scope.currentData = tempData;
}, 0);
};
I have JSON response as follows
{
"results": [
{
"name": "FOO",
"containerName": "Foo",
"accounts": [
{
"id": "10445570_7601",
"shareeAccountInfo": "",
"siteAccountId": "271555",
"siteId": "271555",
"refreshMode": "NORMAL",
"isNetIncl": "true",
"propertyId": null,
"amount": [
"0.0",
"USD"
]
},
{
"id": "1070_20537601",
"shareeAccountInfo": "",
"siteAccountId": "271555",
"siteId": "271555",
"refreshMode": "NORMAL",
"isNetIncl": "true",
"propertyId": null,
"amount": [
"0.0",
"USD"
]
}
]
},
{
"name": "FOO123",
"containerName": "Foo123",
"accounts": [
{
"id": "10445570_20601",
"shareeAccountInfo": "",
"siteAccountId": "271555",
"siteId": "271555",
"refreshMode": "NORMAL",
"isNetIncl": "true",
"propertyId": null,
"amount": [
"0.0",
"USD"
]
},
{
"id": "10445570_37601",
"shareeAccountInfo": "",
"siteAccountId": "271555",
"siteId": "271555",
"refreshMode": "NORMAL",
"isNetIncl": "true",
"propertyId": null,
"amount": [
"0.0",
"USD"
]
}
]
},
{
"name": "FOO83838",
"containerName": "Foo3232",
"accounts": [
{
"id": "1601",
"shareeAccountInfo": "",
"siteAccountId": "271555",
"siteId": "271555",
"refreshMode": "NORMAL",
"isNetIncl": "true",
"propertyId": null,
"amount": [
"0.0",
"USD"
]
}
]
}
]
}
I am having issues creating a Backbone Model from this JSON response.
Should I be using a nested Model? and how should I be creating a collection based of my Model? Instead will it be a good idea to flatten this JSON structure? any ideas?
Your data structure naturally fits a Collection of Models (I'll call the model Group), where each Group contains a collection of Account models. This collection (and optionally its models) should have a reference to the parent Group.
var Account = Backbone.Model.extend({
})
var Accounts = Backbone.Collection.extend({
model: Account,
initialize: function(models, options) {
this.parent = options.parent;
}
});
var Group = Backbone.Model.extend({
initialize: function() {
this.accounts = new Accounts([], { parent: this });
}
});
var Groups = Backbone.Collection.extend({
model: Group,
// Assuming you make requests to `/group` to produce your result JSON
url: 'group',
// Construct models from the `results` attribute of the response
parse: function(response) {
return response.results;
}
});
There are two main implementation choices to make:
Persistence
If individual Accounts can be persisted seperately from the parent container, perhaps using an endpoint like /group/FOO83838/account/1601, the Acccount model can use the default Backbone.Model.save. The Accounts collection should override url to reference the parent URL:
Accounts = Backbone.Collection.extend({
// code from earlier
url: function() {
return this.parent.url() + '/account';
}
});
If accounts can only be saved as part of the overall Group model, you need to do two things:
First, override Account.save to delegate to the parent's save method:
Account = Backbone.Model.extend({
// code from earlier
save: function() {
this.collection.parent.save();
}
});
Second, override the Group.toJSON to include child accounts:
Group = Backbone.Model.extend({
// code from earlier
toJSON: function() {
var json = Backbone.Model.prototype.toJSON.call(this);
json.accounts = this.accounts.toJSON();
return json;
}
});
(In this example I have used the collection's parent reference. If you prefer you could also save a reference to the parent on this model).
Events
You could allow app code to directly listen to Group.accounts events, in which case no code changes are required:
// Example view code
this.listenTo(group.accounts, 'change', this.onAccountChange, this);
Or, if you prefer the extra encapsulation, you can forward child model changes:
Group = Backbone.Model.extend({
// code from earlier
initialize: function() {
this.accounts = new Accounts([], { parent: this });
this.listenTo(this.accounts, 'all', this.onChildEvent, this);
}
onChildEvent: function(eventName, model, options) {
// write logic to whitelist the events and parameters you are interested in
this.trigger('child:' + eventName, model, options);
}
});
// Example view code
this.listenTo(group, 'child:change', this.onAccountChange, this);
You could also look into Backbone extensions like DeepModel (no longer maintained) or Relational. I usually prefer the finer control of a custom implementation.
I'm working with nested models and collections in Backbone (Marionette).
// Basic unit
Models.User = Backbone.Model.extend({});
Models.Users = Backbone.Collection.extend({ model: Models.User });
// A Group has a collection of Users
Models.Group = Backbone.Model.extend({
initialize: function() {
var users = new Models.Users(this.get("users"));
this.set("users", users);
}
});
Models.Groups = Backbone.Collection.extend({ model: Models.Group });
// An Organization has a collection of Groups
Models.Organization = Backbone.Model.extend({
initialize: function() {
var groups = new Models.Groups(this.get("groups"));
this.set("groups", groups);
}
});
Models.Organizations = Backbone.Collection.extend({
model: Models.Organization,
url: "./data/data.json"
});
My understanding is that this.get will return an array of objects (as determined via the data.json file) and convert it to a Backbone Collection.
The data.json file has the following structure:
[{
"id": "org1",
"groups": [{
"id": "group1",
"users": [
{ "name": "Alice" },
{ "name": "Bob" }
]
},
{
"id": "group2",
"users": [{ "name": "Charlie" }]
}]
},
{
"id": "org2",
"groups": [{
"id": "groupA",
"users": [{ "name": "Eve" }]
},
{
"id": "groupB",
"users": [
{ "name": "Linda" },
{ "name": "Mallory" }
]
}]
}]
I'm trying to populate the top-most collection (an Organization) with the data from data.json.
In index.html, I have:
<script type="text/javascript">
$(document).ready(function() {
MyApp.OrgManager.addInitializer(function() {
var data = new MyApp.Models.Organizations();
data.fetch({
success: function(collection) {
console.log("Success", collection);
}
});
});
MyApp.start();
});
</script>
fetch returns successfully, but the output of my console for the collection is an empty array. What went wrong?
Solved it. Had to make sure that
I was running the page on a local webserver, since jQuery doesn't like null origin XMLHttpRequests, and
I had to _.bindAll a few things so that this had a proper context.
I'm new to backbone and trying to establish some good paradigms.
Right now, I'm working on a search heavy site. There are dozens of attributes to search on, many are min max type, but 6 or so are multi select. Prior to backbone, I was using something called listtree to make a collapsible listtree for the multiselect options. I'm still going to use those css classes, but now I'm trying to use backbone with models and views. TBH, this seems like more work than just using straight jquery, so maybe I'm missing something.
My question is, how should I structure the models and the views for several multiselect widgets in a treeview?
Here is the code I have so far:
<script type='text/template' id='listtree_bs'>
<div class="listtree">
<ul>
<% _.each(context, function(element, index){ %>
<li>
<span>
<input class="checkbox-listview-master" type="checkbox" value="<%= element.value %>"><%= element.name %><i class="glyphicon glyphicon-chevron-up"></i>
</span>
<ul style="display: none;">
<% _.each(element.items, function(childelement, index){ %>
<li>
<span>
<input class="checkbox-listview-master" type="checkbox" value="<%= childelement.value %>"><%= childelement.name %><i class="glyphicon glyphicon-chevron-up"></i>
</span>
</li>
<% }); %>
</ul>
</li>
<% }); %>
</ul>
</div>
</script>
var ListTreeModel = Backbone.Model.extend({
urlRoot: "/search/multiselect/",
idAttribute:'value',
});
var ListTreeModels = Backbone.Collection.extend({
model: ListTreeModel,
url: "/search/multiselect/",
parse: function (response) {
return response.data;
}
});
var listtreemodels = new ListTreeModels();
listtreemodels.fetch()
var ListTreeView = Backbone.View.extend({
events: {
"treechecked": "treechecked"
},
treechecked: function( e ){
console.log('triggered');
});
var listtreeview = new ListTreeView({el: $('#listtree_bs')});
The response.data from above looks kind of like this (I can easily change the backend though to facilitate the front end)
{
"data": [
{
"other": 0,
"values": [
{
"value": 1,
"key": "type (35513)"
}
],
"value": "type_of_code",
"key": "C Type",
"missing": 275793
},
{
"other": 25273,
"values": [
{
"value": 41,
"key": "United States of America (187293)"
}
],
"value": "primary_country_id",
"key": "Primary Country",
"missing": 3475
},
{
"other": 10958,
"values": [
{
"value": 623,
"key": "company 623 (12602)"
}
],
"value": "controller_id",
"key": "Search by Controller",
"missing": 248288
},
{
"other": 1294,
"values": [
{
"value": 6,
"key": "animal type (247267)"
},
{
"value": 7,
"key": "animal type y (23315)"
}
],
"value": "animal_id",
"key": "Animals",
"missing": 0
},
{
"other": 0,
"values": [
{
"value": 5,
"key": "Inactive (63693)"
},
{
"value": 1,
"key": "Active (825)"
}
],
"value": "current_status_code_table_id",
"key": "Current Status",
"missing": 109101
},
{
"other": 0,
"values": [
{
"value": 0,
"key": "stuff (275058)"
},
{
"value": 1,
"key": "more stuff (39860)"
},
{
"value": 2,
"key": "even more stuff (668)"
}
],
"value": "stuff_indicator",
"key": "Stuff Indicator",
"missing": 0
}
]
}
so right now, models are populated at the data level, but should they be populated at the nested level and manage this with some kind of one to many relationship?
What these multiselects do is fill out a search form that will get sent back to the server when the user hits search. Can I bind the above views to the model even if they are nested?
I'm trying backbone as an experiment here, but is this what it was really designed for? The search results are complicated and are sliced down in dozens of views. I was hoping to use backbone to keep the dom light and nimble. Right now it's getting bogged down in a lot of event call backs and just a lot of html.
Setting up a hierarchy of models is not that difficult : you just have to build your submodels when you parse the data. One way to do it is
var ListTreeModel = Backbone.Model.extend({
urlRoot: "/search/multiselect/",
idAttribute:'value',
constructor: function(data, opts) {
// force the parsing of the data
opts = _.extend({}, opts, {parse: true});
// setup the children collection
this.values = new ListTreeModels();
// call the parent constructor
Backbone.Model.call(this, data, opts);
},
parse: function(data) {
// populate the children
if (_.isArray(data.values))
this.values.set(data.values);
// remove the children from th emodel attributes
return _.omit(data, 'values');
}
});
var ListTreeModels = Backbone.Collection.extend({
model: ListTreeModel,
url: "/search/multiselect/",
parse: function (response) {
return response.data;
}
});
A demo showing the result http://jsfiddle.net/nikoshr/pZU5J/
Once your model structure is up and running, you can render your views (generate the associated HTML) and defined events. Here is a sample way to do it:
var ListTreeView = Backbone.View.extend({
'tagName': 'ul',
render: function () {
var $el = this.$el;
//create a view for each model
this.collection.each(function(model) {
var view = new ListItemView({
model: model
});
$el.append(view.render().el);
});
return this;
}
});
var ListItemView = Backbone.View.extend({
'tagName': 'li',
events: {
'click ': function(e) {
e.stopPropagation(); // avoid triggering an event on the parent level
console.log(this.model.get('value'));
}
},
render: function () {
//render the node
var template = _.template($('#listitem').html());
this.$el.html(template(this.model.toJSON()));
//and add a view for the sub collection
var subview = new ListTreeView({
collection: this.model.values
});
this.$el.append(subview.render().el);
return this;
}
});
with the listitem template defined as
<script type='text/template' id='listitem'>
<span>
<input class="checkbox-listview" type="checkbox" value="<%= value %>"> <%= key %>
</span>
</script>
And a demo http://jsfiddle.net/nikoshr/pZU5J/3/
Hierarchical views can be tricky to render, you probably will have to investigate further on the matter.
I'm reading a Backbone tutorial http://coenraets.org/blog/2011/12/backbone-js-wine-cellar-tutorial-part-1-getting-started/ on building a wine cellar application. The author of the tutorial doesn't explain one point clearly and I can't figure it out from the documentation. Namely, the use of this.model.models, which you see in the render function view below
window.WineListView = Backbone.View.extend({
tagName:'ul',
initialize:function () {
this.model.bind("reset", this.render, this);
},
render:function (eventName) {
_.each(this.model.models, function (wine) {
$(this.el).append(new WineListItemView({model:wine}).render().el);
}, this);
return this;
}
});
The model for this view is actually a collection
list:function () {
this.wineList = new WineCollection();
this.wineListView = new WineListView({model:this.wineList});
And the collection declares the Wine as its model
window.WineCollection = Backbone.Collection.extend({
model:Wine,
url:"../api/wines"
});
So, when WineListView is instantiated, it's this.model is actually the Wine List Collection. And, from the documentation, models provides access to an array of models inside a collection
modelscollection.models
Raw access to the JavaScript array of models inside of the collection. Usually you'll want to use get, at, or the Underscore methods to access model objects, but occasionally a direct reference to the array is desired.
So if this.model is already the collection of wines (due to the collection being declared as the model in the view), why is it necessary to do this.model.models? to essentially get the collection again?
It looks like this is essentially a stylistic choice. The code
_.each(this.model.models, function (wine) {
$(this.el).append(...);
}, this);
simply iterates through the models in the collection, and should be equivalent to:
this.model.each(function (wine) {
$(this.el).append(...);
}, this);
I would have thought the second version was easier to read, but each to his/her own...
In my case, to get it to work I had to do the following :
_.each(this.model.models, function (foto) {
console.log(foto.attributes);
$(this.el).append(new App.view.foto.foto({model:foto.attributes}).render().el);
}, this);
Not sure why accesing .attributes does the trick, am I missing any convertion?
I am bootstrapping from the DB the following JSON string:
[{
"fid": 1,
"imagen": "sample_colors.jpg",
"width": "110",
"height": "110",
"dimension_id": 1,
"seccion_id": 1,
"estado": 0,
"fecha": {
"date": "2012-06-27 23:02:27",
"timezone_type": 2,
"timezone": "PDT"
}
}, {
"fid": 2,
"imagen": "sample_colors.jpg",
"width": "110",
"height": "110",
"dimension_id": 1,
"seccion_id": 1,
"estado": 1,
"fecha": {
"date": "2012-06-27 12:03:02",
"timezone_type": 2,
"timezone": "PDT"
}
}, {
"fid": 3,
"imagen": "sample_colors.jpg",
"width": "110",
"height": "110",
"dimension_id": 1,
"seccion_id": 1,
"estado": 2,
"fecha": {
"date": "2012-06-27 12:03:20",
"timezone_type": 2,
"timezone": "PDT"
}
}]