Populate Backbone.js JSON response into nested collections inside nested collections/models - javascript

My problem is that I am just starting out with Backbone.js and are having trouble wrapping my head around a complex problem. I want to save a form that have infinite fields, and some of the fields also needs to have infinite options. I'm just worried I might have started at the wrong end with a JSON response, instead of building the models/collections first. Here is a short pseudocode of what I try to achieve.
id:
parent: <blockid>
fields: array(
id:
title:
helpertext
options: array(
id:
type:
value:
)
)
Currently I am working with a faked JSON response from the server, which I built from scratch, and now I want to divide it into models and collections on the client side.
//Fake a server response
var JSONresponse = {
"formid":"1",
"fields":[
{
"fieldid":"1",
"title":"Empty title",
"helper":"Helper text",
"type":"radio",
"options":[
{
"optionid":"1",
"value":"Empty option.."
},
{
"optionid":"2",
"value":"Empty option.."
}
]
},
{
// fieldid2
}
]
};
The idea is to add fields as I see fit, and then if the field type is radio/checkbox/ul/ol there must also be an "options" array within the field.
My work so far:
var app = {};
app.Models = {};
app.Collections = {};
app.View = {};
app.Models.Option = Backbone.Model.extend({
});
app.Collections.Options = Backbone.Collection.extend({
model: app.Models.Option
});
app.Models.Field = Backbone.Model.extend({
options: new app.Collections.Options()
});
app.Collections.Fields = Backbone.Collection.extend({
model: app.Models.Field
});
app.Models.Form = Backbone.Model.extend({
formid : "1",
fields: new app.Collections.Fields(),
initialize: function() {
}
});
How do I split up my JSON response into all these models and collections?
(Perhaps I should re-evaluate my approach, and go for something like form.fieldList and form.optionList[fieldListId] instead. If so, how would that look like?)
Edit: Here is a little jsfiddle after many fixes, but I still don't really know how to make the inner options list work.

The easiest solution would be using Backbone Relational or Backbone Associations.
The documentation should be enough to help you get started.
If you don't want to use a library you could override the parse function on the Form model.
app.Models.Form = Backbone.Model.extend({
defaults: {
fields: new app.Collections.Fields()
},
parse: function(response, options) {
return {
formid: response.formid,
fields: new app.Collections.Fields(_.map(response.fields, function(field) {
if (field.options) {
field.options = new app.Collections.Options(field.options);
}
return field;
}))
};
}
});
Now if you fetch a form from the server, the response will be parsed into an object graph of models and collections.
form.get('fields') will return an app.Collections.Fields collection. form.get('fields').first().get('options') will return an app.Collections.Options collection, if any options exist.
Also, you could create the form model like this:
var form = new app.Models.Form(JSONresponse, {
parse: true
});
This would result in the same object structure.

