Backbone.Models: How to do deep toJSON serialization with embedded Collections? - javascript

I have a Backbone Model that contains a collection:
var Stream = Backbone.Model.extend({
defaults: {
dummyField: "1",
excludedUsers: new Backbone.Collection()
}
});
var s = new Stream;
s.get('excludedUsers').add( {name:'Stefan'} );
console.log(s.toJSON())
yields:
{ dummyField: '1',
excludedUsers:
{ length: 1,
models: [ [Object] ],
_byId: {},
_byCid: { c1: [Object] } } }
instead of the "expected":
{
dummyField: '1',
excludedUsers: [ {name:'Stefan'} ]
}
because Backbone isn't deeply JSONing the Model. The only way of working around is to override the toJSON method on the Stream's prototype but that won't help for other cases. Is there a general/better solution (besides the heavy Backbone.Relational plugin) already?

You might want to overwrite the Backbone.Collection.toJSON() function directly or make a new collection to which you extend all your other collections:
var MyDefaultCollection = Backbone.Collection.extend({
toJSON: function() {
//Your code here
}
});
var Stream = Backbone.Model.extend({
defaults: {
dummyField: "1",
excludedUsers: new MyDefaultCollection()
}
});
//You could also extend it
var NewCollection = MyDefaultCollection.extend({
//custom code here
});
It's just theory, I've never coded it, so any feedback on my idea is welcome :)

function flattenModel(model) {
return _.mapValues(model.attributes, flatten);
}
function flattenCollection(collection) {
return collection.map(flattenModel);
}
function flatten(object) {
if (object instanceof Backbone.Model) {
return flattenModel(object);
} else if (object instanceof Backbone.Collection) {
return flattenCollection(object);
}
return object;
}
This will return an object, and then:
JSON.stringify(flatten(model))
Note that _.mapValues is a handy method from lodash, so you should use that or just port that method.

Related

Knockout.js Adding a Property to Child Elements

My code doesn't create a new property under the child element of knockout viewmodel that is mapped by knockout.mapping.fromJS.
I have:
//model from Entity Framework
console.log(ko.mapping.toJSON(model));
var viewModel = ko.mapping.fromJS(model, mappingOption);
ko.applyBindings(viewModel);
console.log(ko.mapping.toJSON(viewModel));
The first console.log outputs:
{
"Id": 0,
"CurrentUser": {
"BoardIds": [
{
"Id": 0
}
],
"Id": 1,
"UserName": "foo",
"IsOnline": true
},
"Boards": []
}
And then the mappingOption is:
var mappingOption = {
create: function (options) {
var modelBase = ko.mapping.fromJS(options.data);
modelBase.CurrentUser.UserName = ko.observable(model.CurrentUser.UserName).extend({ rateLimit: 1000 });
//some function definitions
return modelBase;
},
'CurrentUser': {
create: function (options) {
options.data.MessageToPost = ko.observable("test");
return ko.mapping.fromJS(options.data);
}
}
};
I referred to this post to create the custom mapping, but it seemed not working as the second console.log outputs the same JSON to the first one.
Also, I tried to create nested mapping option based on this thread and another one but it didn't work too.
var mappingOption = {
create: function (options) {
//modelBase, modifing UserName and add the functions
var mappingOption2 = {
'CurrentUser': {
create: function (options) {
return (new(function () {
this.MessageToPost = ko.observable("test");
ko.mapping.fromJS(options.data, mappingOption2, this);
})());
}
}
}
return ko.mapping.fromJS(modelBase, mappingOption2);
}
};
How can I correctly add a new property to the original viewmodel?
From the mapping documentation for ko.toJS (toJS and toJSON work the same way as stated in the document)
Unmapping
If you want to convert your mapped object back to a regular JS object, use:
var unmapped = ko.mapping.toJS(viewModel);
This will create an unmapped object containing only the properties of the mapped object that were part of your original JS object
If you want the json to include properties you've added manually either use ko.toJSON instead of ko.mapping.toJSON to include everything, or use the include option when first creating your object to specify which properties to add.
var mapping = {
'include': ["propertyToInclude", "alsoIncludeThis"]
}
var viewModel = ko.mapping.fromJS(data, mapping);
EDIT: In your specific case your mapping options are conflicting with each other. You've set special instructions for the CurrentUser field but then overridden them in the create function. Here's what I think your mapping options should look like:
var mappingOption = {
'CurrentUser': {
create: function (options) {
var currentUser = ko.mapping.fromJS(options.data, {
'UserName': {
create: function(options){
return ko.observable(options.data);
}
},
'include': ["MessageToPost"]
});
currentUser.MessageToPost = ko.observable("test");
return ko.observable(currentUser).extend({ rateLimit: 1000 });
}
}
};
and here's a fiddle for a working example

