Backbone.js and Embedded One-To-Many Associations - javascript

The App Layout
I am building an App, where one can create surveys. Every survey has multiple questions. I am embedding the questions into the survey model (with embeds_many in Mongoid), so a survey may look like this:
{
"id": "4f300a68115eed1ddf000004",
"title": "Example Survey",
"questions":
[
{
"id": "4f300a68115eed1ddf00000a",
"title": "Please describe your experience with backbone.js",
"type": "textarea"
},
{
"title": "Do you like it?",
"id": "4f300a68115eed1ddf00000b",
"type": "radiobutton",
"options": ["Yes", "Yes, a lot!"]
}
]
}
Now, there is also a survey editor which consists of a SurveyView, which displays the survey and lists the questions. If I click on a single question, a QuestionView will pop up, where I can edit the question. And when I am satisfied with my survey and I click save, the SurveyModel will be sent to the server.
The problem
What is the best way to handle the embedded association?
If I pass survey.get("questions")[any_index] to the QuestionView, and the question gets changed, I have to manually search for the question.id in my model and update my model. This feels wrong.
If I create a QuestionsCollection in my SurveyModel (is this even possible?). Then I can do things like fetching a Question out of this collection by id, pass it to the view and when I change the model, everything will get updated automatically, but I have to specify an url in the collection, and backbone will send single questions to the server, if things get updated.
Any suggestion on how to do this the backbone way?