It's quite hard to handle the case of nested models and collections right in plain Backbone.
Easiest way of handling this will be something like this:
var Option = Nested.Model.extend({
idAttribute : 'optionid',
defaults : {
optionid : Integer
value : ""
}
});
var Field = Nested.Model.extend({
idAttribute : 'fieldid',
defaults : {
fieldid : Integer,
title : "",
helper : "",
type : "radio",
options : Option.Collection
}
});
var Form = Nested.Model.extend({
idAttribute : 'formid',
defaults : {
formid: Integer,
fields: Field.Collection
});
https://github.com/Volicon/backbone.nestedTypes
And that's it. Yep, you'll get direct access to the attributes as free bonus, just form.fields.first().options.first().value, without that get and set garbage.

Related

Backbone save with LocalStorage with fetched DB data?

UPDATE
Ok, so I am still getting into the world of Backbone. But I have an issue with adding in localStorage into the Model.
So this is sorta off working, but I do not know what is going on?
var Model = Backbone.Model.extend({
//localStorage: new Backbone.LocalStorage("SomeCollection"),
url: "/GetMyData",
defaults: {
"id": "",
"datatest1": "",
"test2": ""
}
});
var NewTest = new Model();
NewTest.fetch();
console.log ( NewTest ); //This as DB data
console.log ( NewTest.attributes ); //This is empty
How can the 'attributes' on the 1st console.log contain db data and then the second one be completely empty? The 1st console.log is also empty, if I uncomment the localStorage, so I am guesting I doing something wrong?
Ok, I am not sure what I am doing wrong, but I am using Backbone and on a fetch call, save that id using the LocalStorage plugin. Now I have the fetch call and LocalStorage working apart but can not get them working together.
So my LocalStorage Code
var Model = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("SomeCollection"),
defaults: {
"id": "",
"datatest1": "",
"test2": ""
}
});
var NewTest2 = new Model();
NewTest2.set({
"id": "99",
"datatest1": "TEST-1-Q22",
"test2": "TEST-2-CL22"
});
NewTest2.save();
So this works, no problems with that. Now I add in a collection, and fetch data form my database. So my fetch code, with collection
var Model = Backbone.Model.extend({
defaults: {
"id": "",
"datatest1": "",
"test2": ""
}
});
var Col = Backbone.Collection.extend({
model: Model,
url: "/GetMyData"
});
var NewTest3 = new Col();
NewTest3.fetch();
console.log( NewTest3 );
I should also say that I am using PHP Slim as the base. The /GetMyData path gets the data from a MySQL database via a PDO connection, which I then convert into a JSON object for Backbone.
Which I assume is good, as that all works, the console.log's list of attributes displays the right data form my DB.
Now when I put the two together, I can not seem to get it to work.
var Model = Backbone.Model.extend({
defaults: {
"id": "",
"datatest1": "",
"test2": ""
}
});
var Col = Backbone.Collection.extend({
localStorage: new Backbone.LocalStorage("SomeCollection2"),
model: Model,
url: "/GetMyData"
});
var NewTest4 = new Col();
NewTest4.fetch();
console.log( NewTest4.save() );
This console.log returns, Uncaught TypeError: undefined is not a function. So I am not sure why? When I set the data in my 1st test, it works fine. Now I have also tried moving the localStorge var into the model but with the same effect.
The main aim for doing this is so I can log all the data coming from the server. When I set a few different data tests, I very much like the way in which this plugin saved the data.
Thanks.
*Please note, I am dyslexic, so I may not have explained myself right, please tell me if there is anything I can re-word to explain myself better. Thank you for your understanding.

backbone populate collection from external json

