Backbone: Using a form to save model, as well as model relationship into the database - database

I have been struggling with a form in one of my Backbone views. This form is supposed to save the information for a project model (e.g. project name, project description, project members). While the project-specific information is saved without any issues into the corresponding database table, I did not manage to save the project-user relationships in a joint database table (projects_users, contains the corresponding ids for the two entities). The users that can be added to the project in the form are already present in the database, so nothing needs to be added into the users database table.
Could anyone put me on the right track here? I tried learning about relations in Backbone. These are two of the links that I have already looked into, but could not translate their content into a solution:
Backbone-relational
Model relationships in Rails and Backbone
Thank you,
Alexandra
EDIT
It was suggested that some code from my side would be useful. Since I do not have a good understanding of what I need to do, my code is pretty much a mess right now ... but let me try.
My form view:
App.Views.Projects.Common.Form = Backbone.View.extend({
...
submitted: function(formElement) {
var newData = this.serializeFormData(formElement);
this.model = new App.Models.Project({
name : newData.name,
description : newData.description
// Somehow put the users array associated with the project here ...
});
this.saveFormData(newData);
return false;
},
serializeFormData: function(formElement) {
var fields = formElement.serializeArray();
var serializedData = {};
$.each(fields, function(index, field) {
serializedData[field.name] = field.value;
});
return serializedData;
},
saveFormData: function(newData) {
var project = this.model;
// placeholder for the users that would be associated with the project
// parsing of the data from the form is required to get a corresponding array of user models
var users = App.users;
project.attributes.users = users;
// this line should save the project to the database table and the project-users relationships
// in the projects_users table; it needs the success and error functions
project.save({}, {});
},
...
})
For the project and user model files, I was thinking along these lines:
App.Models.Project = Backbone.RelationalModel.extend({
urlRoot: '/projects',
// Default attributes for the project.
defaults: {
description: "",
users: []
},
relations: [{
type : Backbone.HasMany,
key : 'users',
relatedModel : 'App.Models.User'
}]
});
App.Models.User = Backbone.RelationalModel.extend({
getId: function() {
return this.get('id');
},
getName: function() {
return this.get('name');
},
getEmail: function() {
return this.get('email');
}
});

Although the same information can be found as one of the comments to my question, I was asked to mark this as the answer, to make it easy for other people on StackOverflow. The solution that worked for me can be found here - see my own answer.

Related

backbone collection contains no models even though the models were already created

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

ExtJS 4 - Model containing other model without Id relation

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!

backbone.js with backgrid.js to populate JSON

I am using backgrid.js with backbone.js. I'm trying to populate JSON (user list) in backgrid. Below is my JSON,
[{"name": "kumnar", "emailId":"kumar#xxx.com",
"locations":{"name":"ABC Inc.,", "province":"CA"}
}]
I can access name & emailId as below,
var User = Backbone.Model.extend({});
var User = Backbone.Collection.extend({
model: User,
url: 'https://localhost:8181/server/rest/user',
});
var users = new User();
var columns = [{
name: "loginId",
label: "Name",
cell: "string"
}, {
name: "emailId",
label: "E-mail Id",
cell: "string"
}
];
var grid = new Backgrid.Grid({
columns: columns,
collection: users
});
$("#grid-result").append(grid.render().$el);
userEntities.fetch();
My question is, how do I add a column for showing locations.name?
I have specified locations.name in the name property of columns but it doesn't work.
{
name: "locations.name",
label: "E-mail Id",
cell: "string"
}
Thanks
Both backbone and backgrid currently don't offer any support for nested model attributes, although there are a number of tickets underway. To properly display the locations info, you can either turn the locations object into a string on the server and use a string cell in backgrid, or you can attempt to supply your own cell implementation for the locations column.
Also, you may try out backbone-deep-model as it seems to support the path syntax you are looking for. I haven't tried it before, but if it works, you can just create 2 string columns called location.name and location.province respectively.
It's really easy to extend Cell (or any of the existing extensions like StringCell). Here's a start for you:
var DeepstringCell = Backgrid.DeepstringCell = StringCell.extend({
render: function () {
this.$el.empty();
var modelDepth = this.column.get("name").split(".");
var lastValue = this.model;
for (var i = 0;i<modelDepth.length;i++) {
lastValue = lastValue.get(modelDepth[i]);
}
this.$el.text(this.formatter.fromRaw(lastValue));
this.delegateEvents();
return this;
},
});
In this example you'd use "deepstring" instead of "string" for your "cell" attribute of your column. Extend it further to use a different formatter (like EmailFormatter) if you want to reuse the built-in formatters along with the deep model support. That's what I've done and it works great. Even better is to override the Cell definitions to look for a "." in the name value and treat it as a deep model.
Mind you, this only works because I use backbone-relational which returns Model instances from "get" calls.

