I'm just starting with Backbone.js and I am having trouble with nested models and collections.
For this example, I only have a single endpoint, /vocabulary.json.
Here is a sample of what that will return:
[
{
"id": 1,
"words": [
{
"native": "hello",
"foreign": "hola"
},
{
"native": "yes",
"foreign": "si"
},
{
//... More words in this lesson
}
]
},
{
//... More lessons coming from this endpoint
}
]
It's basically of collection of lessons, and each lesson has a collection of vocabulary words.
How could I create a words collection without another url endpoint (required by collections, it seems)?
Here's what I have so far. Actually, this is a stripped down, basic version because everything I'm trying isn't working.
/entities/vocabulary.js
Entities.Vocabulary = Backbone.Model.extend({});
Entities.Vocabularies = Backbone.Collection.extend({
model: Entities.Vocabulary,
url: "/vocabulary.json"
});
// Here is where I am struggling
Entities.Vocabulary.Word = Backbone.Model.extend({
// what to do?
});
Entities.Vocabulary.Words = Backbone.Collection.extend({
// what to do?
// Need some method to go into the Entities.Vocabularies Collection, pluck a given id
// and return the "words" attribute as a new Collection to work from.
});
Perhaps, I am thinking about this completely wrong, but I am hoping I have explained my problem well enough to help you help me.
you are almost there. You can use parse method on the model where you can write up your logic of associating the words collection to the vocabulary model.. Something in these lines.
// This would be your main Model
// Set the idAttribute on it
// Use the parse method here which hits before initialize
// where you attach the words collection on each Vocabulary Model
Entities.Vocabulary = Backbone.Model.extend({
idAttribute : 'id',
parse: function (response) {
// If theresponse has wods in response
// attach it words collection to the Vocabulary Model
if (response.words) {
this.words = new Entities.Vocabulary.Words(response.words || null, {
parse: true
});
}
// Delete the words object from response as the collection is already
// created on the model
delete response.words;
return response;
}
});
// Collection of Vocabulary
Entities.Vocabularies = Backbone.Collection.extend({
model: Entities.Vocabulary,
url: "/vocabulary.json"
});
// This would be the model for Word inside a Specific Vocabulory
// Associate a idAttribute if it has one.
// Add a parse method if you have any other extra processing for this model
Entities.Vocabulary.Word = Backbone.Model.extend({
});
// Just a Collection of words for the vocabulory
Entities.Vocabulary.Words = Backbone.Collection.extend({
});
// Pass the object, and pass in the parse: true
// parameter so that parse is called before initialize
var vocabularies = new Entities.Vocabularies(navi, {
parse: true
});
// If you want to fetch a new collection again you would just do
//vocabularies.fetch({parse: true});
console.log(mainCollection);
So each model should have a words collection directly on the Vocabulary model.
Check Fiddle
Related
I'm really confused about something regarding collections and models in backbone. Since I'm learning backbone, I haven't been able to fully understand it. I'm working on a backbone project that displays the question with branching logic; the next question that appears is based on the answer to the current question. I created a collection and model that stores (if this is the proper terminology) the response which is then retrieved in a separate view (an email form view) where these responses are added to the message box. Here's my collection to the response.
App.Collections.ResponseCollection = Backbone.Collection.extend({
model: App.Models.ResponseModel
});
and this is the model:
App.Models.ResponseModel = Backbone.Model.extend({
idAttribute: "_id",
defaults: {
response: '',
answer: ''
}
});
which is created like this:
saveResponse: function (qModel, option) {
var qid = qModel.get('question_id');
var mod = this.response.find(function (model1) {
return model1.get('_id') == qModel.get('question_id');
});
if (typeof (mod) == "undefined") {
var responseModel = new App.Models.ResponseModel({
_id: qModel.get('question_id'),
response: qModel.get('question'),
answer: option
});
this.response.add(responseModel);
}
else {
mod.set('answer', option);
}
},
In the view which displays the questions, I instantiated the model and added the response to it which is then added to the collection. This works fine - the responses are added to the model and also it is added to the collection. Now, I want to retrieve this collection and fetch the response from each model so that it's added in the textarea. So, this is what I did in the email view..
var options = new App.Collections.ResponseCollection();
But when I print the collection in console, this is what I see - there's no model in the collection
child {length: 0, models: Array[0], _byId: Object, constructor: function, model: function…}
These data are not saved and retrieved from the database. These responses are added to the model only to retrieve it later to be added to the email form.
I'm confused why it doesn't show me any models in the collection
I'm new to Backbone and on fetching a collection, I can see the server return all 15 collections. The fetch success returns all 15 models in the response object but the collection object has only the last of the 15 models.
var BracketModel = Backbone.Model.extend({
defaults: {
id: '',
name: '',
title: ''
},
urlRoot: 'http://test.com/bracket/rest.php',
.....
}),
var BracketsCollection = Backbone.Collection.extend({
url: 'http://test.com/bracket/rest.php?op=list',
model: BracketModel,
}),
bracketCollection.fetch({
success: function (collection, response) {
// Collection.models only has one model, response has 15
var bracketsView = new BracketsView({collection: collection});
},
Try
var bracketsView = new BracketsView({collection: response});
Or
var bracketsView = new BracketsView({collection: collection.toJSON()});
I haven't tested it now, but if I remember well, both are equivalent.
The first parameter returns the collection object, which gives you access to different collection attributes. The second parameter returns 'an array containing the attributes hash of each model in the collection', which is likely the thing you are looking for.
The pattern that I usually go with for passing a collection to a view goes like this:
var bracketCollection = new BracketsCollection();
var view = new brackatsView({collection: bracketCollection});
brackCollection.fetch();
Then inside of your view's initialization method do this:
this.listenTo(this.collection, 'sync', this.render);
What this all is doing is creating your collection and your view, and then when you create the view you are telling it about the collection. Calling fetch on the collection is an asynchronous event that will fire a 'sync' even when it is done. The view will listen for this sync event, and when it happens will call the render function.
Given is a nested model structure like this:
Model Website
+ id
+ name
+ images[] // List of Image instances
Model Image
+ imageName
+ imageUrl
A serialised version of the response looks like:
{
"id": 4711,
"name": "Some name",
"images" [
{"imageName": "Beach", "imageUrl": "http://example.com/whatever.jpg"},
...
]
}
This nested model set is persisted in a document store and is returned on request by Website.id.
There is no by-id-relation to the nested list of images, as they are persisted as a list directly in the parent model. As far as I know, the classic relations in Ext.data.Model refer to the related models via a by-id-relation.
The question is: Is there any way that I can tell the parent model to use the Image model for each of the children in it's images list?
As a first step, you can make your images data to be loaded into the model by using a field type of auto:
Ext.define('My.Model', {
extend: 'Ext.data.Model'
,fields: [
{name: 'images', type: 'auto'}
// ... other fields
}
});
Then:
myModel.get('images');
Should return:
[
{"imageName": "Beach", "imageUrl": "http://example.com/whatever.jpg"},
...
]
From there, you should theoretically be able to implement a fully automatized solution to creates the models from this data, and -- the hardest part -- try to keep these created records and the children data in the parent model synchronized. But this is a very involved hack, and a lot of entry points in Ext code base have to be covered. As an illustration, I once tried to do that for "has one" relations, and that represent a lot of code. As a result, I never took the time to consolidate this code, and finally never used it.
I would rather advocate for a simple and local (to the model) solution. You can add a simple method to your model to get the images as records. For example:
Ext.define('My.Model', {
// ...
,getImages: function() {
var store = this.imageStore;
if (!store) {
store = new Ext.data.Store({
model: 'My.ImageModel'
,data: this.get('images') || []
});
this.imageStore = store;
}
return store;
}
});
Creating a store for the associated model will save you from having to play with the proxy and the reader. It also gives you an interface that is close to Ext's default one for associations.
If you need support for loading images more than once for the same parent record, you can hook on the field's convert method.
Finally, you may also need to handle client-side modifications of associated data, in order to be able to save them to the server. If your associated model allows it, you could simply use the children store's sync method (and don't forget to update the parent model's data in the sync callback!). But if your associated model isn't connected to an endpoint on the server-side, you should be able to hook on the serialize method to save the data in the associated store (as opposed to the one stored in the parent record, that won't get updated if you work with the associated store).
Here's a last example showing both:
Ext.define('My.Model', {
extend: 'Ext.data.Model'
,fields: [
{
name: 'images'
,type: 'auto'
// enables associated data update
,convert: function(data) {
var store = this.imageStore;
if (store) {
store.loadData(data || []);
}
return data;
}
// enables saving data from the associated store
,serialize: function(value, record) {
var store = record.imageStore,
if (store) {
// care, the proxy we want is the associated model's one
var writer = store.proxy && store.proxy.writer;
if (writer) {
return Ext.Array.map(store.getRange(), function(record) {
return writer.getRecordData(record);
});
} else {
// gross implementation, simply use the records data object
return Ext.pluck(store.getRange(), 'data');
}
} else {
return record.get('images');
}
}
}
// ... other fields
}
,getImages: function() {
var store = this.imageStore;
if (!store) {
store = new Ext.data.Store({
model: 'My.ImageModel'
,data: this.get('images') || []
});
this.imageStore = store;
}
return store;
}
});
Please notice that I haven't tested this code, so it might still contains some mistakes... But I hope it will be enough to give you the general idea!
I have a basic Backbone application which obtain an array of JSON objects from a remote service and displays them: all good so far. However, each JSON object has an array of tags and I want to display the tags in a separate area of the webpage.
My question is: what is the most Backbone-friendly way of doing this? I could parse the existing data again in a second view, which is cleaner but takes up more computation (processing the entire array twice).
An alternative is gathering up the tag information in the primary view as it is working through the array and then passing it along to the subsidiary view, but then I'm linking the views together.
Finally, I'd like to filter based on those tags (so the tags will become toggle buttons and turning those buttons on/off will filter the information in the primary view); does this make any difference to how this should be laid out?
Bonus points for code snippets.
Hm. I'm not sure if this is the Backbone-friendly way, but I'll put the logic to retrieve a list of tags (I think that's what you meant by "parse") in the collection.
Both the main view and the subview will "listen" to the same collection, and the subview will just call collection.getTags() to get a list of tags it needs.
// Model that represents the list data
var ListDataModel = Backbone.Model.extend({
defaults: function() {
return {
name: null,
tags: []
};
}
});
// Collection of list data
var ListDataCollection = Backbone.Collection.extend({
model: ListDataModel,
initialize: function() {
var me = this;
// Expires tag collection on reset/change
this.on('reset', this.expireTagCache, this);
this.on('change', this.expireTagCache, this);
},
/**
* Expires tag cache
* #private
*/
expireTagCache: function() {
this._cachedTags = null;
},
/**
* Retrieves an array of tags in collection
*
* #return {Array}
*/
getTags: function() {
if (this._cachedTags === null) {
this._cachedTags = _.union.apply(this, this.pluck('tags'));
}
return this._cachedTags;
},
sync: function(method, model, options) {
if (method === 'read') {
var me = this;
// Make an XHR request to get data for this demo
Backbone.ajax({
url: '/echo/json/',
method: 'POST',
data: {
// Feed mock data into JSFiddle's mock XHR response
json: JSON.stringify([
{ id: 1, name: 'one', tags: [ 'number', 'first', 'odd' ] },
{ id: 2, name: 'two', tags: [ 'number', 'even' ] },
{ id: 3, name: 'a', tags: [ 'alphabet', 'first' ] }
]),
},
success: function(resp) {
options.success(me, resp, options);
},
error: function() {
if (options.error) {
options.error();
}
}
});
}
else {
// Call the default sync method for other sync method
Backbone.Collection.prototype.sync.apply(this, arguments);
}
}
});
var listColl = new ListDataCollection();
listColl.fetch({
success: function() {
console.log(listColl.getTags());
}
});
I guess two reasons for handling this in the collection:
It keeps the View code cleaner (This is given that we are not doing very complex logic in the tag extraction - It's just a simple _.pluck() and _.union().
It has 0 business logic involved - It can arguably belong to the data layer.
To address the performance issue:
It does go through the collection twice - However, if the amont of data you are consuming is too much for the client to process even in this case, you may want to consider asking the Backend to provide an API endpoint for this. (Even 500 pieces of data with a total of 1000 tags shouldn't bee too much for a somewhat modern browser to handle nowadays.)
Hmm. Does this help?
JSFiddle to go with this with the collection and the model: http://jsfiddle.net/dashk/G8LaB/ (And, a log statement to demonstrate the result of .getTags()).
When creating new collection (Choices) I want to set a property on it (ex: _question) which links back to the containing Model (MultipleChoiceQuestion)
This took me quite a bit of time to figure out, so in case somebody in the future has this problems...here's the code I ended up writing.
I discovered, unlike Model, Collection's initialize() function accepts 2 parameters. The first is models (which is a list of models you can initialize the collection with). The second is options (what you want). For a while my Collection started out with 1 model inside and I couldn't figure out why. Turns out I was passing my options into the models field.
THE CONTAINING MODEL:
m.MultipleChoiceQuestion = Backbone.Model.extend({
initialize: function(){
//NULL as first parameter because we don't want models
this.choices = new c.Choices(null, {
_question: this //this is referring to current question
}); //choices Collection is this
}
});
THE COLLECTION
c.Choices = Backbone.Collection.extend({
initialize: function(models, options){
this._question = options._question;
},
model: m.Choice
});
I actually found that although my 1st answer technically works, there's a plugin that makes care of storing collections in models (and creating appropriate One->Many, One->One and Many->One relationships
https://github.com/PaulUithol/Backbone-relational
Using that plugin you store the parent question as an ATTRIBUTE
m.MultipleChoiceQuestion = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'choices', //says to store the collection in the choices attribute
relatedModel: m.Choice, //knows we are talking about the choices models
includeInJSON: true, //when we do toJSON() on the Question we want to show serialize the choices fully
reverseRelation: {
key: 'question', //in the CHOICE object there will be an attribute called question which contains a reference to the question
includeInJSON: false //when we do .toJSON on a CHOICE we don't want to show the question object (or maybe we want to show the ID property in which case we set it to "id")
}
}],
coolFunction: function () {
this.get('choices').each(function(choice){
choice.doSomethingChoicey();
});
}
});
So now if we are in the choices model we can fully reference anything in the parent question:
m.Choice = m.RelationalModel.extend({
coolFunction: function(){
this.get('question').doSomemethingQuestiony();
var question_numer = this.get('question').get('question_number');
}
});