Below is the current code structure I have in place for a collection that I have manually constructed. I have a json file on my server which I am now trying to load in and basically remove the manual one and construct a collection based on that data. Was wondering what would I possibly need to change below to my code to help accommodate this.
var Game = Backbone.Model.extend({
defaults: {
name: 'John Doe',
age: 30,
occupation: 'worker'
}
});
var GameCollection = Backbone.Collection.extend({
model: Game,
url: 'path/to/json',
parse: function(response) {
return response;
}
});
var GamesView = Backbone.View.extend({
tagName: 'ul',
render: function() {
//filter through all items in a collection
this.collection.each(function(game){
var gameView = new GameView({model: game});
this.$el.append(gameView.render().el);
}, this)
return this;
}
});
var GameView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#gameTemplate').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var gameCollection = new GameCollection([
{
name: 'John Doe',
age: 30,
occupation: 'worker'
},
{
name: 'John Doe',
age: 30,
occupation: 'worker'
},
{
name: 'John Doe',
age: 30,
occupation: 'worker'
}
]);
var gamesView = new GamesView({collection: gameCollection});
$(document.body).append(gamesView.render().el);
This is one of the many things to love about Backbone. I don't know what you are using for your backend, but you state that you have a json file on your server, hopefully a json file full of the models that should be in your collection. And now here is the magic code (drumroll please..):
var GameCollection = Backbone.Collection.extend({
model: Game,
url: 'path/to/json/on/external/server',
});
var gameCollection = new GameCollection();
gameCollection.fetch();
Not much to it, right? Of course there are several options you can add or change to a fetch, so check out the docs here: http://backbonejs.org/#Collection-fetch. Backbone uses jQuery.ajax() be default, so check out the docs here to see all of the options: http://api.jquery.com/jQuery.ajax/
You shouldn't need the custom parse in your collection unless your models on the server don't match your backbone models.
Things to know:
fetch is asynchronous. It takes time to talk to the server, and the rest of your javascript will move on and complete. You will probably need to at least add a callback function to the success option, which will be called when fetch is finished, and it is good to add something to error as well, in case something goes wrong. You can add data as a query string so that your backend can use it using the data option, the data has to be an object. Here is an example:
gameCollection.fetch({
data: {collection_id: 25},
success: function(){
renderCollection(); // some callback to do stuff with the collection you made
},
error: function(){
alert("Oh noes! Something went wrong!")
}
});
fetch should receive data as JSON, so your url should either exclusive return JSON or be set up to detect an AJAX request and respond to it with JSON.
Firstly you need to fetch it from server as RustyToms said. And the other consideration is how to force the collection view to render itself again once data collected from server, as muistooshort commented.
If you manipulating fetch or sync you'll need to do it multiple times when there are more than one collection in app.
Doing such is native with Marionette, but in plain Backbone you can mimic the method of Marionette's CollectionView and do such:
//For the collection view
var GamesView = Backbone.View.extend({
initialize: function({
this.listenTo(this.collection, 'reset', this.render, this);
});
// Others
});
Then, when collection data fetched from server, the collection will trigger a reset event, the collection view noticed this event and render itself again.
For more than one collections, you can extract the code into a parent object in app and inherit from that.
var App.CollectionView = Backbone.View.extent({
initialize: //code as above
});
var GamesView = App.CollectionView.extend({
//Your code without initialize
});
I know this is a bit old at this point, but wanted to answer for anyone else stuck on this.
The code seems to come from the tutorial found here: http://codebeerstartups.com/2012/12/a-complete-guide-for-learning-backbone-js/
I too re-purposed the demo app found in that tutorial and had trouble rendering using external data.
The first thing is that the data itself needs to be converted to valid JSON or else you'll get a .parse() error.
SyntaxError: JSON.parse: expected property name or '}' at line 3 column 9 of the JSON data
or
error: SyntaxError: Unexpected token n
In your data source file, object properties need to be surrounded by quotes. It should look something like this:
[
{
"name": "John Doe",
"age": 30,
"occupation": "worker"
},
{
"name": "John Doe",
"age": 30,
"occupation": "worker"
},
{
"name": "John Doe",
"age": 30,
"occupation": "worker"
}
]
Secondly, once it's clear the external data is loading, we need to get it to render. I solved this (perhaps ungracefully) by moving the render() command into the success function of your gameCollection.fetch().
gameCollection.fetch({
success: function(collection, response, options) {
console.log('Success!! Yay!!');
$(document.body).append(gamesView.render().el);
},
error: function(collection, response, options) {
console.log('Oh, no!');
// Display some errors that might be useful
console.error('gameCollection.fetch error: ', options.errorThrown);
}
});
There are certainly better ways to accomplish this, but this method directly converts the code learned in the tutorial into something that works with external data.

Proper method for backbone-relational relationship definitions based on keys

