Backbone.js get and set nested object attribute - javascript

I have a simple question about Backbone.js' get and set functions.
1) With the code below, how can I 'get' or 'set' obj1.myAttribute1 directly?
Another question:
2) In the Model, aside from the defaults object, where can/should I declare my model's other attributes, such that they can be accessed via Backbone's get and set methods?
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
}
})
var MyView = Backbone.View.extend({
myFunc: function(){
console.log(this.model.get("obj1"));
//returns the obj1 object
//but how do I get obj1.myAttribute1 directly so that it returns false?
}
});
I know I can do:
this.model.get("obj1").myAttribute1;
but is that good practice?

While this.model.get("obj1").myAttribute1 is fine, it's a bit problematic because then you might be tempted to do the same type of thing for set, i.e.
this.model.get("obj1").myAttribute1 = true;
But if you do this, you won't get the benefits of Backbone models for myAttribute1, like change events or validation.
A better solution would be to never nest POJSOs ("plain old JavaScript objects") in your models, and instead nest custom model classes. So it would look something like this:
var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});
var MyModel = Backbone.Model.extend({
initialize: function () {
this.set("obj1", new Obj());
}
});
Then the accessing code would be
var x = this.model.get("obj1").get("myAttribute1");
but more importantly the setting code would be
this.model.get("obj1").set({ myAttribute1: true });
which will fire appropriate change events and the like. Working example here: http://jsfiddle.net/g3U7j/

I created backbone-deep-model for this - just extend Backbone.DeepModel instead of Backbone.Model and you can then use paths to get/set nested model attributes. It maintains change events too.
model.bind('change:user.name.first', function(){...});
model.set({'user.name.first': 'Eric'});
model.get('user.name.first'); //Eric

Domenic's solution will work however each new MyModel will point to the same instance of Obj.
To avoid this, MyModel should look like:
var MyModel = Backbone.Model.extend({
initialize: function() {
myDefaults = {
obj1: new Obj()
}
this.set(myDefaults);
}
});
See c3rin's answer # https://stackoverflow.com/a/6364480/1072653 for a full explanation.

I use this approach.
If you have a Backbone model like this:
var nestedAttrModel = new Backbone.Model({
a: {b: 1, c: 2}
});
You can set the attribute "a.b" with:
var _a = _.omit(nestedAttrModel.get('a')); // from underscore.js
_a.b = 3;
nestedAttrModel.set('a', _a);
Now your model will have attributes like:
{a: {b: 3, c: 2}}
with the "change" event fired.

There is one solution nobody thought of yet which is lots to use. You indeed can't set nested attributes directly, unless you use a third party library which you probably don't want. However what you can do is make a clone of the original dictionary, set the nested property there and than set that whole dictionary. Piece of cake.
//How model.obj1 looks like
obj1: {
myAttribute1: false,
myAttribute2: true,
anotherNestedDict: {
myAttribute3: false
}
}
//Make a clone of it
var cloneOfObject1 = JSON.parse(JSON.stringify(this.model.get('obj1')));
//Let's day we want to change myAttribute1 to false and myAttribute3 to true
cloneOfObject1.myAttribute2 = false;
cloneOfObject1.anotherNestedDict.myAttribute3 = true;
//And now we set the whole dictionary
this.model.set('obj1', cloneOfObject1);
//Job done, happy birthday

I had the same problem #pagewil and #Benno had with #Domenic's solution. My answer was to instead write a simple sub-class of Backbone.Model that fixes the problem.
// Special model implementation that allows you to easily nest Backbone models as properties.
Backbone.NestedModel = Backbone.Model.extend({
// Define Backbone models that are present in properties
// Expected Format:
// [{key: 'courses', model: Course}]
models: [],
set: function(key, value, options) {
var attrs, attr, val;
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
_.each(this.models, function(item){
if (_.isObject(attrs[item.key])) {
attrs[item.key] = new item.model(attrs[item.key]);
}
},this);
return Backbone.Model.prototype.set.call(this, attrs, options);
}
});
var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});
var MyModel = Backbone.NestedModel.extend({
defaults: {
obj1: new Obj()
},
models: [{key: 'obj1', model: Obj}]
});
What NestedModel does for you is allow these to work (which is what happens when myModel gets set via JSON data):
var myModel = new MyModel();
myModel.set({ obj1: { myAttribute1: 'abc', myAttribute2: 'xyz' } });
myModel.set('obj1', { myAttribute1: 123, myAttribute2: 456 });
It would be easy to generate the models list automatically in initialize, but this solution was good enough for me.

