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"
}
}]
Related
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 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 have this model structure in my mind:
var app = app || {};
// Caratteristica
app.Attribute = Backbone.Model.extend({
defaults: {
name: '',
selected: false
}
});
app.Attributes = Backbone.Collection.extend({
model: app.Attribute
});
// Tipo Caratteristica
app.AttributeCategory = Backbone.Model.extend({
defaults: {
name: '',
attributes: new app.Attributes()
}
});
app.AttributeCategories = Backbone.Collection.extend({
model: app.AttributeCategory,
url: '/ajax/attributes.cfm'
});
My API in '/ajax/attributes.cfm' will give me a response like that:
[
{
"id": "1",
"name": "Type1",
"attributes": [
{
"id": "1",
"name": "Attribute1"
},
{
"id": "2",
"name": "Attribute2"
},
{
"id": "3",
"name": "Attribute3"
}
]
},
{
"id": "2",
"name": "Type2",
"attributes": [
{
"id": "1234",
"name": "Attribute1234"
},
{
"id": "2567",
"name": "Attribute2567"
}
]
}
]
My question is: will this json data be parsed correctly into my nested data structure?
I mean I want to end up having two app.AttributeCategory items in my app.AttributeCategories collection. Each of these two items must then have its attributes property filled with the corresponding app.Attributes collection.
If the answer was NO, how would I override the parse() function for achieving that result?
I did it like this:
// Tipo Caratteristica
app.AttributeCategory = Backbone.Model.extend({
defaults: {
name: ''
},
initialize: function(options) {
this.set('attributes', new app.Attributes(options.attributes));
Backbone.Model.prototype.apply(this, arguments);
}
});
But better use RationalModel for set up relations betweens models
You can create the collection inside an initialize method in your AttributeCategory model, like this:
app.AttributeCategory = Backbone.Model.extend({
...
initialize: function () {
this.set('attributes', new app.Attributes(this.get('attributes')));
}
});
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.
my combobox will always have only one record store assigned to it, that comes from the server like this way:
[
{
"consulta_id": 23752,
"convenio_id": 1,
"data": "2012-07-11",
"id": 36569,
"paciente": {
"nome": "Alvasole",
"id": 12
},
"tipo": 1
},
{
"consulta_id": 23753,
"convenio_id": 61,
"data": "2012-07-11",
"id": 36579,
"paciente": {
"nome": "Felintoi",
"id": 33
},
"tipo": 1
}
]
In this case, the store needs to be assigned to 'paciente' object (with id and nome).
I'm not getting this automatically to work.
This way works, but needs to be called manually:
setData: function(data) {
this.getStore().loadRawData(data.get('paciente'));
this.select(this.getStore().getAt(0));
},
One way to flatten out your Model is to use mapping attribute and use dot notation to get to the nested object's fields like this:
Ext.define('MyApp.model.Paciente', {
extend:'Ext.data.Model',
fields:[
{name:'id', mapping:'paciente.id'}
...
]
});