In BackboneJS, clarification on setting the attributes of models - javascript

Two common scenarios when I am using backbone backbone:
Attribute is listed as default value, then set
modelExample_A: Backbone.Model.extend({
defaults: {
whatever: 'foo'
something: 'blah'
}
});
viewExample_A: Backbone.View.extend({
//our view definition
});
var Example_A = new viewExample_A({
model: new modelExample_A()
})
Example_A.set({
'whatever': 'bar',
'something': 'weeeeeee',
});
Attribute is not listed as a default value, then set
modelExample_A: Backbone.Model.extend({
});
viewExample_A: Backbone.View.extend({
//our view definition
});
var Example_A = new viewExample_A({
model: new modelExample_A()
})
Example_A.set({
'whatever': 'bar',
'something': 'weeeeeee',
});
Attribute is not listed as a default value, set on creation
modelExample_A: Backbone.Model.extend({
});
viewExample_A: Backbone.View.extend({
//our view definition
});
var Example_A = new viewExample_A({
model: new modelExample_A({
'whatever': 'bar',
'something': 'weeeeeee',
})
})
But what about situations where I want to set a property of the model? I know this is generally discouraged, but sometimes in my code I like to make a not of a what model is the parent of the current model. This is something that almost certainly won't ever change, so there is no reason to put in the attribute for event listening/onChange purposes. Further, this is something without a default value (it can only get a value in context), so is it okay to just set it as a property of the model? Or will this cause problems down the line?
Setting a property instead of an attribute
modelExample_A: Backbone.Model.extend({
defaults: {
whatever: 'foo'
something: 'blah'
}
});
viewExample_A: Backbone.View.extend({
//our view definition
});
var Example_A = new viewExample_A({
model: new modelExample_A({
'whatever': 'bar',
'something': 'weeeeeee',
})
})
Example_A.parentModel = parentModelExample;

Used in moderation and with consideration, setting non-attribute properties on model instances is fine. Just be careful not to have this be data that can easily get into an inconsistent state, and if you are doing this a lot, that's a code smell. In that case, you may want to consider modeling some state as actual models with attributes, but just not persisting them (never call .save).

Related

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

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.

Merge attributes when using model.set with Backbone Associations

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

Backbone.js pass array to view

I want to pass an array to a view like this
this.account_nav = new AccountNav.View({
views: [
{ ref: new Member.Views.AccountNav({ model: this.model }), id: 'viewA' },
{ ref: new Member.Views.SettingsNav({ model: this.model}), id: 'viewB' }
]
});
However there is an error:
Uncaught Error: The argument associated with selector '' is defined
and a View. Set manage property to true for Backbone.View
instances. backbone.layoutmanager.js:208
pointing to
this.account_nav = new AccountNav.View({
Any ideas why I get this error?
If your View definition is created using Backbone.Layout.extend rather than Backbone.View.extend, this issue shouldn't arise.
To cite the Layout Manager documentation example:
var LoginView = Backbone.Layout.extend({
template: "#login-template"
});
versus
var LoginView = Backbone.View.extend({
// [...]
});
https://github.com/tbranyen/backbone.layoutmanager/wiki/Example-usage#structuring-a-view

backbonejs model.toJSON() can't get correct output, with backbone.iobind

backbone Model,board:
define([
'underscore',
'backbone',
'collections/lists',
'iobind',
'iosync'
], function( _, Backbone, Lists,ioBind,ioSync) {
var BoardModel = Backbone.Model.extend({
urlRoot: 'board',
noIoBind: false,
socket: io.connect(''),
idAttribute: '_id',
defaults: {
title: 'One Thousand and One Nights'
},
initialize: function() {
this.id = 1;
this.lists = new Lists;
this.socket.emit('joinBoard',this.id);
_.bindAll(this, 'getBoard');
this.ioBind('initBoard', this.getBoard, this);
},
getBoard: function(data){
this.set(data.data.board[0]);
}
});
return BoardModel;
});
backbone View: boardView:
var IndexView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing elements in the HTML.
el: '#board',
// Board template html
template: Mustache.render(Template.board),
events: {
},
initialize: function() {
//Init Data
this.model = new Board();
// var lists = {
// lists: [
// {name: "To Do",
// cards:[
// {name: "Art work for A."},
// {name: "B Prototype."},
// {name: "C prototype."}
// ]
// },
// {name: "Doing",
// cards: [
// {name: "Art work for A."}
// ]
// },
// {name: "Done"}
// ]
// }
// var partial = {card: Template.card_in_list};
// var listHtml = Mustache.render(Template.list,lists,partial);
// template = $(this.template).find('.list-area').append(listHtml);
},
render: function() {
console.log(this.model);
console.log(this.model.toJSON());
var partial = {card: Template.card_in_list};
var listHtml = Mustache.render(Template.list,this.model,partial);
template = $(this.template).find('.list-area').append(listHtml);
this.$el.html(template);
}
});
in View function: render function, the console.log get different result.
console.log(this.model) can get correct object result:
child
_callbacks: Object
_changing: false
_escapedAttributes: Object
_ioEvents: Object
_pending: Object
_previousAttributes: Object
_silent: Object
attributes: Object
__v: 0
_id: "50b750a7795f285d4e000014"
created: "2012-11-29T12:10:15.269Z"
description: "simple is better, but not simpler"
dueDate: "2012-11-29T12:10:15.269Z"
lists: Array[6]
status: true
title: "test board unique"
__proto__: Object
changed: Object
cid: "c1"
getBoard: function () { [native code] }
id: "50b750a7795f285d4e000014"
lists: child
__proto__: ctor
but this.model.toJSON() only get model default values:
Object
title: "One Thousand and One Nights"
__proto__: Object
it confuse me. anyone know why reason the same model get different result.
In a Backbone Model, your business values (description, title ...) are store in the attributes attribute. When you call toJSON() on your model, what it does is it takes the attributes values, and remove the Backbone.Model object framework's functions and attributes.
When you manually want to set model attributes, you want to use set. I don't know what is in you data.data object, so you should check the doc : http://backbonejs.org/#Model-set
set model.set(attributes, [options])
Set a hash of attributes (one or
many) on the model. If any of the attributes change the models state,
a "change" event will be triggered, unless {silent: true} is passed as
an option. Change events for specific attributes are also triggered,
and you can bind to those as well, for example: change:title, and
change:content. You may also pass individual keys and values.
note.set({title: "March 20", content: "In his eyes she eclipses..."});
book.set("title", "A Scandal in Bohemia"); If the model has a validate
method, it will be validated before the attributes are set, no changes
will occur if the validation fails, and set will return false.
Otherwise, set returns a reference to the model. You may also pass an
error callback in the options, which will be invoked instead of
triggering an "error" event, should validation fail. If {silent: true}
is passed as an option, the validation is deferred until the next
change.
I found i trigger boardView.render twice. when i change code:
a = new boardView;
a.render();
to
a = new boardView;
i got the thing done.
by the way thanks Marcel Falliere's comments.