Solution proposed by Domenic has some drawbacks. Say you want to listen to 'change' event. In that case 'initialize' method will not be fired and your custom value for attribute will be replaced with json object from server. In my project I faced with this problem. My solution to override 'set' method of Model:
set: function(key, val, options) {
if (typeof key === 'object') {
var attrs = key;
attrs.content = new module.BaseItem(attrs.content || {});
attrs.children = new module.MenuItems(attrs.children || []);
}
return Backbone.Model.prototype.set.call(this, key, val, options);
},

While in some cases using Backbone models instead of nested Object attributes makes sense as Domenic mentioned, in simpler cases you could create a setter function in the model:
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
},
setObj1Attribute: function(name, value) {
var obj1 = this.get('obj1');
obj1[name] = value;
this.set('obj1', obj1);
}
})

If you interact with backend, which requires object with nesting structure.
But with backbone more easy to work with linear structure.
backbone.linear can help you.

Related

Knockout mapping - difference between objects and observables in class being mapped to

This is driving me absolutely insane, and I just cannot see what I have done wrong. Please help before I start bibbling and gnawing my colleagues leg off. He doesn't deserve that.
I have an object I am mapping, which has a property that can or cannot contain an object of that same type. That is the only level of nesting there is. It is very simple; it is complicated only by the fact that the object is calling a base class constructor to set some default behaviour.
This base class sets up all the fields that can appear in the model (it is a generated file) and then maps the datasource, if it has one.
The mapping of the nested field to the correct constructor works if the field is set up initially as an observable. It does not if it is set up as a plain object.
var NS = {};
var _itest = 0;
NS.FieldModelBase = function(data, mapping)
{
var _this = this;
this.Text = ko.observable();
// DOES NOT WORK
this.AlternateField = {};
// WORKS
//this.AlternateField = ko.observable();
ko.mapping.fromJS(data, mapping, _this);
};
// =====================================================================
NS.FieldModel = function(data, mapping, parent)
{
var _this = this;
window.console && console.log('CREATING FIELD', data);
var _mapping = $.extend({}, mapping, {
'include': [ 'Test' ],
'AlternateField': {
create:
function(o)
{
window.console && console.log('FOUND SUBFIELD', o.data);
return o.data ? new NS.FieldModel(o.data) : null;
}
}
});
this.Test = ko.observable(_itest++);
NS.FieldModelBase.call(_this, data, _mapping);
}
// =====================================================================
var model = new NS.FieldModel({
Text: "Main option",
AlternateField: {
Text: "Alternate option",
AlternateField: null
}
}, { include: [ 'Test' ] });
ko.applyBindings(model);
https://jsfiddle.net/whelkaholism/fkr0w98u/
So, when setup as an object, printing model out after running the code gives:
{"Test":0,"Text":"Main option","AlternateField":{"Text":"Alternate option","AlternateField":null}}
There is no Test property on the alternate field. If you check the console, what happens is that the mapping create is in fact called, but the o.data property is null.
Change to an observable, and the output is, as expected:
{"Test":0,"Text":"Main option","AlternateField":{"Test":1,"Text":"Alternate option","AlternateField":null}}
So, what is the mapping plugin doing here? It was my understanding that it would map everything in source data, regardless of the existence or type of any existing properties on the object?
EDIT: I have solved my immediate problem with this change:
NS.FieldModel = function(data, mapping, parent)
{
var _this = this;
var _mapping = {
copy: [ 'AlternateField' ]
};
NS.FieldModelBase.call(_this, data, _mapping);
this.AlternateField = data.AlternateField ? new NS.FieldModel(data.AlternateField, null, _this) : null;
}
This manually creates the correct object type for the alternate field after the mapping. The copy directive in the mapping is absolutely required, or the newly created object has no properties mapped.
I don't now why this is, so I'm still looking for the answer on why the mapping plugin works differently depending on the content of pre-existing variables, because I despise having code that I don't know exactly why it works!
The mapping plugin maps only values, not objects e.g.
"myString" => ko.observable("myString")
null => ko.observable(null)
{ myStringProperty: "myString" } => { myStringProperty: ko.observable("myString") }
{ myProperty: null } => { myPropery: ko.observable(null) }
{} => {}

Match values in nested object to corresponding knockout bindings?