Return object with subset of its attributes

I've got a flat JavaScript object like this:
{
id: 3726492,
kind: 'user',
permalink: 'nicholas',
username: 'Nicholas',
...
a lot more attributes
}
I'd like to create a new object which only has a subset of the attributes of the original object.
Something like
var newObject = oldObject.fields(['id', 'username']);
newObject would be
{
id: 3726492,
username: 'Nicholas'
}
Is there already something like this?
Try this
function pick(data, keys) {
var result = {};
keys.forEach(function (key) {
if (data.hasOwnProperty(key)) {
result[key] = data[key];
}
});
return result;
}
var data = {
id: 3726492,
kind: 'user',
permalink: 'nicholas',
username: 'Nicholas'
}
var newData = pick(data, ['id', 'kind']);
console.log(newData);
In underscorejs or lodash there is method .pick
var data = {
id: 3726492,
kind: 'user',
permalink: 'nicholas',
username: 'Nicholas',
};
var newObject = _.pick(data, 'id', 'username');
console.log(newObject);
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
You can use Array.prototype.reduce to reduce one object to another using the list of properties:
function subset(obj, propList) {
return propList.reduce(function(newObj, prop) {
obj.hasOwnProperty(prop) && (newObj[prop] = obj[prop]);
return newObj;
}, {});
}
var obj = {
id: 3726492,
kind: 'user',
permalink: 'nicholas',
username: 'Nicholas'
};
var newObj = subset(obj, ['id', 'username']);
console.log(newObj);
document.getElementById('json').innerText = JSON.stringify(newObj);
<pre id="json"></pre>
Not built-in, but you can sure define a simple function that does the job:
var original = {a:1112, b:434, c:666, d:222};
function fieldSubset(obj, fields) {
var subsetClone = {};
for( var i=0,l=fields.length; i<l; i++) {
// This can prevent filling undefined as properties
if(obj.hasOwnProperty(fields[i])) {
subsetClone[fields[i]] = obj[fields[i]];
}
}
return subsetClone;
}
fieldSubset(original, ["a", "c"]);
You can also use this in Object.prototype, but be aware that this might happen to conflict with native API in the future versions of JavaScript:
var original = {a:1112, b:434, c:666, d:222};
Object.defineProperty(Object.prototype, "fieldSubset", {
value: function(fields) {
var subsetClone = {};
for( var i=0,l=fields.length; i<l; i++) {
// This can prevent filling undefined as properties
if(this.hasOwnProperty(fields[i])) {
subsetClone[fields[i]] = this[fields[i]];
}
}
return subsetClone;
},
enumerable: false,
configurable: true}
);
original.fieldSubset(["a", "c"]);
One liner using Array.prototype.reduce. We are also using Object.assign. The idea is to keep extending a blank object with the keys found in the filters array. If you see, the reduce function takes a callback function with arg1,arg2,arg3 params as the first argument and an empty object as the second argument. This object will be cloned and extended with the help of the keys specified in the filters array.
var a = {
id: 3726492,
kind: 'user',
permalink: 'nicholas',
username: 'Nicholas',
};
var filters = ["id","username","permalink"];
var sub = Object.keys(a).reduce((arg1,arg2,arg3)=>{ var res = {}; if(filters.indexOf(arg2)>=0){ res[arg2] = a[arg2]; } return Object.assign(arg1,res);},{})
console.log(sub);
You haven't specifically mentioned what is the type of values behind your object's keys. Your current answers cover the shallow copy and deep copy.
Another alternative would be to create a view of the original object. This would be helpful if you have very large data objects and you do not want them copy in the memory.
function View(obj, properties) {
var view = {};
properties.forEach(function(prop) {
Object.defineProperty(view, prop, {
get: function() {
return obj[prop];
},
set: function(val) {
obj[prop] = val;
},
enumerable: true,
configurable: true
});
});
return view;
}
then with your data you can do:
var data = {
id: 3726492,
kind: 'user',
permalink: 'nicholas',
username: 'Nicholas',
},
view = new View(data, ['id', 'username']);
view.id; // 3736492
view.username; // Nicholas
of course you have to be aware that you can change your original object just by view.id = 'something else'. However it is easily preventable.

What is the most elegant way to transform both keys and values of an object using lodash?