The way to do embedded one-to-many associations in backbone.js
I have implemented the answer from #Sander and just wanted to post some code:
class Survey extends Backbone.Model
# Handles the Survey.questions association to parse stuff from the server
parse: (resp) ->
if #attributes?.questions?
#attributes.questions.reset(resp.questions)
else
resp.questions = new QuestionsCollection(resp.questions)
resp
# Recollects Survey.questions
toJSON: ->
attributes = _.clone(#attributes)
attributes.questions = attributes.questions.toJSON()
attributes
Then I can do stuff like:
survey = Survey.get("my-id")
survey.questions.at(0).title = "First question"
survey.questions.at(1).title = "Second question"
survey.save()
Which works quite comfortable.

you can most certainly have a questionsCollection inside your SurveyModel.
but you are right on the question being seen as a single question (since every question should have his own ID, it might still be possible to know on the server which survey it belongs to...)
then there is the parsing of your json:
if you are building your collection's and models manually you won't have this issue, but if you would add your nested JSON it will not automatically create a sub collection to your model. you would need to specify such things in an overridden parse method.

I think you can even do that at contruction
class Survey extends Backbone.Model
initialize: ->
#questions = new QuestionsCollection(#get('questions'))
Also you can extend model universally to get the nested data:
_.extend Backbone.Model::, deepToJSON: ->
obj = #toJSON()
_.each _.keys(obj), (key) ->
obj[key] = obj[key].deepToJSON() if _.isFunction(obj[key].deepToJSON)
obj
_.extend Backbone.Collection::, deepToJSON: ->
#map (model) ->
model.deepToJSON()

Overriding parse/toJSON is a workable solution. One gotcha to be aware of though is that "this" in the parse method is not the model object when the object is fetched via a collection. What happens then is that parse is invoked and the result is passed to initialize. The reason you may need "this" to point to the model object is if you want to bind events on the collection. An alternative approach is to override the set method instead. I put up a simple script on Github that showcases this approach.

Related

Optimal URL structure for Backbone.js and Backbone implementation

I'm developing a RESTful API for a Quiz app, which is going to be built with Backbone.js and Marionette. I'm quite new to backbone and was wondering what de best URL structure would be. I have the following resources:
Answer,
Question which contains Answers,
Question Group which contains Questions,
Quiz which contains Question Groups.
Two possible URL structures come to mind:
GET /quizzes/:id
GET /quizzes/:id/questiongroups
GET /quizzes/:id/questiongroups/:id
GET /quizzes/:id/questiongroups/:id/questions
GET /quizzes/:id/questiongroups/:id/questions/:id
GET /quizzes/:id/questiongroups/:id/questions/:id/answers
or:
GET /quizzes/:id
GET /quizzes/:id/questiongroups
GET /questiongroups/:id
GET /questiongroups/:id/questions
...
Now, I have been trying to use both of these options. With the first one, I can't figure out how to define the collections as a property of the parent models in Backbone so that I can use fetch() on them. The problem with the second option is a bit different: as I understand it, Backbone derives the url for a model from its collection, but the collection is a child of another resource, whereas the url for getting a single resource uses another collection, namely the global set of resources.
I'm pretty sure I'd have to override url() in both cases. I tried some things but didn't come up with anything useable at all. Also, I'd rather not override every single url()-model in the app, changing the API structure to suit the preferences of Backbone seems like a better option to me.
Any pointers as to what seems the right way to do it with Backbone would be great!
Thanks
If questiongroups can only appear in a single quiz, then the first option (the hierarchical one) is an obvious choice. To comply with RESTful conventions, you might want to consider using singular nouns instead: /quiz/:id/questiongroups/:id/question/:id/answer/:id
To solve your fetching problem, I would recommend using nested backbone models as per this answer: https://stackoverflow.com/a/9904874/1941552. I've also added a cheeky little parentModel attribute.
For example, your QuizModel could look something like this:
var Quiz = Backbone.Model.extend({
urlRoot: '/quiz/', // backbone appends the id automatically :)
defaults: {
title: 'My Quiz'
description: 'A quiz containing some question groups.'
},
model: {
questionGroups: QuestionGroups,
},
parse: function(response){
for(var key in this.model){
var embeddedClass = this.model[key];
var embeddedData = response[key];
response[key] = new embeddedClass(embeddedData, {
parse:true,
parentModel:this
});
}
return response;
}
});
Then, your QuestionGroups model could have the following url() function:
var QuestionGroups = Backbone.Model.extend({
// store metadata and each individual question group
url: function() {
return this.parentModel.url()+'/questiongroup/'+this.id;
}
});
Alternatively, if you don't need to store any metadata, you could use a Backbone.Collection:
var QuestionGroups = Backbone.Collection.extend({
model: QuestionGroup,
url: function() {
return this.parentModel.url()+'/questiongroup/'+this.id;
}
});
I'm afraid I haven't tested any of this, but I hope it can be useful anyway!

backbone how to handle same model with multiple views

edit: can I pass a parameter to a new view declaration ? so something like
new articleView({
template: "my desired template",
})
Suppose I have an array of objects, where each object represents a topic and contains a few properties: a title, a template type, and an array of articles. All the topics render nearly identical minus a few template differences.
I am using backbone and I have a general question: should each "topic" be a separate instance of the same collection type? Where would I declare the template type to be used for each topic? Should the collection have a variable template type property?
var topics = [
{
title: "Topic One",
template: "detailedView",
articles: [
{
title: "A very good article",
timestamp: "2013-01-24"
},
{
//more articles here
}
]
},
{
//another topic here...
}
];
To answer your first question, you can certainly pass parameters when instantiating a new view. The relevant part of the documentation reads as follows:
When creating a new View, the options you pass — after being merged
into any default options already present on the view — are attached to
the view as this.options for future reference.
So your template parameter would be available in your view instance like so:
var template = this.options.template;
To answer your general question, I think what you mean is should I define a single collection containing a separate instance of the same model type to represent each topic? In which case, based on your description of your data structure, I would suggest that this is a good way to go about it. The Topic model can certainly contain a property to identify its template.

Is it okay to delete attributes in my Backbone.Model's initialize method, and change them to properties of the model?

I have the following object relations between my three models (I am not using Backbone-relational... this is just describing the underlying structure of my data) :
Person has many Cars
Car has many Appraisals.
I have a single method to retrieve a Person, which brings along all the Cars and the Appraisals of those Cars. It looks like this:
{id: 1,
name: John Doe,
cars: [
{id: 3, make: 'Porsche',
appraisals: [
{id: 27, amount: '45000', date: '01/01/2011'}
]
},
{id: 4, make: 'Buick', appraisals: []}
]
}
When I create a new Person I pass in this entire mess. In my Person's initialize function I do this:
...
initialize: function() {
//Cars => Collection of Car
this.cars = new Cars();
_.each(this.get('cars'), function(car) {
this.cars.add(new Car(car));
});
this.unset('cars');
}
...
And in my Car initialize function I do something similar:
...
initialize: function() {
//Appraisals => Collection of Appraisal
this.appraisals = new Appraisals();
_.each(this.get('appraisals'), function(appraisal) {
this.appraisals.add(new Appraisal(appraisal));
});
this.unset('appraisals');
}
...
I also have to override the toJSON function for Person and Car models.
Is there anything wrong with this? I've seen it suggested elsewhere to make nested collections properties rather than attributes, and my experience confirms that is easier, but I'm wondering if I am breaking some rules or doing something dumb here.
I don't have the answer for 'storing nested collections as properties or as attributes' question, but I think you can simplify your code a bit initializing nested collections like this:
Person:
...
initialize: function() {
this.cars = new Cars(this.get('cars'));
this.unset('cars');
}
...
Car:
...
initialize: function() {
this.appraisals = new Appraisals(this.get('appraisals'));
this.unset('appraisals');
}
...
I answered a similar question here: backbone.js - getting extra data along with the request
In the answer that I provided, it was more about a collection owning a model association — a has one, basically.
I think a Person should have a CarsList containing Car models. A Car should have an AppraisalsList containing Appraisal models. You would probably override the parse and toJSON functions of Person and Car as needed.
I would definitely avoid using attributes for associations. The unset functions in the above examples are a bad smell to me.
If I may give my 2 cents worth of input(s):
If you were to draw an OOD class diagram of the classes and model the associations in any object-oriented language of your choice (other than javascript) how would you do it?
You see backbone.js helps put 'structure' to your javascript that could become an tangled spaghetti code. So if you Person has many Cars and a Car has many Appraisals you have two options: Compositions vs. Associations
Composition: What you are doing above: A person object is responsible for creating the cars and car objects for creating appraisals. The 'lifetime' of each object is dependent on the parent. Now that may/may not be how it 'should' be modeled, but that's the choice you've made.
Now, let's see simple associations. You create the person, cars, and appraisals independently (probably appraisal cannot exist without the car, but let's assume otherwise for now).
Now these objects are created but you need to "wire up" these associations - you can do that externally in a separate "initializer" class/container so to speak and just use setter/getters to connect them.
Conclusion: Use what best models your domain and don't let it be governed by your data store (i.e., the JSON object in this case). Backbone's sheer beauty comes from this ability of imparting classic OO structure to your code and thinking in that way when coding. So choose a good mix of OO relations (compositions, aggregations or simple associations) and select the 'best model' for your problem and implement accordingly.
Combining with #kulesa's suggestion, you'll "clean up" your code and achieve exactly what you want without worrying about breaking any principles/practices while organizing your code effectively.
Hope this helps!
I don’t personally think it makes sense to use properties to store some of a model’s data. What experiences did you have that made properties feel easier?
Backbone, internally, appears to use properties only for metadata (e.g. the by-id and by-cid maps of the models in a collection) and quick access to attributes (e.g. the id property, which is updated whenever the id attribute changes). Using properties also stops you from using Backbone’s event system or .get()/.set(), and forces you to override .toJSON().
Sticking with attributes, I believe that you could get the same result by overriding .set() — It gets called when a new instance of a model is created, before .initialize(), and it will also be called if something else tries to set any attribtues. I once did something similar like this:
var Person = Backbone.Model.extend({
set: function(attributes, options){
var outAttributes = {};
_.each(attributes, function(value, name){
switch(name){
case 'cars':
outAttributes.cars = new Cars(value);
break;
default:
outAttributes[name] = value;
}
}, this);
Backbone.Model.prototype.set.call(this, outAttributes, options);
}
});
…you could modify it to, say, update an existing Cars instance instead of creating a new one.
.set() is also where Backbone updates the value of the id property, so if you choose to use properties instead of attributes, it might still be the best to suck in the values (instead of .initialize()).
I had a similar situation and I solved it this way:
parse: function(data) {
if (data.Success) {
var policies = Rens.get('Policies').model, // model with nested collextion
claims = Rens.get('Claims').model; // model with nested collextion
// reseting collections
claims.claims.reset();
policies.policies.reset();
$(data.Result.Policies).each(function(i, policy) {
var claimsList = policy.ClaimsList,
policyWithClaims = _.clone(policy);
claims.claims.add(claimsList);
_.extend(policyWithClaims, {
ClaimsList: claims.getPolicyClaims.bind({
context: claims,
policyNumber: policy.PolicyNumber
}),
CarYearString: Rens.formatDate(policy.CarYear).date.HH,
PolicyEndDateString: Rens.formatDate(policy.PolicyEndDate).fullDate
});
policies.policies.add(policyWithClaims);
});
}
}
After this i have collection with policies and each policy has attribute with method linked to claims collection.
claims.getPolicyClaims returns all claims for current policy