I've been over the docs quite a few times, but this aspect still isn't clear to me. It's entirely possible that I'm thinking backbone-relational does something that it doesn't.
I'm looking for the way to define relationships based on key to avoid all the boilerplate fetching nonsense.
Take the canonical Artists and Albums example:
An artist has many albums as defined by album.artist_id
/api/artist/62351 might return
{
id: 62351,
name:"Jimi Hendrix"
}
similarly /api/album?artist_id=62351 might return
[
{
id:5678,
name: "Are You Experienced?"
artist_id: 62351
},
{
id: 4321,
name: "Axis: Bold as love",
artist_id: 62351
}
]
How might I define Artist and Album relationships such that
var hendrixInstance = new Artist({id:62351});
hendrixInstance.get('albums');
would fetch and return a collection of albums based on the album foreign_key artist_id? It must just be some permutation of key/keySource/keyDestination that I've yet to try, or be a problem that backbone-relational isn't trying to solve, but my doc groking has failed and I think a concise answer to this on SO might help future Googlers.
var Artist = Backbone.RelationalModel.extend({
urlRoot: '/api/artist',
relations:[{
key: 'albums', //Docs say this is the foreign key name, but in practice it doesn't appear that way. Need keySource/Destination?
type: Backbone.HasMany,
reverseRelation: {
key: 'artist',
type: Backbone.HasOne
}
}]
});
var Album = Backbone.RelationalModel.extend({
urlRoot: '/api/album'
});
Bonus points to an example model that references its self adjacency list style with parent_id
So, #xzhang's method above kept me iterating on this problem. First off, I'd love to be proven wrong on this, but I haven't found a way that backbone-relational handles this problem without additional custom code. Since this in my mind is an incredibly basic example of a OneToMany relationship, I'm still holding out hope that I'm just not getting something obvious.
Here's what I ended up doing to handle the situation. Unfortunately it still does not automatically fetch from the server when someobject.fetch('somerelationship') is called, which is what I really want. The parse function won't be necessary for most people, but it's required for the api I'm calling.
First I set up a base collection from which to extend:
var BaseCollection = Backbone.Collection.extend({
initialize: function(models, options) {
if (_.isObject(options.relation)) {
this.url = '/api/'
+ options.relation.keySource
+ '?search.'+options.relation.reverseRelation.keySource
+ '=' + options.foreignId;
}
},
parse: function(res) { return res.success ? res.list : res },
});
Then a reusable helper function (could probably be rolled into BaseCollection) to assist with creating relationships
function collectionOptions(instance) {
return {"relation":this, "foreignId":instance.get('id') };
};
And finally, those relationships are told to use BaseCollection as their CollectionType, and the collectionOptions() helper is assigned to set collectionOptions.
var Form = BaseModel.extend({
urlRoot: '/api/form',
relations:[
{
key: 'fills',
keySource: 'fill',
relatedModel: Fill,
type: Backbone.HasMany,
collectionOptions: collectionOptions,
collectionType: BaseCollection,
reverseRelation: {
key: 'form',
keySource: 'form_id',
type: Backbone.HasOne
}
},{
key: 'children',
keySource: 'form',
relatedModel: 'Form',
type: Backbone.HasMany,
collectionOptions: collectionOptions,
collectionType: BaseCollection,
reverseRelation: {
key: 'parent',
keySource: 'parent_id',
type: Backbone.HasOne
}
}
]
});
This allows me to avoid changing the server side API to return a list of ids and then individually fetch those ids. Instead I can just:
var form = new Form({id:1});
form.get('children').fetch();
form.toJSON(); //now has {id:1, ..., ..., children:[child,child,child,...]}
An extension to autoFetch children on the first call to .get('children') would be just the ticket, but I haven't discovered how to do that without modifying backbone-relational itself.
I am facing the exactly problem (Backbone-relational hasmany best practices), after 2 days research and look into the source code, I don't think key/keySource/keyDestination will do the work (correct me if I am wrong).
So I end up with create my own relation type, so far works fine. This may not a good solution, but hope can help you.
var LazyMany = Backbone.HasMany.extend({
setRelated: function (related) {
var relation = this.options
, instance = this.instance
;
if (related && !_.result(related, 'url')) {
related.url = relation.relatedModel.prototype.urlRoot +
'?' + relation.reverseRelation.key + '=' + instance.id;
}
return LazyMany.__super__.setRelated.apply(this, arguments);
}
});
Then in your model:
var Album = Backbone.RelationalModel.extend({
urlRoot: '/api/album/'
});
var Artist = Backbone.RelationalModel.extend({
urlRoot: '/api/artist/',
relations:[{
key: 'albums',
type: LazyMany,
includeInJSON: false,
relatedModel: Album,
reverseRelation: {
key: 'artist',
// I didn't test this, I don't have artist_id, artist is "id" in my app
keySource: 'artist_id',
keyDestination: 'artist_id',
includeInJSON: 'id'
}
}]
});
So if you don't define a collectionType or your collection don't have a url field, LazyMany will create a collection with url: /api/album/?artist=62351.
Then you just need fetch the collection: artist.get('albums').fetch().
Hope this can help, and I am still looking for better solutions.

Foreign key populated with an object