Say I have the following Object:
var Models = { User: ...
{ collection:
{ name: 'users' }
} ...
, Post: { ...
collection:
{ name: 'posts'
}
}
}
I want to turn this into:
{'users': 'User', 'posts': 'Post'}
Does anyone have something more elegant than this approach (lodash):
collectionNames = _.mapValues(_.mapKeys(Models, function(model) {
return model.collection.name;
}), function(model) {
return model.modelName;
});
What is wrong with a basic loop, sort of easier to understand and no dependencies?
node.js
var Models = { User: {collection: { name: 'users' }}, Post: { collection: {name:'posts' }}
var res = {}
for (var key in Models) res[Models[key].collection.name] = key

Backbone collection sortBy

I make my first backbone app and get some problems with collection sorting.
After using this
var SortedFriends = MyFriends.sortBy(function(friend) {
return friend.get("uid");
});
console.log(SortedFriends) show that SortedFriends contains sorted models, but when i try to use collection functions like 'SortedFriends.each' or 'SortedFriends.at' it make error:
TypeError: SortedFriends.each is not a function.
Code:
var Friend = Backbone.Model.extend({});
var Friends = Backbone.Collection.extend({
model: Friend,
});
var MyFriends = new Friends();
MyFriends.reset(<?=$friends?>);
var FriendView = Backbone.View.extend({
initialize: function(){
model:Friend
},
tagName: "tr",
template: _.template($('#item-template').html()),
className: "document-row",
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var SortedFriends = MyFriends.sortBy(function(friend) {
return friend.get("uid");
});
var addOne = function(element){
var view = new FriendView({model: element});
$("#friends").append(view.render().el);
}
console.log(JSON.stringify(SortedFriends));
SortedFriends.each(function(friend){
var view = new FriendView({model: friend});
$("#friends").append(view.render().el);
});
If youre using backbone collections then youre probably better off using the comparator rather than collection methods
http://backbonejs.org/#Collection-comparator
When youre ready to sort your collection:
MyFriends.comparator = function(friend){
return friend.get("uid");
});
MyFriends.sort();
OR if you want to keep the order of the unsorted collection then you will need to clone it first
http://backbonejs.org/#Collection-clone
var SortedFriends = MyFriends.clone();
SortedFriends.comparator = function(friend){
return friend.get("uid");
});
SortedFriends.sort();
I'm not sure if it's a bug or a feature of Backbone's adaptation of sortBy, but apparently it returns an array, not an Underscore collection.
One workaround is to wrap the whole thing in _( ... ), which tells Underscore to wrap the array back into a collection:
var SortedFriends = _(MyFriends.sortBy(function(friend) {
return friend.get("uid");
}));
Edit
Most of the Underscore methods in Backbone seem to be chainable (replace sortBy with reject, for example, and it runs). Looking at the Backbone source where they wire up the Underscore proxies, it seems that sortBy is treated differently. I can't understand why they do it this way ...
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
'isEmpty', 'chain'];
_.each(methods, function(method) {
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};

Why are my Backbone Models nested strangely within a Collection, requiring drilling down to access methods/properties?

I've got a Collection and a Model, both using attributes/options to augment them with additional capabilities. Here's the Model (LoadRouteGroup):
return Backbone.Model.extend({
initialize: function () {
console.log(this);
},
fetchf: function () {
console.log("FETCH");
}
});
And the Collection (LoadRouteGroups):
return Backbone.Collection.extend({
constructUrl: function(options) {
if (options.groupingType === "facility") {
// TODO: new endpoint: /api/v1/loadroutes?grouping=facility
this.url = clawConfig.endpoints.webApiRootUrl + "/api/loads/facilities";
}
else {
this.url = clawConfig.endpoints.webApiRootUrl + "/api/v1/loadroutes";
}
},
initialize: function (models, options) {
options || (options = {});
this.constructUrl(options);
console.log(this);
}
});
They're instantiated as such:
var loadRouteGroup = new LoadRouteGroup({
entityType: "facility"
});
// WORKS
loadRouteGroup.fetchf();
// assign groupingType option to collection to denote which URL to use
var loadRouteGroups = new LoadRouteGroups({
model: loadRouteGroup
}, {
groupingType: "facility"
});
var firstGroup = loadRouteGroups.at(0);
// DOESN'T WORK
firstGroup.fetchf();
// WORKS
firstGroup.attributes.model.fetchf();
I would expect that call to firstGroup.fetchf() to work... but it doesn't. Instead, I have to weirdly drill down and use firstGroup.attributes.model.fetchf() in order to access the method.
What's going on here? This would seem straightforward to me, but I can't for the life of me figure out what's wrong with the relationship between my Collection and Model.
The collection definition should include the model type:
return Backbone.Collection.extend({
// ....
model: LoadRouteGroup
});
When initializing the collection, pass in an array of models:
var loadRouteGroup = new LoadRouteGroup({
entityType: "facility"
});
var loadRouteGroups = new LoadRouteGroups([loadRouteGroup], {
groupingType: "facility"
});
Specify the model when you extend the collection instead of when you instantiate.

Categories

Resources