Recursive nested property creation in JavaScript - javascript

I am trying to recursively build an object with a tree of properties based on a MongoDB-ish selector "top.middle.bottom". There are some underscorejs helpers as well:
function setNestedPropertyValue(obj, fields, val) {
if (fields.indexOf(".") === -1) {
// On last property, set the value
obj[fields] = val;
return obj; // Recurse back up
} else {
var oneLevelLess = _.first(fields.split("."));
var remainingLevels = _.rest(fields.split(".")).join(".");
// There are more property levels remaining, set a sub with a recursive call
obj[oneLevelLess] = setNestedPropertyValue( {}, remainingLevels, val);
}
}
setNestedPropertyValue({}, "grandpaprop.papaprop.babyprop", 1);
Desired:
{
grandpaprop: {
papaprop: {
babyprop: 1
}
}
}
Outcome:
undefined
Helps and hints would be appreciated.

Instead of recursion I would choose for an iterative solution:
function setNestedPropertyValue(obj, fields, val)
{
fields = fields.split('.');
var cur = obj,
last = fields.pop();
fields.forEach(function(field) {
cur[field] = {};
cur = cur[field];
});
cur[last] = val;
return obj;
}
setNestedPropertyValue({}, "grandpaprop.papaprop.babyprop", 1);

EDIT
And here is another version thanks to the suggestions by Scott Sauyet:
function setPath(obj, [first, ...rest], val) {
if (rest.length == 0) {
return {...obj, [first]: val}
}
let nestedObj = obj[first] || {};
return {...obj, [first]: setPath(nestedObj, rest, val)};
}
function setNestedPropertyValue(obj, field, val) {
return setPath(obj, field.split('.'), val);
}
// example
let test_obj = {};
test_obj = setNestedPropertyValue(test_obj, "foo.bar.baz", 1);
test_obj = setNestedPropertyValue(test_obj, "foo.bar.baz1", 1);
// will output {"foo":{"bar":{"baz":1,"baz1":1}}}, while in the original version only "baz1" will be set
console.log(JSON.stringify(test_obj));
It's plain javascript
It only appends properties and will not override a top level object
setNestedPropertyValue() does not mutate the passed object (although keep in mind it only returns a shallow copy of the object, so some properties may be shared references between the original object and the new one)
I know this is old, but I needed exactly that kind of function and wasn't happy with the implementation, so here is my version:
function setNestedPropertyValue(obj, field, val) {
if (field.indexOf(".") === -1) {
obj[field] = val;
} else {
let fields = field.split(".");
let topLevelField = fields.shift();
let remainingFields = fields.join(".");
if (obj[topLevelField] == null) {
obj[topLevelField] = {};
}
setNestedPropertyValue(obj[topLevelField], remainingFields, val);
}
}
// example
let test_obj = {};
setNestedPropertyValue(test_obj, "foo.bar.baz", 1);
setNestedPropertyValue(test_obj, "foo.bar.baz1", 1);
// will output {"foo":{"bar":{"baz":1,"baz1":1}}}, while in the original version only "baz1" will be set
console.log(JSON.stringify(test_obj));
It's plain javascript
It only appends properties and will not override a top level object
setNestedPropertyValue() does not return the object so it is clear that it mutates the passed object

As mentioned by Jack in the question, I was not returning my object in the last line in the else statement. By adding this, it is now working:
obj[oneLevelLess] = setNestedPropertyValue( {}, remainingLevels, val);
return obj; // Add this line
}

Related

Mixing Index Array with "Associative Array"