I would like to make a relation between two models User and Task using backbone-relational.
The relation between the two models is the following:
taskModel.creator_id = userModel.id
// TaskModel
var TaskModel = Backbone.RelationalModel.extend({
relations: [
{
type: Backbone.HasOne,
key: 'creator',
keySource: 'creator_id',
relatedModel: Users
}
],
// some code
});
// Task collection
var TaskCollection = Backbone.Collection.extend({
model: TaskModel,
// some code
});
// User Model
var User = Backbone.RelationalModel.extend({
// some code
});
Actually the problem is in the collection.models, please see the attached images:
Please check this jsfiddle: http://jsfiddle.net/2bsE9/5/
var user = new User(),
task = new Task(),
tasks = new Tasks();
task.fetch();
user.fetch();
tasks.fetch();
console.log(user.attributes, task.attributes, tasks.models);
P.S.:
Actually I am using requireJs to get the UserModel, so I cannot include quotes in relatedModel value.
define([
'models/user',
'backbone',
'relationalModel'
], function (User) {
"use strict";
var Task = Backbone.RelationalModel.extend({
relations: [
{
type: Backbone.HasOne,
key: 'creator',
keySource: 'creator_id',
relatedModel: User
}
],
});
);
Edit 2:
http://jsfiddle.net/2bsE9/13/
I updated the jsfiddle to reflect the changes I suggested below. As long as you are calling toJSON on your task, what gets to the server is a json object with the creator_id property set to the actual id of the user. The keyDestination here is redundant as the documentation states it is set automatically if you use keySource.
Edit:
https://github.com/PaulUithol/Backbone-relational#keysource
https://github.com/PaulUithol/Backbone-relational#keydestination
https://github.com/PaulUithol/Backbone-relational#includeinjson
The combination of the three above might solve your issue.
var Task = Backbone.RelationalModel.extend({
relations: [
{
type: Backbone.HasOne,
// The User object can be accessed under the property 'creator'
key: 'creator',
// The User object will be fetched using the value supplied under the property 'creator_id'
keySource: 'creator_id',
// The User object will be serialized to the property 'creator_id'
keyDestination: 'creator_id',
// Only the '_id' property of the User object will be serialized
includeInJSON: Backbone.Model.prototype.idAttribute,
relatedModel: User
}
],
});
The documentation also states that the property specified by keySource or keyDestination should not be used by your code. The property cannot be accessed as an attribute.
Please try this and comment if that fixes your issue.
Btw, here is a nice blog post that uses backbone-relational end to end.
http://antoviaque.org/docs/tutorials/backbone-relational-tutorial/
Edit
Updated jsfiddle
The problem is that Backbone-Relational explicitly deletes the keySource to 'prevent leaky abstractions'. It has a hardcoded call to unset on the attribute, in Backbone-Relational:
// Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
if ( this.key !== this.keySource ) {
this.instance.unset( this.keySource, { silent: true } );
}
You will need to overwrite the unset method in your Task model:
var Task = Backbone.RelationalModel.extend({
urlRoot: ' ',
relations: [
{
type: Backbone.HasOne,
key: 'creator',
relatedModel: User,
keySource: 'creator_id'
}
],
unset: function(attr, options) {
if (attr == 'creator_id') {
return false;
}
// Original unset from Backbone.Model:
(options || (options = {})).unset = true;
return this.set(attr, null, options);
},
sync: function (method, model, options) {
options.success({
id: 1,
name: 'barTask',
creator_id: 1
});
}
});
Obvious problems with this approach are that you will need to modify your code if either Backbone changes its Backbone.Model.unset method or Backbone-Relational changes its keySource behavior.

How I'll create a model from json file? (ExtJS)

This the model that I want to create using json file
Ext.define('Users', {
extend: 'Ext.data.Model',
fields: [{name: 'user_id', type: 'int'},
{name: 'user_name', type: 'string'}]
});
What do I have to do in order to automatically create this model, based on the content of a json response from the server?
In order to have the model created automatically, you need to include the metaData field with your Json data. metaData can be used to describe all of the fields for the Model.
In the ExtJS 4.1 documentation - Ext.data.reader.Json has a section called Response MetaData which describes basic use of this feature.
You should be able to pull down some json with fields and or some format that can be transformed into that format pretty easily.
Make call to service to get model's fields. Might need to define some chain that first calls model service and performs subsequent steps after.
Build model's field array w/ fields results from #1. May need to transform data based on response in #1.
var fields = response.fields;
Define model based on fields in Store's constructor
var store = Ext.create('Ext.data.Store', {
constructor: function () {
var model = Ext.define("Users", {
extend: "Ext.data.Model",
fields: fields
});
this.model = model.$className;
this.callParent(arguments);
}
});
I only use the jsonp, which loads an json file and parses it automatically, don't know if Ext.Ajax does this, too.
But you would do something like this:
definition.json:
{
"name": "User",
"fields": [
{ "name": "user_id" , "type": "int" },
{ "name": "user_name", "type": "string" }
]
}
load it:
Ext.Ajax.request({
url : "..../definition.json"
success: function( res ) {
Ext.define( res.name, {
extend: 'Ext.data.Model',
fields: res.fields
}, function() {
Ext.create( 'somestore', { model: res.name });
});
}
});

Categories