Creating nested models in Backbone with Backbone-relational

I would like to use backbone-relational to have nested models in my backbone.js application.
I have been able to follow the examples in the documentation to create nested objects (e.g. one-to-many relations). However I don't understand how to bind the lower level elements in a way that will update the upper level objects. I think a working application would be a very helpful tutorial.
So my question is: How do I extend the Todos tutorial using backbone-relational so that:
one can add/remove subitems for each item
double clicking on any subitem edits it (just like the original Todo example)
clicking on an item hides/reveals its subitems
subitems are not fetched separately but are simply an array attribute of Todo items
Update: I have created a jsfiddle for this question. So far I have:
Imported the Todo example mentioned above
Created a TodoSubitem model and a TodoSubitemList collection
Altered the Todo model to extend RelationalModel instead of Model, with a HasMany relation to TodoSubitem
Added a subitem-template in the html code
But I'm still not sure how to:
add an input field for subitems that appears only when you click a Todo div
have subitem data as an attribute of Todo objects, but still have TodoSubitemView bind DOM elements to them (e.g. <li> tags).
I don't think I'd create a separate 'TodoSubItem' in this case - why not create a HasMany relation from Todo->Todo, so a Todo can have 0..* children, and 0..1 parent?
This way, you can re-use the order logic (if you change it to apply per collection), can create deeper nesting levels as desired (or limit that to a certain depth, if you want as well), etc. A number of things will need to be updated though, to accomodate this - for example, keep a list of child views so you can loop over them to mark each as done, and maintaining (and updating from) an ordering per TodoList.
Anyway, a rough outline of a possible solution to get you started, as a sort of diff with your current version (sorry, it's completely untested and could thus contain horrible mistakes):
//Our basic **Todo** model has `text`, `order`, and `done` attributes.
window.Todo = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'children',
relatedModel: 'Todo',
collectionType: 'TodoList',
reverseRelation: {
key: 'parent',
includeInJSON: 'id'
}
}],
initialize: function() {
if ( !this.get('order') && this.get( 'parent' ) ) {
this.set( { order: this.get( 'parent' ).nextChildIndex() } );
}
},
// Default attributes for a todo item.
defaults: function() {
return { done: false };
},
// Toggle the `done` state of this todo item.
toggle: function() {
this.save({done: !this.get("done")});
}
nextChildIndex: function() {
var children = this.get( 'children' );
return children && children.length || 0;
}
});
// The DOM element for a todo item...
window.TodoView = Backbone.View.extend({
//... is a list tag.
tagName: "li",
// Cache the template function for a single item.
template: _.template($('#item-template').html()),
// The DOM events specific to an item.
events: {
'click': 'toggleChildren',
'keypress input.add-child': 'addChild',
"click .check" : "toggleDone",
"dblclick div.todo-text" : "edit",
"click span.todo-destroy" : "clear",
"keypress .todo-input" : "updateOnEnter"
},
// The TodoView listens for changes to its model, re-rendering.
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
this.model.bind( 'update:children', this.renderChild );
this.model.bind( 'add:children', this.renderChild );
this.el = $( this.el );
this.childViews = {};
},
// Re-render the contents of the todo item.
render: function() {
this.el.html(this.template(this.model.toJSON()));
this.setText();
// Might want to add this to the template of course
this.el.append( '<ul>', { 'class': 'children' } ).append( '<input>', { type: 'text', 'class': 'add-child' } );
_.each( this.get( 'children' ), function( child ) {
this.renderChild( child );
}, this );
return this;
},
addChild: function( text) {
if ( e.keyCode == 13 ) {
var text = this.el.find( 'input.add-child' ).text();
var child = new Todo( { parent: this.model, text: text } );
}
},
renderChild: function( model ) {
var childView = new TodoView( { model: model } );
this.childViews[ model.cid ] = childView;
this.el.find( 'ul.children' ).append( childView.render() );
},
toggleChildren: function() {
$(this.el).find( 'ul.children' ).toggle();
},
// Toggle the `"done"` state of the model.
toggleDone: function() {
this.model.toggle();
_.each( this.childViews, function( child ) {
child.model.toggle();
});
},
clear: function() {
this.model.set( { parent: null } );
this.model.destroy();
}
// And so on...
});
I don't think you can make self-relating models in Backbone-relational (as described an the other answer here). When I have tried this, I get an error: Backbone-relational needs the relatedModel to be defined before it can create relationships with it.
So, I've modified the many-to-many pattern described on the backbone-relational page:
https://github.com/PaulUithol/Backbone-relational#many-to-many-relations
In essence, I am creating a linking model to contain references to the model being referred to, so that this link model can be available to Backbone-relational when it is defining the actual model.
I find it convenient to give this link model a separate relationship with both data models in the relationship, so that either can perform look relational look ups. Alternately, you could simply stuff the second model inside the link model, but then the relationship would be one directional unless you explicitly add your own references to the link model in the data model.
Let us create a 'Person' model that has children who are other 'Person' models.
Person = Backbone.RelationalModel.extend({
relations: [
{
type: 'HasMany',
key: 'Children',
relatedModel: 'FamilyRelation',
reverseRelation: {
key: 'Childrenof'
}
},
{
type: 'HasMany',
key: 'Parent',
relatedModel: 'FamilyRelation',
reverseRelation: {
key: 'Parentof'
}
}
]
});
FamilyRelation needs to be defined >before< Person is, so Backbone-relational can create the links, so this goes before the Person model definition in your code:
// FamilyRelation is link model between two "Person"s
// to achieve the Fan/Admiree relation.
FamilyRelation = Backbone.RelationalModel.extend({
})
If we create two "Person"s:
KingKong = new Person({name: 'KingKong'});
SonOfKong = new Person({name: 'SonOfKong'});
Then we can create a FamilyRelationship model that is the 'parentof' SonOfKong, and add it to KingKong's children with this line:
KingKong.get("children").add({"parentof":SonOfKong});
You can then add convenience functions to the Person model, to retrieve the nested models from the FamilyRelationship model, and don't really need to touch FamilyRelation any more, except to make sure it's being saved and retrieved appropriately.
For non-hierarchical relationships (say 'Friend', rather than 'Parent/Child', you still need these two relationships with the linking model in order to be able to retrieve one from the other, which is a bit of a hack, but it works.
After some fiddling I have found a way to create a true nested model:
var theModel = Backbone.RelationalModel.extend({ [...] });
theModel.prototype.relations.push({
type: Backbone.HasOne,
key: 'key',
relatedModel: theModel
});
At the point where the model is used (when pushing to the relations on the prototype) it is available, thus making everything work.
this post is pretty old by now, but I was searching for the same thing and thought I would share the solution I got.
To create a self-referencing model you simply omit relatedModel. So something like this:
Person = Backbone.RelationalModel.extend({
relations: [{
type: 'HasMany',
key: 'Children',
}]
})
It is explained in the docs

Categories