Any recommendations for deep data structures with Backbone?

I've run into a headache with Backbone. I have a collection of specified records, which have subrecords, for example: surgeons have scheduled procedures, procedures have equipment, some equipment has consumable needs (gasses, liquids, etc). If I have a Backbone collection surgeons, then each surgeon has a model-- but his procedures and equipment and consumables will all be plain ol' Javascript arrays and objects after being unpacked from JSON.
I suppose I could, in the SurgeonsCollection, use the parse() to make new ProcedureCollections, and in turn make new EquipmentCollections, but after a while this is turning into a hairball. To make it sensible server-side there's a single point of contact that takes one surgeon and his stuff as a POST-- so propagating the 'set' on a ConsumableModel automagically to trigger a 'save' down the hierarchy also makes the whole hierarchical approach fuzzy.
Has anyone else encountered a problem like this? How did you solve it?
This can be helpful in you case: https://github.com/PaulUithol/Backbone-relational
You specify the relations 1:1, 1:n, n:n and it will parse the JSON accordingly. It also create a global store to keep track of all records.
So, one way I solved this problem is by doing the following:
Have all models inherit from a custom BaseModel and put the following function in BaseModel:
convertToModel: function(dataType, modelType) {
if (this.get(dataType)) {
var map = { };
map[dataType] = new modelType(this.get(dataType));
this.set(map);
}
}
Override Backbone.sync and at first let the Model serialize as it normally would:
model.set(response, { silent: true });
Then check to see if the model has an onUpdate function:
if (model.onUpdate) {
model.onUpdate();
}
Then, whenever you have a model that you want to generate submodels and subcollections, implement onUpdate in the model with something like this:
onUpdate: function() {
this.convertToModel('nameOfAttribute1', SomeCustomModel1);
this.convertToModel('nameOfAttribute2', SomeCustomModel2);
}
I would separate out the different surgeons, procedures, equipment, etc. as different resources in your web service. If you only need to update the equipment for a particular procedure, you can update that one procedure.
Also, if you didn't always need all the information, I would also lazy-load data as needed, but send down fully-populated objects where needed to increase performance.