Let's say I have a list of knockout bindings placed in a nested/namespaced object, resembling this:
var bindings = {
event: {
eventid: ko.observable(),
office: ko.observable(),
employee: {
name: ko.observable(),
group: ko.observable()
}
},
...
}
Now let's say there are a number of different sets of data that might be loaded into this - so one does an ajax query and gets a JSON result like this:
{
"defaults": {
"event": {
"eventid": 1234,
"employee": {
"name": "John Smith"
}
},
...
}
}
Note that not every binding has a default value - but all defaults are mapped to a binding. What I want to do is read the defaults into whatever knockout binding they correspond to.
There are definitely ways to traverse a nested object and read its values. Adding an extra argument to that example, I can keep track of the default's full key (eg event.employee.name). Where I'm getting stumped is taking the default's key and using it to target the associated knockout binding. Obviously, even if i have key = "event.employee.name", bindings.key doesn't reference what I want. I can only think of using eval(), and that leaves me with a bad taste in my mouth.
How would one go about using a key to reference the same location in a different object? Perhaps knockout provides a way to auto-map an object to its bindings, and I've just overlooked it? Any insight would be helpful. Thanks in advance!
I would suggest you have a look at the Knockout Mapping Plugin which will do most of what you want to do. If that doesn't workout then you can turn your bindings object into a series of constructor functions that accepts a data parameter. Something like
var Employee = function (data){
var self = this;
self.name = ko.observbale(data.name || '');
self.group = ko.observable(data.group);
};
var Event = function(data){
var self = this;
self.eventid = ko.observable(data.id || 0);
self.office = ko.observable(data.office || '');
self.employee = ko.observable(new Employee(data.employee));
};
var bindings = function(data){
var self = this;
self.event = ko.observable(new Event(data));
}
I'll be putting Nathan Fisher's solution into a future update, but I wanted to share the fix I found for now as well. Each time the defaults object recurses, I simply pass the corresponding bindings object instead of tracking the entire keypath.
var setToDefaults = function(data){
loopDefaults(data.defaults, bindings);
};
var loopDefaults = function(defaults, targ){
for(var d in defaults){
if(defaults.hasOwnProperty(d) && defaults[d]!==null){
if(typeof(defaults[d])=="object"){
loopDefaults(defaults[d], targ[d]);
}else{
// defaults[d] is a value - set to corresponding knockout binding
targ[d](defaults[d]);
}
}
}
};

Backbone: annoying behaviour of prototype object

