backbone how to handle same model with multiple views - javascript

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.

Related

How do I pass references of views/models to other views/models/routes in backbone.js

So I'm really new to backbone.js and I'm trying to understand the basic concept when it comes to getting parts(views/models/routes) to interact with other parts.
Here's an example. I have a 'screen' model object being rendered by a 'singleScreen' View to the page. I also have a 'sidebar' model and view being rendered. When i click on a link in the sidebar i want it to render a different screen model object and alter some of the HTML in a separate html div (the heading) according to the 'name' attribute i gave my screen model.
So first question, should all of the code for re-rendering a different view and changing the heading html be done in the routes.js file? It seems like that file would get pretty big pretty fast. If so how do i get a reference to the model object i want to render in the routes.js file so i can access things like myModel.name (which is instantiated in the app.js file)?
Do I handle the browser history and rendering of my view as separate things and add code for 'link is clicked, render this' functionality in my app.js file (the file where I instantiate all my objects)? If that's the case how does my app know what to render if a user tries to go directly to a view by typing in the URL, rather than clicking?
OR, and this is the most likely scenario as far as i can tell,
Do i use the initialize functions of my models/views to trigger/listenTo an event for when a link is clicked(or backbone.history() is changed?) and call render?
I've messed around with all 3 approaches but couldn't understand how to pass references of objects to other parts of the app, without just making those objects global variables (which feels so wrong).
For the last scenario, I messed around with events but everywhere I've read says you have to include a reference to the object that it's listening too, which seems to defeat the whole purpose of setting up an events object and listening for an event rather than just querying the state of a variable of that object...
eg.
this.listenTo(sidebarModel , "change:selected", this.render());
How do i pass a reference to sidebarModel object to the singleScreen view for it to know what it's meant to be listening to.
I'm not really looking for a coded answer, more so just an understanding of best practices and how things are meant to be done.I feel like I'm close but I know i'm missing/not understanding something which is why I'm not able to figure this out myself, so a little enlightening on the whole topic would be greatly appreciated. Thanks.
First, you need to understand the role of each Backbone classes. Reading on MVC first might help.
Model
The model isn't necessary when rendering a view. Its role is to handle the data, which can be local, or from an API. All the functions that affect the data should be in the model, and a model shouldn't have anything related to rendering.
There are always exception, where you could use a model to handle data related only to rendering, but you'll know when you find such case.
A simple example is a book:
// The Book model class
var Book = Backbone.Model.extend({
idAttribute: 'code',
urlRoot: '/api/book'
});
// a Book instance
var solaris = new Book({ code: "1083-lem-solaris" });
Fetching from an API would call:
// GET http://example.com/api/book/1083-lem-solaris
solaris.fetch(); // this is async
When fetching, the API returns the JSON encoded data.
{
"code": "1083-lem-solaris",
"title": "Test title",
}
The attributes are merged with the existing attributes, adding the ones that are not there yet, and overwriting the values of the ones already there.
Collection
A collection's role is to manage an array of models, which, again, can be local or fetched from an API. It should contain only functions related to managing the collection.
var Library = Backbone.Collection.extend({
model: Book,
url: '/api/book'
});
var myLibrary = new Library();
// just adds an existing book to our array
myLibrary.add(solaris);
You can fetch a collection to get an array of existing books from an API:
myLibrary.fetch();
The API should return:
[
{ "code": "blah-code-123", "title": "Test title" },
{ "code": "other-code-321", "title": "Other book title" }
]
Using the collection to create a new book and sync with the API:
var myNewBook = myLibrary.create({ title: "my new book" });
This will send a POST request with the attributes and the API should return:
{ "code": "new-code-123", "title": "my new book" },
View
The view handles its root DOM element. It should handle events from its DOM. It's best used to wrap small component and build bigger components from smaller components.
Put links directly in the templates, in the href of an anchor tag. There's no need to use events for that.
link`
Here's how I render (simplified) a list.
// book list item
var BookView = Backbone.View.extend({
tagName: 'li',
template: _.template('<%= title %>'),
initialize: function() {
// when the model is removed from the collection, remove the view.
this.listenTo(this.model, 'remove', this.remove);
},
render: function() {
this.$el.empty().append(this.template(this.model.toJSON()));
return this;
}
});
// book list
var LibraryView = Backbone.View.extend({
template: '<button type="button" class="lib-button">button</button><ul class="list"></ul>',
events: {
'click .lib-button': 'onLibButtonClick'
},
initialize: function(options) {
this.listenTo(this.collection, 'add', this.renderBook);
},
render: function() {
// this is a little more optimised than 'html()'
this.$el.empty().append(this.template);
// caching the list ul jQuery object
this.$list = this.$('.list');
this.collection.each(this.renderBook, this);
return this; // Chaining view calls, good convention http://backbonejs.org/#View-render
},
addItem: function(model) {
// Passing the model to the Book view
var view = new BookView({
model: model
});
this.$list.append(view.render().el);
},
onLibButtonClick: function() {
// whatever
}
});
Router
The router handle routes and should be as simple as possible to avoid it getting too big too fast. It can fetch collections and models, or the view can handle that, it's a matter of pattern at that point.
var LibraryRouter = Backbone.Router.extend({
routes: {
'*index': 'index',
'book/:code': 'bookRoute',
},
index: function() {
var library = new LibraryView({
el: 'body',
// hard-coded collection as a simple example
collection: new Library([
{ "code": "blah-code-123", "title": "Test title" },
{ "code": "other-code-321", "title": "Other book title" }
])
});
library.render();
},
bookRoute: function(code) {
var model = new Book({ code: code });
// here, I assume an API is available and I fetch the data
model.fetch({
success: function() {
var view = new BookPageView({
el: 'body',
model: model
});
view.render();
}
});
}
});
Events
Everything in Backbone has the Events mixin, even the global Backbone object. So every class can use listenTo to bind callbacks to events.
It would be very long to go in depth for everything in Backbone, so ask questions and I'll try to extend my answer.

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.js and Embedded One-To-Many Associations

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.

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

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