Persisting & loading metadata in a backbone.js collection

I have a situation using backbone.js where I have a collection of models, and some additional information about the models. For example, imagine that I'm returning a list of amounts: they have a quantity associated with each model. Assume now that the unit for each of the amounts is always the same: say quarts. Then the json object I get back from my service might be something like:
{
dataPoints: [
{quantity: 5 },
{quantity: 10 },
...
],
unit : quarts
}
Now backbone collections have no real mechanism for natively associating this meta-data with the collection, but it was suggested to me in this question: Setting attributes on a collection - backbone js that I can extend the collection with a .meta(property, [value]) style function - which is a great solution. However, naturally it follows that we'd like to be able to cleanly retrieve this data from a json response like the one we have above.
Backbone.js gives us the parse(response) function, which allows us to specify where to extract the collection's list of models from in combination with the url attribute. There is no way that I'm aware of, however, to make a more intelligent function without overloading fetch() which would remove the partial functionality that is already available.
My question is this: is there a better option than overloading fetch() (and trying it to call it's superclass implementation) to achieve what I want to achieve?
Thanks
Personally, I would wrap the Collection inside another Model, and then override parse, like so:
var DataPointsCollection = Backbone.Collection.extend({ /* etc etc */ });
var CollectionContainer = Backbone.Model.extend({
defaults: {
dataPoints: new DataPointsCollection(),
unit: "quarts"
},
parse: function(obj) {
// update the inner collection
this.get("dataPoints").refresh(obj.dataPoints);
// this mightn't be necessary
delete obj.dataPoints;
return obj;
}
});
The Collection.refresh() call updates the model with new values. Passing in a custom meta value to the Collection as previously suggested might stop you from being able to bind to those meta values.
This meta data does not belong on the collection. It belongs in the name or some other descriptor of the code. Your code should declaratively know that the collection it has is only full of quartz elements. It already does since the url points to quartz elements.
var quartzCollection = new FooCollection();
quartzCollection.url = quartzurl;
quartzCollection.fetch();
If you really need to get this data why don't you just call
_.uniq(quartzCollecion.pluck("unit"))[0];

Categories