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.
Related
I dynamically create my store from a model relation and attach this store to a grid.
var map = record.getCommunity().mappings();
map.load({
url: '/myUrl/mappings',
scope: this,
callback: function(records, operation, success) {
map.group('type');
//ExtJS bug https://www.sencha.com/forum/showthread.php?265674
me.getOutgoingGrid().destroy();
childGrid = Ext.create('hds.view.outgoingGrid', {
store: map
});
me.getGridHolder().add(childGrid);
me.getOutgoingGrid().getSelectionModel().select(0, false) ;
}
});
When I want to create a new model instance and insert it into this dynamic store I get the following error:
Cannot read property 'isModel' of undefined
Here is the code that triggers the error:
var newMap = Ext.create('hds.model.MappingModel', {
indetifier : "something",
});
me.getOutgoingGrid().store.insert(0, newMap);
I cannot found the reason of this problem....Any ideas?
It is hard to know what is breaking your code but here are a couple of things:
1 - You need to define the model identifier in the class prototype not in the instance.
2 - You misspelled identifier
So your model should look like:
Ext.define('hds.model.MappingModel', {
identifier : "something",
});
var newMap = new hds.model.MappingModel({
//...your config here
});
The error that you are seeing Cannot read property 'isModel' of undefined is thrown when the store tries to check if model is an instance of Ext.data.Model but the model being passed is undefined.
This can happen for several reasons but usually it's because you are trying to create a model before the prototype has been defined or because there is a typo on your code.
Please, try creating a a fiddle (https://fiddle.sencha.com) reproducing this error or it will be very hard to help you.
Regards,
If there was an fiddle, It would be more helpful. As I understand from your question, you have some data and you can't set the data to store or model correctly. If you had defined your model or store before set to grid, there would not be a problem. I added a fiddle how model proxy works with store and etc. Hope it helps. If the fiddle does not explain your problem, please change the fiddle codes. So, we can understand what your problem exactly. Here is the fiddle: https://fiddle.sencha.com/#fiddle/tvq
Ext.define('model.Users', {
extend: 'Ext.data.Model',
fields: [
{ name: 'Name', type: 'string'},
{ name: 'City', type: 'string'},
{ name: 'Country', type: 'string'},
],
//idProperty: 'Name',
proxy: {
type: 'ajax',
rootProperty: 'records',
rootUrl: 'users', // used when updating proxy url
url: 'users',
reader: {
type: 'json',
rootProperty: 'records'
}
}
}); //model
var modelExt = Ext.create('model.Users', { Name: 'Ernst Handel'});
var storeExt = Ext.create('Ext.data.Store', {
requires: 'model.Users',
model: 'model.Users'
});
modelExt.load({
scope: this,
success: function(record) {
var colObjects = [];
Ext.each(Object.keys(record.data), function(key) {
colObjects.push({
text: key,
dataIndex: key
});
});
storeExt.loadData([record]);
//console.log(record, storeExt);
var grid = Ext.create('Ext.grid.Panel', {
store: storeExt,
columns: colObjects,
renderTo: Ext.getBody()
});
},
failure: function (err) {
}
});
Create model like this and then use.
var newMap = Ext.create('Ext.data.model', {
identifier : "something"
});
Add a new file in Model folder named like MappingModel.js and define model like this.
Ext.define('hds.model.MappingModel', {
extend: 'Ext.data.Model',
fields: [
'something'
]
});
Add this in app.js and then use MappingModel in your dynamic store.
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.
My problem is very specific to Backbone Associations (http://dhruvaray.github.io/backbone-associations). I'm wondering if it's possible to merge attributes when setting properties on nested models. Here is a reduction of the issue:
// define the Layout model
var Layout = Backbone.AssociatedModel.extend();
// define the User model, with layout as a Related model
var User = Backbone.AssociatedModel.extend({
relations: [
{
type: Backbone.One,
key: 'layout',
relatedModel: Layout
}
],
defaults: {
layout: {}
}
});
// create a new user
var user = new User({ user_name: 'pascalpp' });
// set a property on the layout model
user.set('layout.foo', 'bar');
user.get('layout.foo'); // returns 'bar'
// call set on the user directly, passing a JSON structure with no foo property
user.set({ layout: { 'baz': 'bing' } });
user.get('layout.foo'); // foo got wiped, so this returns undefined
The real-world scenario I'm facing is that we need to fetch partial data for a user and set that on the user model without obliterating previously set attributes that don't exist in the current fetch. So I'm hoping we can merge when setting attributes. Is this possible?
Backbone-associations updates existing nested model if id's of new and existing model match. If id's are undefined or they don't match, then the nested model gets replaced by a new one.
What I do for these singleton nested models is I introduce fake id=0 and then it works like expected.
Here is a working jsfiddle.
Working code:
// define the Layout model
var Layout = Backbone.AssociatedModel.extend({
defaults: {
id: 0
}
});
// define the User model, with layout as a Related model
var User = Backbone.AssociatedModel.extend({
relations: [
{
type: Backbone.One,
key: 'layout',
relatedModel: Layout
}
],
defaults: {
layout: {}
}
});
// create a new user
var user = new User({ user_name: 'pascalpp' });
// set a property on the layout model
user.set('layout.foo', 'bar');
user.get('layout.foo'); // returns 'bar'
// call set on the user directly, passing a JSON structure with no foo property
user.set({ layout: { id:0, 'baz': 'bing' } });
user.get('layout.foo'); // foo got wiped, so this returns undefined
alert(user.get('layout.foo'))
Model code:
App.Team = Backbone.RelationalModel.extend({
urlRoot: 'data/json/team',
/*urlRoot: 'data/json/myteam.txt',*/
idAttribute: 'id',
relations:
...
app.currentTeam = new App.Team({id:11});
View:
var trophiesBox = JST['teaminfo/history/leftbox'](app.currentTeam.attributes);
$("#teamhistory_leftbox").append(trophiesBox);
for (var i = 1; i <= app.currentTeam.attributes.history.length; i++) {
var historyData = app.currentTeam.attributes.history.get(i);
var historyRow = JST['teaminfo/history/row'] (historyData.attributes);
$("#teamhistory_table_body").append(historyRow);
}
I'm getting "Uncaught TypeError: Cannot read property 'attributes' of undefined"
on var historyRow = JST['teaminfo/history/row'] (historyData.attributes); line.
Before I had problems defining historyData, probably since it is a model in a collection (app.currentTeam.attributes.history) inside of another model (app.currentTeam). I was getting [Object] (app.currentTeam.attributes.history) doesn't have a 'get' method type of error. Now it passes fine, but I get another error message in the next line, so I wonder what is wrong with my code here.
app.currentTeam.attributes loads fine, so I guess there is a problem retrieving attributes of a model that is inside a collection within another model.
Edit: relation of Team and History collection:
{
type: Backbone.HasMany,
key: 'history',
relatedModel: 'App.HistoryItem',
collectionType: 'App.History',
reverseRelation: {
key: 'team',
includeInJSON: 'id',
}
}
You get Model from Collection from wrong method
app.currentTeam.attributes.history.get(i);
You have to use
app.currentTeam.get('history') // you got collection
app.currentTeam.get('history').at(i); // you got model from collection by index
Update1:
Try use iterator for get elements from collection:
var trophiesBox = JST['teaminfo/history/leftbox'](app.currentTeam.attributes);
$("#teamhistory_leftbox").append(trophiesBox);
app.currentTeam.get('history').each(function(historyData, i) {
var historyRow = JST['teaminfo/history/row'](historyData.attributes);
$("#teamhistory_table_body").append(historyRow);
}, this);
Have you checked Backbone Associations. This might be of some help.
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.