I understand this is a problem (or behaviour) of javascript itself rather than Backbone's extend method, but I'd like to know what is the best strategy to avoid it.
Let's better put it in code:
var MyModel = Backbone.Model.extend({
value: 0,
values: []
});
var myFirstModel = new MyModel();
myFirstModel.value // 0, as expected
myFirstModel.values // [], as expected
var mySecondModel = new MyModel();
mySecondModel.value = 2;
mySecondModel.values.push(2)
mySecondModel.value // 2, as expected
mySecondModel.values // [2], as expected
myFirstModel.value // 0, as expected
myFirstModel.values // [2], ... WAT!!!
I do understand that the problem is I'm not assigning a new value to mySecondModel.values I'm just operating on the values variable that is in the prototype, that is MyModel.prototype.values (same problem with any other object, of course)
But it's very easy to mess with that. The most intuitive thing is to just think of those as INSTANCE variables, and not variables common to every instance (static or class variables in class based languages).
So far now the general solution I've found is to initialize every variable in the initialize method, like this:
var MyModel = Backbone.Model.extend({
initialize: function() {
this.value = 0;
this.values = [];
}
});
That way everything works as expected, and even though it wouldn't be neccesary for a simple value (like this.value) I find it much easier to just stick to this prnciple in every case.
I'm wondering if there's some better (more elegant, clearer) solution to this problem
This is an effect of JavaScript's prototypical inheritance and the fact that Array objects are reference types. The key/value pairs of the object you pass to extend are copied onto the prototype of MyModel, so they will be shared by all instances of MyModel. Because values is an array, when you modify it, you modify the array for every instance.
What you are doing by setting values inside initialize is called shadowing the prototype, and it is the correct way to solve this issue.
That said, in the case of Backbone.Model, if you are attempting to deal with the model's attributes, you can use the defaults function to provide defaults like this:
var MyModel = Backbone.Model.extend({
defaults: function() {
return {
value: 0,
values: []
}
}
});
Again, this is only for attributes of an instance.
var inst = new MyModel();
// The defaults will be created for each new model,
// so this will always return a new array.
var values = inst.get('values');
For what you are doing, where you are specifying properties on the model itself, it is up to you to set the defaults inside of initialize, as you have done.
Are you intentionally not setting value and values as backbone attributes? If you set attributes on an instance, instead of putting them in the extended backbone model definition, it might work how you expect.
var MyModel = Backbone.Model.extend();
var myFirstModel = new MyModel({
value: 0,
values: []
});
console.log(myFirstModel.get('value'); // 0
console.log(myFirstModel.get('values'); // []
var mySecondModel = new MyModel({
value: 2,
values: [2]
});
//mySecondModel.value = 2;
//mySecondModel.values.push(2)
console.log(mySecondModel.get('value'); // 2
console.log(mySecondModel.get('values'); // [2]
console.log(myFirstModel.get('value'); // 0
console.log(myFirstModel.get('values'); // []
jsFiddle, check the console log.
I too had stumbled across this problem some time back and solved it by defining a defaults method in the model.
var MyModel = Backbone.Model.extend({
defaults: function() {
return {
value: 0,
values: []
}
}
});

set attribute of all models in backbone collection

I understand that using pluck method we can get an array of attributes of each model inside a backbone collection
var idsInCollection = collection.pluck('id'); // outputs ["id1","id2"...]
I want to if there is a method that sets up an attribute to each model in the collection,
var urlArray = ["https://url1", "https://url1" ...];
collection.WHAT_IS_THIS_METHOD({"urls": urlArray});
There's not exactly a pre-existing method, but invoke let's you do something similar in a single line:
collection.invoke('set', {"urls": urlArray});
If you wanted to make a re-usable set method on all of your collections, you could do the following:
var YourCollection = Backbone.Collection.extend({
set: function(attributes) {
this.invoke('set', attributes);
// NOTE: This would need to get a little more complex to support the
// set(key, value) syntax
}
});
* EDIT *
Backbone has since added its own set method, and if you overwrite it you'll completely break your Collection. Therefore the above example should really be renamed to setModelAttributes, or anything else which isn't set.
I don’t there is a method for it, but you can try:
collection.forEach(function(model, index) {
model.set(url, urlArray[index]);
});
Expanding on David's answer, you can easily put this functionality into a custom method on the collection. Here's my way of doing it using coffeescript:
class Collection extends Backbone.Collection
setAll: () ->
_args = arguments
#models.forEach (model) -> model.set _args...
class SomeCollection extends Collection
url: '/some-endpoint.json'
myLovelyCollection = new SomeCollection()
myLovelyCollection.fetch
success: (collection, response) ->
collection.setAll someVar: true
collection.setAll anotherVar, 'test'
If you wanted to do it in vanilla JS it's exactly the same but not harnessing the power of classes or splats. So more like:
var Collection = Backbone.Collection.extend({
setAll: function () {
var _args = arguments;
this.models.forEach(function (model) {
model.set.apply(model, _args);
});
}
});
Just thought I'd post my slightly updated method based on machineghost's version. This uses lodash invokeMap method instead of underscore's invoke. It supports the same optional syntax as the standard model.set method ... e.g. ('prop', 'val') or ({prop: 'val', prop: 'val'}) as well accepting and passing an options object.
var YourCollection = Backbone.Collection.extend({
setModels: function(key, val, options) {
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
if (attrs) {
_.invokeMap(this, 'set', attrs, options);
}
return this;
}
});
If you are using invoke the syntax according to the underscore site should be
_.invoke(list, methodName, *arguments) http://underscorejs.org/#invoke
So the above function mentioned by machineghost should be
collection.invoke({'url': someURL},'set');
Hope that helps :)

jquery using objects as filters

Is there any way to have DOM elements selectable through objects?
For example I want to be able to associate objects to DOM elements like so:
var obj = { a: 1, b:2 };
$('a').click(function() { this.selectThing = obj });
And later on...
$.something(obj);
Or even better:
$('a|selectThing?=', obj);
Something like that. You can see that I want to associate an object to a DOM element in such a way that I can grab the element with the object.
I know this can be done with the filter() method, my question is if there's a more elegant way that doesn't use filter() to do this.
EDIT:
To clarify, I want to be able to use an object kind of like a selector, so I can do something similar to this $(obj) obviously that won't work, but you get the idea (I hope)
EDIT #2:
I want to be able to do something like this:
var obj = { prop: 'prop' };
$('a').bindTo(obj);
$.retreive(obj) // should equal $('a')
I don't want it to alter obj in any way though (obj should still be {prop: 'prop'} only).
demo
var $div1 = $('.box1');
var $div2 = $('.box2');
var obj = { a: $div1, b: $div2 };
obj.a.css({background:'red'});
Or the short way: var obj = { a: $('.box1'), b: $('.box2') };
demo jsBin 2
var obj = $('.box1, .box2'); // store objects
obj.css({background:'red'}); // access collection
You're looking for $.data. This method associates any JavaScript object or primitive with a DOM element. Under the hood, it's not adding the data as an expando to the DOM element or anything--instead, jQuery maintains its own object cache of DOM elements and data hashes. But that's under the hood; the point is, I think it's exactly what you're looking for.
$('#example').data('foo', { bar: 'quux' }); // returns the jquery object containing '#example', like most jQuery methods
Then, later:
console.log($('#example').data('foo')); // returns {bar: 'quux'}
I dont think this is easily achievable. Let me clarify:
To achieve what you want you would require a hashmap that allows objects in the position of keys. JavaScript does not (yet) support objects as keys in hashmaps though. So, for example, the following does not work:
var key = {value: 'key'};
var data {value: 'data'};
var map = {};
map[key] = data;
There are other solutions to achieve this in current javascript implementations, eg. a double lookup:
var key = {value: 'key'};
var data {value: 'data'};
var map = { keys: [], data: [], get: function (key) {
var k = this.keys.indexOf(key);
if (k >= 0) {
return this.data[k];
} else return undefined;
}, set: function (key, val) {
var k = this.keys.indexOf(key);
if (k < 0) {
k = this.keys.push(k) - 1;
}
this.data[k] = val;
} };
map.set(key, data);
map.get(key).value;
This implementation however is of a terrible performance. There is a proposal for a so called WeakMap in JavaScript Harmony. Firefox I believe is currently the only browser implementing them, though. Since the feature required is not widely available and workarounds are of poor performance I would recommend trying to figure out a different way of achieving what you are trying to.
Extend jQuery with three methods:
jQuery.bindObj(data)
jQuery.unbindObj(data)
$.retrieve(data)
Your code looks like:
$('a').bindObj({blorg: 'shmorg'});
console.log($.retrieve({blorg: 'shmorg'})); // logs live result of $('a');
Full source: http://jsfiddle.net/nUUSV/6/.
The trick to this solution is storing the selectors/identifiers based to the jQuery constructor in one array, and the objects bound to those selectors/identifiers in another array, then using $.inArray to get the index of the object upon retrieval and using that index to grab the bound jQuery collection.
As I understand, you're looking for some sugar way to run multiple named searches on the DOM and have results filtered in a namespace object.
If so, I guess the following jquery extension might be helpfull to you:
$.fn.seek = function (selectors) {
var container = this,
found = {};
$.each(selectors, function (name) {
if ($.isPlainObject(selectors[name])) {
found[name] = $(container).seek(selectors[name]);
}
else if ($.type(selectors[name]) === 'string') {
found[name] = $(container).find(selectors[name]);
}
});
return found;
}
And here's the example of how the above extension might be applicable to your cases:
var res = $('body').seek({
links: 'a',
headers: 'h1,h2,h3,h4,h5,h6'
});
$(res.links).css({ color: 'green' });
$(res.headers).css({ color: 'red' });
I hope this helps you.
Not sure if this is what you are looking for. Perhaps you could write a custom selector based on the jquery selector, which handles objects with a selector-property the way you like. A selectable object would look like
var objSelector = {'selector' : '#select-me', 'a' : 'somestring', 'b' : 1243};
So you are free to use it like any other object, but you have to add the selector property. Than you add your custom selector:
$$ = (function($) {
return function(el, tag) {
if (typeof el === 'object' && el.selector !== undefined) {
return $(el.selector);
}
return $(el);
}
}($));
Now you can do things like
$$(objSelector).css({'border':'1px solid red'});
See an implementation on http://jsfiddle.net/JXcnJ/
If I understood correctly then, I think you need to define a property and say enumerable as false. See below,
Note: Below is just an example to demonstrate and not exactly meant to do such stuff,
DEMO
$(function() {
$.fn.bindTo = function(o) {
var _that = this;
Object.defineProperty(o, 'myFx', {
value: function() { return $(_that); },
writable: true,
enumerable: false,
configurable: true
});
}
$.retrieve = function(obj) {
return obj.myFx();
}
var obj = {
prop: 'prop'
};
$('#test').bindTo(obj);
$($.retrieve(obj)).html('Test');
//below is for proof
for (i in obj) {
alert(obj[i]);
}
});
Reference: http://yehudakatz.com/2011/08/12/understanding-prototypes-in-javascript/

Categories

Resources