How to store a reference to a Collection in a Model in Backbone.js?

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');
}
});

Backbone-relational fetchRelated not sending request

I'm using backbone.js and backbone relational 0.5.0 with a Rails 3.2 backend. I have a Card model which has_many Notes.
Here are my JS models and collections:
Workflow.Collections.Cards = Backbone.Collection.extend({
model: Workflow.Models.Card,
url: '/cards'
});
Workflow.Models.Card = Backbone.RelationalModel.extend({
modelName : 'card',
urlRoot : '/cards',
relations: [
{
type: Backbone.HasMany,
key: 'notes',
relatedModel: 'Workflow.Models.Note',
collectionType: 'Workflow.Collections.Notes',
includeInJSON: false,
reverseRelation: {
key: 'card',
includeInJSON: 'id'
}
}]
});
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note,
url: '/cards/74/notes' // intentionally hard-coded for now
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : '/notes'
});
Normal fetching works great, but when I try fetchRelated in the console, I get an empty array:
card = new Workflow.Models.Card({id: 74}) // cool
card.fetch() // hits the sever with GET "/cards/74" - works great
card.fetchRelated('notes') // [] - didn't even try to hit the server
What's weird is that this works:
card.get('notes').fetch() // cool - GET "/cards/74/notes"
I could use that method and parse the response text, but it feels really dirty.
Anyone know what I'm missing here?
Thanks in advance, this one is really torturing me!
Stu
You should create Card with Note ids array: card = new Workflow.Models.Card({id: 74, notes: [74, 75]}); and change the url method of Notes accordingly:
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : function () {
return this.get('card').url() + '/notes';
}
});
card = new Workflow.Models.Card({id: 74, notes: [74, 75]});
card.fetchRelated('notes');
http://jsfiddle.net/theotheo/5DAzx/
I should have posted my solution a while back - there might well be a better way, but this is the convention I've gone with:
All of the following code is in the card view (which is where the notes are displayed).
First, I bind a renderNotes method to the 'reset' event on the card's notes collection:
initialize: function () {
_.bindAll(this);
this.model.get('notes').on('reset', this.renderNotes);
var self = this;
this.model.get('notes').on('add', function(addedNote, relatedCollection) {
self.renderNote(addedNote);
});
}
I also bind to the 'add' on that collection to call a singular renderNote.
The renderNotes and renderNote methods work like this:
renderNotes: function () {
if (this.model.get('notes')) {
this.model.get('notes').each(this.renderNote);
}
},
renderNote: function (note) {
var noteView = new Workflow.Views.Note({ model: note });
this.$('.notes').append(noteView.render().el);
},
Then, the last piece of the puzzle is to actually hit the server up for the card's notes (which will in turn fire the 'reset' event I bound to above). I do this in the card view's render method:
render: function () {
// render all of the eager-loaded things
this.model.get('notes').fetch();
return this;
},
As #user1248256 kindly helped me work out in the comments on my OP, the confusion was mainly in that I expected fetchRelated to pull down lazy-loaded records - that's actually not the case.
As a side-note, this view is actually a modal and be opened and closed (removed from the page). To prevent the zombie events problem described in this excellent post, I also manually unbind the events mentioned above.

Resources