Since I need to access my items sometime by index and sometime by code. Is it a good idea to mix integer index with string index?
Note that the code, index, amount of items never changes after the data is loaded.
I'm thinking of doing something like this, where the same object is pushed and set as a hashtable.
function DataInformation(code, dataValue) {
this.code = code;
this.dataValue = dataValue;
}
var dataList = [];
function fillDataList() {
addNewData(new DataInformation("C1", 111));
addNewData(new DataInformation("C2", 222));
addNewData(new DataInformation("C3", 333));
}
function addNewData(newData) {
dataList.push(newData);
dataList[newData.code] = newData;
}
Then I would be able to access the object with either:
dataList[0].dataValue
dataList["C1"].dataValue
Before I used to loop to find the item.
function findItemByCode(code) {
for (var i = 0; i < dataList.length; i++) {
if (dataList[i].code == code) {
return dataList[i];
}
}
return null;
}
findItemByCode("C1").dataValue
Do you ever need to iterate dataList in strict order? Or is it just a bag of items for which you want random access by a certain key?
If ordered iteration is not a concern, use an object instead of an array. Watch out for key clashes, though.
var dataList = {};
function addNewData(newData) {
dataList[newData.code] = newData;
dataList[newData.dataValue] = newData;
}
// that's it, no other changes necessary
If key clashes can occur - or ordered iteration is necessary, or if you just want to make it particularly clean, use an array and an accompanying index object.
var dataList = [];
var dataIndex = {
byCode: {},
byValue: {}
};
function addNewData(newData) {
dataList.push(newData);
dataIndex.byCode[newData.code] = newData;
dataIndex.byValue[newData.dataValue] = newData;
}
Here is my try using Proxies
// Code goes here
function DataInformation(code, dataValue) {
this.code = code;
this.dataValue = dataValue;
}
var _dataList = [];
var dataList = new Proxy(_dataList, {
get: function(target, name) {
if (target && target.myMap && target.myMap[name]) return target[target.myMap[name]];
return target[name];
},
set: function(obj, prop, value) {
// The default behavior to store the value
obj.myMap = obj.myMap || {};
obj.myMap[value.code] = prop;
obj[prop] = value;
return true;
}
});
function fillDataList() {
addNewData(new DataInformation("C1", 111));
addNewData(new DataInformation("C2", 222));
addNewData(new DataInformation("C3", 333));
}
function addNewData(newData) {
dataList.push(newData);
}
fillDataList();
console.log(dataList[0].dataValue);
console.log(dataList["C1"].dataValue);

Return array of functions as an array of booleans

So I'm working on updating some old projects and I am trying to find a source or an example of something I'm trying to accomplish.
what I have
// sample object of functions
var functions = {
required : function(value){
return value.length > 0;
},
isObject : function(value){
return typeof value == 'object';
}
};
Above is a sample of functions in an object. What I want to know is can the following be done:
pseudo code
//user input
var funcs = [required(''), isObject({key : 'v'})];
// what the function I'm building will process, in a sense
functions[funcs[i]](//arguments from funcs[i]);
// what would remain after each function
funcs = [false, true] // with an end result of false
I'm not 100% sure that this can't be done, I'm just not sure how in the slightest something like this would be able to come about. Let's bounce some ideas around here and see what we come up with.
Let me know if you all need any clarification of anything I asked. Thank you ahead of time for all help!
clarification on what I am trying to achieve
The object of functions is not finite, there can be any amount of functions for this specific program I am writing. They are going to be predefined, so user input is not going to be an issue. I need to be able to determine what function is called when it is passed, and make sure any arguments passed with said function are present and passed as well. So when I pass required(''), I need to be able to go through my object of functions and find functions['required'] and passed the empty string value with it. So like this functions['required']('').
other issues
The functions object is private access and the user won't have direct access to it.
How about this.
var functions = {
required : function(value){
return value.length > 0;
},
isObject : function(value){
return typeof value == 'object';
}
};
// Because these values are user inputs, they should be strings,
// so I enclosed them in quotes.
var funcs = ["required('')", "isObject({key: 'v'})"];
funcs.map(function(e) {
return eval('functions.' + e);
});
Running this should gives you an array of return values from the functions in the object.
Trivially, this could be done with:
var tests = [functions.required(''), functions.isObject({key: 'v'})];
If that's all you need, consider that my answer.
For a more general approach, the right tool here seems to be Arrays.prototype.map(). However, since you have an object containing all your functions instead of an array of functions, you'll need some way to make the correspondence. You can easily do this with a separate array of property names (e.g., ['required', 'isObject']). Then you could do something like this:
var functions = {
required : function(value){
return value.length > 0;
},
isObject : function(value){
return typeof value == 'object';
}
};
var args = ['', {key: 'v'}];
var results = ['required', 'isObject'].map(
function(test, i) {
return functions[test](args[i]);
}
);
Of course, if functions were an array instead of an object, you could simplify this:
var functions = [
/* required : */ function(value){
return value.length > 0;
},
/* isObject : */ function(value){
return typeof value == 'object';
}
];
var args = ['', {key: 'v'}];
var results = functions.map(
function(test, i) {
return test(args[i]);
}
);
If you wanted to encapsulate this a bit, you could pass the args array as a second argument to map(), in which case inside the function you would use this[i] instead of args[i].
Sure it's possible. Something like this:
var results = [];
var value = "value_to_pass_in";
for(var f in functions)
{
results.push(f.call(this, value));
}
UPDATE:
function superFunc(value)
{
var results = [];
for(var f in functions)
{
results.push(f.call(this, value));
}
return results;
}
superFunc("value_to_pass_in");
What you want is a map function. You can mimic it like this (I guess if you want one line):
https://jsfiddle.net/khoorooslary/88gh2yeh/
var inOneLine = (function() {
var resp = {};
var i = 0;
var fns = {
required : function(value){
return value.length > 0;
},
isObject : function(value){
return typeof value == 'object';
}
};
for (var k in fns) resp[k] = fns[k](arguments[i++]);
return resp;
}).apply(null, [ '' , {key : 'v'}]);
console.log(inOneLine);
var functions = {
required : function(value){
return value.length > 0;
},
isObject : function(value){
return typeof value == 'object';
}
};
var funcs = ["required('')", "isObject({key: 'v'})"];
function f(funcs){
return funcs.map(function(e) {
return eval('functions.' + e);
});
}
console.log(f(funcs));

Array that can store only one type of object?

Is it possible to create an array that will only allow objects of a certain to be stored in it? Is there a method that adds an element to the array I can override?
Yes you can, just override the push array of the array (let's say all you want to store are numbers than do the following:
var myArr = [];
myArr.push = function(){
for(var arg of arguments) {
if(arg.constructor == Number) Array.prototype.push.call(this, arg);
}
}
Simply change Number to whatever constructor you want to match. Also I would probably add and else statement or something, to throw an error if that's what you want.
UPDATE:
Using Object.observe (currently only available in chrome):
var myArr = [];
Array.observe(myArr, function(changes) {
for(var change of changes) {
if(change.type == "update") {
if(myArr[change.name].constructor !== Number) myArr.splice(change.name, 1);
} else if(change.type == 'splice') {
if(change.addedCount > 0) {
if(myArr[change.index].constructor !== Number) myArr.splice(change.index, 1);
}
}
}
});
Now in ES6 there are proxies which you should be able to do the following:
var myArr = new Proxy([], {
set(obj, prop, value) {
if(value.constructor !== Number) {
obj.splice(prop, 1);
}
//I belive thats it, there's probably more to it, yet because I don't use firefox or IE Technical preview I can't really tell you.
}
});
Not directly. But you can hide the array in a closure and only provide your custom API to access it:
var myArray = (function() {
var array = [];
return {
set: function(index, value) {
/* Check if value is allowed */
array[index] = value;
},
get: function(index) {
return array[index];
}
};
})();
Use it like
myArray.set(123, 'abc');
myArray.get(123); // 'abc' (assuming it was allowed)

Avoiding duplication in object- Javascript

I have the next object:
var persons= {};
persons["Matt"].push("A");
persons["Matt"].push("B");
persons["Matt"].push("C");
And I want to know if the object contains the element which I try to insert.
E.g:
persons["Matt"].push("A"); /* The element A already exist... And I don't want duplicate elements.*/
Anybody know one way to make it?
EDIT WITH MORE DETAILS:
I have a the next code:
function insertIfNotThere(array, item) {
if (array.indexOf(item) === -1) {
array.push(item);
}
}
function EventManager(target) {
var target = target || window, events = {};
this.observe = function(eventName, cb) {
if (events[eventName]){
insertIfNotThere(events[eventName], cb);
}else{
events[eventName] = []; events[eventName].push(cb);
}
return target;
};
this.fire = function(eventName) {
if (!events[eventName]) return false;
for (var i = 0; i < events[eventName].length; i++) {
events[eventName][i].apply(target, Array.prototype.slice.call(arguments, 1));
}
};
}
I use your method for checking if the element with the content indicated exist. But... It push the element ever... I don't know what's happening...
First things first. When you do
persons= {};
you are creating a global property called persons and assigning an empty object to it. You might want a local variable here. So, change it to
var persons = {};
And then, when you create a new key in the object, by default, the value will be undefined. In your case you need to store an array. So, you have to initialize it like this
persons['Matt'] = [];
and then you can use the Array.prototype.indexOf function to find out if the item being added is already there in the array or not (it returns -1 if the item is not found in the array), like this
if (persons['Matt'].indexOf("A") === -1) {
persons['Matt'].push("A");
}
if (persons['Matt'].indexOf("B") === -1) {
persons['Matt'].push("B");
}
You can create a function to do this
function insertIfNotThere(array, item) {
if (array.indexOf(item) === -1) {
array.push(item);
}
}
var persons = {};
persons['Matt'] = [];
insertIfNotThere(persons['Matt'], 'A');
insertIfNotThere(persons['Matt'], 'B');
// This will be ignored, as `A` is already there in the array
insertIfNotThere(persons['Matt'], 'A');
Use indexOf to check for the existence of A. If it doesn't exist (is -1), add it to the array:
if (persons['Matt'].indexOf('A') === -1) {
persons['Matt'].push('A');
}

Ember model to json

I am looking for an efficient way to translate my Ember object to a json string, to use it in a websocket message below
/*
* Model
*/
App.node = Ember.Object.extend({
name: 'theName',
type: 'theType',
value: 'theValue',
})
The websocket method:
App.io.emit('node', {node: hash});
hash should be the json representation of the node. {name: thename, type: theType, ..}
There must be a fast onliner to do this.. I dont want to do it manualy since i have many attributes and they are likely to change..
As stated you can take inspiration from the ember-runtime/lib/core.js#inspect function to get the keys of an object, see http://jsfiddle.net/pangratz666/UUusD/
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, ret = [];
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
} // ignore useless items
if (Ember.typeOf(v) === 'function') {
continue;
}
ret.push(key);
}
}
return this.getProperties.apply(this, ret);
}
});
Note, since commit 1124005 - which is available in ember-latest.js and in the next release - you can pass the ret array directly to getProperties, so the return statement of the getJson function looks like this:
return this.getProperties(ret);
You can get a plain JS object (or hash) from an Ember.Object instance by calling getProperties() with a list of keys.
If you want it as a string, you can use JSON.stringify().
For example:
var obj = Ember.Object.create({firstName: 'Erik', lastName: 'Bryn', login: 'ebryn'}),
hash = obj.getProperties('firstName', 'lastName'), // => {firstName: 'Erik', lastName: 'Bryn'}
stringHash = JSON.stringify(hash); // => '{"firstName": "Erik", "lastName": "Bryn"}'
I have also been struggling with this. As Mirko says, if you pass the ember object to JSON.stringify you will get circular reference error. However if you store the object inside one property and use stringify on that object, it works, even nested subproperties.
var node = Ember.Object.create({
data: {
name: 'theName',
type: 'theType',
value: 'theValue'
}
});
console.log(JSON.stringify(node.get('data')));
However, this only works in Chrome, Safari and Firefox. In IE8 I get a stack overflow so this isn't a viable solution.
I have resorted to creating JSON schemas over my object models and written a recursive function to iterate over the objects using the properties in the schemas and then construct pure Javascript objects which I can then stringify and send to my server. I also use the schemas for validation so this solution works pretty well for me but if you have very large and dynamic data models this isn't possible. I'm also interested in simpler ways to accomplish this.
I modifed #pangratz solution slightly to make it handle nested hierarchies of Jsonables:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {};
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
App.io.emit('node', {node: node.toJSON()});
Or if you have an ID property and want to include it:
App.io.emit('node', {node: node.toJSON({includeId: true})});
Will this work for you?
var json = JSON.stringify( Ember.getMeta( App.node, 'values') );
The false is optional, but would be more performant if you do not intend to modify any of the properties, which is the case according to your question. This works for me, but I am wary that Ember.meta is a private method and may work differently or not even be available in future releases. (Although, it isn't immediately clear to me if Ember.getMeta() is private). You can view it in its latest source form here:
https://github.com/emberjs/ember.js/blob/master/packages/ember-metal/lib/utils.js
The values property contains only 'normal' properties. You can collect any cached, computed properties from Ember.meta( App.node, false ).cached. So, provided you use jQuery with your build, you can easily merge these two objects like so:
$.extend( {}, Ember.getMeta(App.node, 'values'), Ember.getMeta(App.node, 'cache') );
Sadly, I haven't found a way to get sub-structures like array properties in this manner.
I've written an extensive article on how you can convert ember models into native objects or JSON which may help you or others :)
http://pixelchild.com.au/post/44614363941/how-to-convert-ember-objects-to-json
http://byronsalau.com/blog/convert-ember-objects-to-json/
I modified #Kevin-pauli solution to make it works with arrays as well:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {}, inspectArray = function (aSome) {
if (Ember.typeof(aSome) === 'array') {
return aSome.map(inspectArray);
}
if (Jsonable.detect(aSome)) {
return aSome.getJson();
}
return aSome;
};
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (Ember.typeOf(v) === 'array') {
v = v.map(inspectArray);
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
I also made some further modification to get the best of both worlds. With the following version I check if the Jsonable object has a specific property that informs me on which of its properties should be serialized:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {}, base, inspectArray = function (aSome) {
if (Ember.typeof(aSome) === 'array') {
return aSome.map(inspectArray);
}
if (Jsonable.detect(aSome)) {
return aSome.getJson();
}
return aSome;
};
if (!Ember.isNone(this.get('jsonProperties'))) {
// the object has a selective list of properties to inspect
base = this.getProperties(this.get('jsonProperties'));
} else {
// no list given: let's use all the properties
base = this;
}
for (var key in base) {
if (base.hasOwnProperty(key)) {
v = base[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (Ember.typeOf(v) === 'array') {
v = v.map(inspectArray);
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
I am using this little tweak and I am happy with it. I hope it'll help others as well!
Thanks to #pangratz and #Kevin-Pauli for their solution!
Here I take #leo, #pangratz and #kevin-pauli solution a little step further. Now it iterates not only with arrays but also through has many relationships, it doesn't check if a value has the type Array but it calls the isArray function defined in Ember's API.
Coffeescript
App.Jsonable = Em.Mixin.create
getJson: ->
jsonValue = (attr) ->
return attr.map(jsonValue) if Em.isArray(attr)
return attr.getJson() if App.Jsonable.detect(attr)
attr
base =
if Em.isNone(#get('jsonProperties'))
# no list given: let's use all the properties
this
else
# the object has a selective list of properties to inspect
#getProperties(#get('jsonProperties'))
hash = {}
for own key, value of base
continue if value is 'toString' or Em.typeOf(value) is 'function'
json[key] = jsonValue(value)
json
Javascript
var hasProp = {}.hasOwnProperty;
App.Jsonable = Em.Mixin.create({
getJson: function() {
var base, hash, hashValue, key, value;
jsonValue = function(attr) {
if (Em.isArray(attr)) {
return attr.map(jsonValue);
}
if (App.Jsonable.detect(attr)) {
return attr.getJson();
}
return attr;
};
base = Em.isNone(this.get('jsonProperties')) ? this : this.getProperties(this.get('jsonProperties'));
json = {};
for (key in base) {
if (!hasProp.call(base, key)) continue;
value = base[key];
if (value === 'toString' || Em.typeOf(value) === 'function') {
continue;
}
json[key] = jsonValue(value);
}
return json;
}
});
Ember Data Model's object counts with a toJSON method which optionally receives an plain object with includeId property used to convert an Ember Data Model into a JSON with the properties of the model.
https://api.emberjs.com/ember-data/2.10/classes/DS.Model/methods/toJSON?anchor=toJSON
You can use it as follows:
const objects = models.map((model) => model.toJSON({ includeId: true }));
Hope it helps. Enjoy!
I have:
fixed and simplified code
added circular reference prevention
added use of get of value
removed all of the default properties of an empty component
//Modified by Shimon Doodkin
//Based on answers of: #leo, #pangratz, #kevin-pauli, #Klaus
//http://stackoverflow.com/questions/8669340
App.Jsonable = Em.Mixin.create({
getJson : function (keysToSkip, visited) {
//getJson() called with no arguments,
// they are to pass on values during recursion.
if (!keysToSkip)
keysToSkip = Object.keys(Ember.Component.create());
if (!visited)
visited = [];
visited.push(this);
var getIsFunction;
var jsonValue = function (attr, key, obj) {
if (Em.isArray(attr))
return attr.map(jsonValue);
if (App.Jsonable.detect(attr))
return attr.getJson(keysToSkip, visited);
return getIsFunction?obj.get(key):attr;
};
var base;
if (!Em.isNone(this.get('jsonProperties')))
base = this.getProperties(this.get('jsonProperties'));
else
base = this;
getIsFunction=Em.typeOf(base.get) === 'function';
var json = {};
var hasProp = Object.prototype.hasOwnProperty;
for (var key in base) {
if (!hasProp.call(base, key) || keysToSkip.indexOf(key) != -1)
continue;
var value = base[key];
// there are usual circular references
// on keys: ownerView, controller, context === base
if ( value === base ||
value === 'toString' ||
Em.typeOf(value) === 'function')
continue;
// optional, works also without this,
// the rule above if value === base covers the usual case
if (visited.indexOf(value) != -1)
continue;
json[key] = jsonValue(value, key, base);
}
visited.pop();
return json;
}
});
/*
example:
DeliveryInfoInput = Ember.Object.extend(App.Jsonable,{
jsonProperties: ["title","value","name"], //Optionally specify properties for json
title:"",
value:"",
input:false,
textarea:false,
size:22,
rows:"",
name:"",
hint:""
})
*/
Ember.js appears to have a JSON library available. I hopped into a console (Firebug) on one the Todos example and the following worked for me:
hash = { test:4 }
JSON.stringify(hash)
So you should be able to just change your line to
App.io.emit('node', { node:JSON.stringify(hash) })

Categories