I have an nested JSON object like this:
var jsonObj =
{ "level1" :
{ "status" : true,
"level2" : {} // and it has the same format and can go to level3, 4, etc
}
}
What I want to do is simple, I want to get to Level2, and add a new Level3 object to it.
Basically I want to do the following code below, but since the levels are dynamic, I need a function that traverse my object.
obj.Level1.Level2.Level3 = { 'status' : true}
Here's my snippet of code:
function updateStatusForLevel(nestedObj, categoryHierarchy){
// categoryHierarchy that is passed = ['Level1', 'Level2', 'Level3'];
var obj = nestedObj;
angular.forEach(categoryHierarchy, function(value, key){
obj = obj[value];
if (key === categoryHierarchy.length - 1 && angular.isUndefined(obj)){
obj[value] = {}; // I want to add 'Level3' = {}
}
});
obj.status = 'true'; // and finally, update the status
console.info("my original obj is " + JSON.stringify(nestedObj));
}
However seems like I'm missing something. If I do this, my original nestedObj is still the same as what I'm passing in (it's not updated, only the obj object is updated. I believe this should be a really simple code that traverse a nested JSON object. Why is the shallow copy not updating the original object?
Maybe like this
function updateStatusForLevel(nestedObj, categoryHierarchy){
// categoryHierarchy that is passed = ['Level1', 'Level2', 'Level3'];
if(categoryHierarchy.length) {
var shifted = categoryHierarchy.shift();
nestedObj[shifted] = {status: true};
return updateStatusForLevel(starter[shifted], categoryHierarchy);
} else {
return nestedObj;
}
}
Then calling updateStatusForLevel(nestedObj, ['level1', 'level2', 'level3']) will modify nestedObj as
level1: Object
level2: Object
level3: Object
status: true
status: true
status: true
note, this answer is not clever, so better have plnkr or something for better asnwer, but for now, try this in browser dev console
Since you only want to add a value from a certain path of nested objects, then how about creating a generic function that can do this, instead of creating a custom one. I have created a factory named helper, which can be a collection of helper functions that you may want to add later on.
DEMO
JAVASCRIPT
.factory('helper', function() {
var helper = {};
helper.set = function(object, path, value) {
// save reference of an object
var reference = object,
// last key n the path
lastKey;
path = angular.isArray(path)? path: // set as an array if it is an array
angular.isString(path)? path.split('.'): // split the path as an array if it is a string
false; // set to false and do nothing if neither of the conditions above satisfies
// check if path is truthy
if(path) {
// get the last key of the path
lastKey = path.pop();
// reduce the references until all the remaining keys
reference = path.reduce(function(reference, key) {
// check if the current object reference is undefined
if(angular.isUndefined(reference[key])) {
// set current object reference as an object if it is undefined
reference[key] = {};
}
// return the current object reference for the next iteration
return reference[key];
}, reference);
// set the last object reference for the value
reference[lastKey] = value;
}
return object;
};
return helper;
})
.run(function(helper) {
var object1 = {},
object2 = {},
object3 = {},
object4 = {
"level1" : {
"status" : true,
"level2" : {}
}
};
helper.set(object1, 'z.k.v.q', { status: false });
// object1 = { z: { k: { v: { q: { status: false } } } } }
console.log(object1);
helper.set(object2, 'a.e.i.o.u', { status: true });
// object2 = { a: { e: { i: { o: { u: { status: true } } } } } }
console.log(object2);
helper.set(object3, ['hello', 'world'], { status: undefined });
// object3 = { hello: { world: { status: undefined } } }
console.log(object3);
helper.set(object4, 'level1.level2.level3', { status: true });
// object4 = { status: true, level1: { level2: { level3: { status: true } } } }
console.log(object4);
});
Alternatively, you can use lodash for this, and you'd be able to do more object, array and collection manipulation. The lodash function you should be looking for would be _.set()
DEMO
JAVASCRIPT
.service('_', function($window) {
// you can add mixins here
// read more about lodash if you
// want to customize data manipulation
return $window._;
})
.run(function(_) {
var object1 = {},
object2 = {},
object3 = {},
object4 = {
"level1" : {
"status" : true,
"level2" : {}
}
};
_.set(object1, 'z.k.v.q', { status: false });
// object1 = { z: { k: { v: { q: { status: false } } } } }
console.log(object1);
_.set(object2, 'a.e.i.o.u', { status: true });
// object2 = { a: { e: { i: { o: { u: { status: true } } } } } }
console.log(object2);
_.set(object3, ['hello', 'world'], { status: undefined });
// object3 = { hello: { world: { status: undefined } } }
console.log(object3);
_.set(object4, 'level1.level2.level3', { status: true });
// object4 = { status: true, level1: { level2: { level3: { status: true } } } }
console.log(object4);
});
Related
I have a keys property that is related to map property. The length of keys correspond with how deep the level of each map property goes. In this case only 2 levels.
If I add another entry to keys then each map property will go one more level deeper.
Below is the data
{
keys: [
"vendorApNbr",
"type"
],
map: {
_default: { <-** 1st level
_default: "'100026'", <-** 2nd level
PT_CC: "'120035'", <-** 2nd level
PT_DC: "'120037'"
},
A-00: { <- ** 1st level
_default: "'120037'" <- ** 2nd level
},
A-01: {
_default: "'120035'"
},
A-02: {
_default: "'120035'"
},
A-03: {
_default: "'120036'"
},
A-04: {
_default: "'100024'"
}
}
}
I would like to create an array of arrays where each item in the array is iteration of going from level 1 to level 2 (but can go down more levels if needed)
i.e.
[
['_default', '_default', "'10026'"],
['_default', 'PT_CC', "'120035'"],
['_default', 'PP_DC', "'120037'"],
['A-00', '_default', "'120037'"],
['A-01', '_default', "'120035'"],
...etc
['A-04', '_default', "'100024'"]
]
I'm limited to ES5 or lodash. I'm thinking of recursion but not sure how to approach this. Any suggestion can help.
Edit
also to have a way to turn the array form back to nested object form
What about this? It doesn't care about how many nested the object is and what the level is. Additionally, each depth could be different.
var obj = {
"_default": {
"_default": "'100026'",
"PT_CC": "'120035'",
"PT_DC": "'120037'"
},
"A-00": {
"_default": "'120037'"
},
"A-01": {
"_default": "'120035'"
},
"A-02": {
"_default": "'120035'"
},
"A-03": {
"_default": "'120036'"
},
"A-04": {
"_default": "'100024'"
}
}
var result = [];
function rec(acc, obj) {
if (typeof obj === "object") {
for (var key in obj) {
rec(acc.concat([key]), obj[key]);
}
return;
}
result.push(acc.concat([obj]));
}
rec([], obj);
console.log(result);
You can do it by using Depth-Fist Search.
The code below is an example extracted from this webpage. The difference here is that is concatenates every key, but you can use the same algorithm with some modifications to get a list.
var obj = {
baz: {
foo: {
bar: "5"
},
hell: {
sin: "0"
}
},
a: {
b: "1"
}
};
var hash = {};
var str = '';
var dfs = function(obj, str) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'string')
hash[str + key] = obj[key];
else {
dfs(obj[key], str + key + '.');
}
}
}
};
dfs(obj, str);
console.log(hash);
You can use the following recursive function to flatten the object's properties in an array.
This function...
only takes one parameter as argument and
doesn't rely on external vars
var data = {
keys: [
"vendorApNbr",
"type"
],
map: {
_default: {
_default: "'100026'",
PT_CC: "'120035'",
PT_DC: "'120037'"
},
A00: {
_default: "'120037'"
},
A01: {
_default: "'120035'"
},
A02: {
_default: "'120035'"
},
A03: {
_default: "'120036'"
},
A04: {
_default: "'100024'"
}
}
};
function makeItFlat(data) {
var newArray = [];
var properties = Object.getOwnPropertyNames(data);
for (var prop of properties) {
if (typeof data[prop] === 'object') {
var flat = makeItFlat(data[prop]);
for (var f of flat) {
if (!Array.isArray(f)) {
f = [f];
}
newArray.push([prop].concat(f));
}
} else {
newArray.push([prop].concat([data[prop]]));
}
}
return newArray;
}
var flatArray = makeItFlat(data.map);
console.log(flatArray);
For converting the array back to the original object, you can use this code:
var flatArray = [
["_default", "_default", "'100026'"],
["_default", "PT_CC", "'120035'"],
["_default", "PT_DC", "'120037'"],
["A00", "_default", "'120037'"],
["A01", "_default", "'120035'"],
["A02", "_default", "'120035'"],
["A03", "_default", "'120036'"],
["A04", "_default", "'100024'"]
];
function convertArrayOfStringsToObject(flatArray) {
var newObject = {};
var key = flatArray[0];
var entry = null;
if (flatArray.length == 2) {
entry = flatArray[1];
} else {
entry = convertArrayOfStringsToObject(flatArray.slice(1));
}
if (key in newObject) {
//key exists already, then merge:
Object.assign(newObject[key], entry);
} else {
newObject[key] = entry;
}
return newObject;
}
function expandArray(flatArray) {
var newObject = {}
for (var line of flatArray) {
var key = line[0];
var entry = convertArrayOfStringsToObject(line.slice(1));
if (key in newObject) {
//key exists already, then merge:
Object.assign(newObject[key], entry);
} else {
newObject[key] = entry;
}
}
return newObject;
}
console.log(expandArray(flatArray));
I want to filter on the property of children object and return parents with children that passes the filter.
I tried with combination of Array.filter, Array.some, and Object.values, but I can't think of a way to get the key back once Ive used Object.values
var data = {
parent1: {
child1: {
source: true
},
child2: {
source: true
}
},
parent2: {
child3: {
source: true
},
child4: {
source: false
}
},
parent3: {
child5: {
source: false
}
}
}
I want the outcome to be:
var afterFilter = {
parent1: {
child1: {
source: true
},
child2: {
source: true
}
},
parent2: {
child3: {
source: true
}
}
}
If you want a solution with a reuseable function, I suggest looking at this implementation.
const data = {parent1:{child1:{source:true},child2:{source:true}},parent2:{child3:{source:true},child4:{source:false}},parent3:{child5:{source:false}}}
function objectMapReduce (object, map, filter) {
// iterate key-value pairs of object
return Object.entries(object).reduce(
(accumulator, [key, value]) => {
// map each value in object
const result = map(value, key, object)
// filter each mapped value
return filter(result, key, object)
? Object.assign(accumulator, { [key]: result })
: accumulator
},
// initial value of accumulator
{}
)
}
const afterFilter = objectMapReduce(
data, // map-reduce each parent in data
parent => objectMapReduce(
parent, // map-reduce each child in parent
({ source}) => ({ source }), // copy each child
({ source }) => source // keep child if source is true
),
parent => Object.keys(parent).length > 0 // keep non-empty parent
)
console.log(afterFilter)
Instead of using Array methods, you can also try a simple for...of loop:
var data = {
parent1: {
child1: {
source: true
},
child2: {
source: true
}
},
parent2: {
child3: {
source: true
},
child4: {
source: false
}
},
parent3: {
child5: {
source: false
}
}
}
var afterFilter = {};
for (const [key, value] of Object.entries(data)){
for (const [k, v] of Object.entries(value)){
const { source } = v;
if (source !== true)
continue;
// If `afterFilter[key]` does not exist, init with {}
afterFilter[key] = afterFilter[key] || {};
afterFilter[key][k] = { source };
}
}
console.log(afterFilter)
Try this using Array.reduce and Object.entries , for each parent entry iterate through the children of the parent object filter it based on the source.
If the current child of the parent has the source as true then add it to the accumulator acc of the reduce else ignore it:
const data = {parent1:{child1:{source:true},child2:{source:true}},parent2:{child3:{source:true},child4:{source:false}},parent3:{child5:{source:false}}};
const res = Object.entries(data).reduce((acc, [key, value]) =>{
for(child in value){ //value is the child-object of the parent, iterating throgh all the key of the child and checking if the source is true for this key of the child
if(value[child].source){
acc[key] = {...acc[key], [child] : value[child]}; //using spread operator to preserve previous values
}
}
return acc;
}, {});
console.log(res);
If you find to find whose children is true and return that parent, maybe this is correct answer for you
const data = [
{ name: 'parent1', parent : { child: { source : true } } },
{ name: 'parent2', parent : { child: { source : true } } },
{ name: 'parent3', parent : { child: { source : false } } }
];
const newData = data.filter((e)=> e.parent.child.source === true);
console.log(newData);
This is my solution. Try this
var data = {
parent1: {
child1: {
source: true
},
child2: {
source: true
}
},
parent2: {
child3: {
source: true
},
child4: {
source: false
}
},
parent3: {
child5: {
source: false
}
}
}
var afterFilter = {}
for(var key in data){
for(var childkey in data[key]){
if(data[key][childkey].source){
if(afterFilter[key])
afterFilter[key][childkey] = data[key][childkey]
else
afterFilter[key] = {[childkey]: data[key][childkey]}
}
}
}
console.log(afterFilter);
I have a class "House" like :
class House{
constructor(params){
this.clear();
// this = {...params} // I know that don't work !!!
//--
// if(params.address !== undefined) this.address = {...params.address}
//...
}
clear(){
this.address = {
number: null,
street: null,
zipcode: null,
ton: null,
}
this.access = {
doorcode: null,
stair: null,
}
}
}
I want to create a new instance of House and inject in constructor multiple json like :
const h = new House({address: { /* json */ }, access: { /* json */});
Or only one like :
const h = new House({access: { /* json */});
In constructor, am i obliged to check all values in "params" to insert in good properties (nested object)
I would like to avoid to create other classes like address and access and in the house constructor create new instance of each.
What's the best practice ?
Regards
Using Object.assign() and object destructuring with default parameters in the constructor, you can achieve this quite easily:
class House {
static get defaultAddress () {
return {
number: null,
street: null,
zipcode: null,
town: null
}
}
static get defaultAccess () {
return {
doorcode: null,
stair: null
}
}
constructor({ address = House.defaultAddress, access = House.defaultAccess } = {}) {
this.clear()
Object.assign(this.address, address)
Object.assign(this.access, access)
}
clear () {
const { defaultAddress, defaultAccess } = House
Object.assign(this, { address: defaultAddress, access: defaultAccess })
}
}
// no object
console.log(new House())
// empty object
console.log(new House({}))
// partial object
console.log(new House({ address: { number: 1, street: 'street', zipcode: 12345, town: 'town' } }))
// empty sub-objects
console.log(new House({ address: {}, access: {} }))
// partial sub-objects
console.log(new House({ address: { number: 1, street: 'street' }, access: { doorcode: 321 } }))
// complete object
console.log(new House({ address: { number: 1, street: 'street', zipcode: 12345, town: 'town' }, access: { doorcode: 321, stair: 3 } }))
.as-console-wrapper{min-height:100%!important}
You can loop through the parameters and set them manually. Then, to clear, remove all own properties (properties that aren't inherited).
class House {
constructor(params) {
// set data
Object.assign(this, params);
}
clear() {
for (let key in this) {
if (this.hasOwnProperty(key))
this[key] = undefined; // or `delete this[key];`
}
}
}
let house = new House({type: "normal", height: 40});
console.log(house, house instanceof House);
Of course, you probably want to limit the input keys to a predefined set. You could store those keys in a static class variable and use them to loop through the properties in constructor and clear.
class House {
constructor(params) {
// check for invalid properties
Object.keys(params).forEach(key => {
if (!House.keys.includes(key))
throw `Invalid paramater ${key}`;
});
// set data
Object.assign(this, params);
}
clear() {
for (let key in House.keys) {
if (this.hasOwnProperty(key))
this[key] = undefined; // or `delete this[key];`
}
}
}
House.keys = ['type', 'height'];
let house = new House({type: 'normal', height: 40});
console.log(house, house instanceof House);
let error = new House({helloWorld: true});
I think you want a common namespace for your instance properties - similar to React's props pattern - you can also specify defaults for each instance you are creating:
const defaultProps = { address: {}, access: {} };
class House {
constructor(props = {}) {
this.props = {...defaultProps, ...props};
}
clear() {
this.props = {...defaultProps};
}
}
I'm having a hard time merging nested objects with potentially the same key using Object.assign();
See code as an example
// Initial structure
let state = {
pageIndex: 1,
allResults: {
queries: {}
}
}
Code
const assign = (query, page) => {
const obj = {
[page]: {
res: 'hi'
}
}
state.allResults.queries = Object.assign(
{},
state.allResults.queries,
state.allResults.queries[query] || {[query]: {}},
obj
)
}
assign('hi', state.pageIndex);
assign('hi', (state.pageIndex + 1));
assign('hello', (state.pageIndex + 1));
console.log(state)
What I'm getting
state = {
pageindex: 1,
allResults: {
queries: {
1: {
res: 'hi'
},
2: {
res: 'hi'
},
hello: {},
hi: {}
}
}
}
What I expect
let state = {
pageIndex: 1,
allResults: {
queries: {
hi: {
1: {
res: 'h'
},
2: {
res: 'h'
}
},
hello: {
2: {
res: 'h'
}
}
}
}
}
So, this the way how I'm doing it doesn't really work, and I can't figure out how to get the expected result.
Thanks in advance
This will assign the desired sub key of the queries key which you send to the assign function (hi or hello) to their previous value, combined with the new value.
state.allResults.queries[query] = Object.assign(
{},
state.allResults.queries[query] || {},
obj
)
I think this could work for you:
const assign = (query, page) => {
const obj = {
[page]: {
res: 'hi'
}
}
let _obj = Object.assign(
{},
state.allResults.queries[query] || {},
obj
);
state.allResults.queries = Object.assign(
{},
state.allResults.queries,
{ [query]: _obj }
)
}
First I created the plain object that will be assigned to the subQuery object. Then I merge it into a existing (if not, {} an empty) object.
After that I just merge that into the query object.
Hope that it helps you.
You could use a nested Object.assign.
const assign = (query, page) => {
const obj = { [page]: { res: 'hi' } }
state.allResults.queries = Object.assign(
{},
state.allResults.queries,
{ [query]: Object.assign(state.allResults.queries[query] || {}, obj) }
);
}
let state = { pageIndex: 1, allResults: { queries: {} } };
assign('hi', state.pageIndex);
assign('hi', (state.pageIndex + 1));
assign('hello', (state.pageIndex + 1));
console.log(state)
.as-console-wrapper { max-height: 100% !important; top: 0; }
Tell me, how correctly to check the existence of a key in associative arrays?
For example:
var mydata = {
key1: '',
key2: {
subkey1: {
subkey1_1: {
value1: ''
value2" '',
},
},
subkey2: '';
},
}
if ((mydata.key2 != undefined) && (mydata.key2.subkey1 != undefined) && (mydata.key2.subkey1.subkey1_1 != undefined))
mydata.key2.subkey1.subkey1_1.value1 = 'test';
Too long and confusing
((mydata.key2 != undefined) && (mydata.key2.subkey1 != undefined) && (mydata.key2.subkey1.subkey1_1 != undefined))
I would like to use a simpler function, like
safeSet(mydata.key2.subkey1.subkey1_1.value1, 'test');
or
if (is_undefined(mydata.key2.subkey1.subkey1_1.value1) == true)
mydata.key2.subkey1.subkey1_1.value1 = 'test'; // now - error if 'mydata.key2.subkey1.subkey1_1' not exist
You can create custom function using reduce() to test if nested property exists. You can just pass key as string.
var mydata = {
key1: '',
key2: {
subkey1: {
subkey1_1: {
value1: '',
value2: '',
},
},
subkey2: ''
},
}
function safeSet(key, data) {
return key.split('.').reduce(function(r, e) {
return r ? r[e] : undefined;
}, data) != undefined
}
console.log(safeSet('key2.subkey1.subkey1_1.value1', mydata))
You should use the in operator:
"key" in obj // true, regardless of the actual value
Or, if you want to particularly test for properties of the object instance (and not inherited properties), use hasOwnProperty:
obj.hasOwnProperty("key") // true
hope this would help you.
Source: http://www.advancesharp.com/questions/628/checking-if-an-associative-array-key-exists-in-javascript
Alternatively, you can make use of the .has() method of Lodash.
Then, you would only need to check:
if (_.has(mydata, 'key2.subkey1.subkey1_1.value1')
mydata.key2.subkey1.subkey1_1.value1 = 'test';
For trying to get something in a nested structure I'd do something like this:
function getPath(element, path) {
var handledSoFar = [];
for (var i = 0; i < path.length; i++) {
var property = path[i];
handledSoFar.push(property);
if (typeof element[property] === 'undefined') {
throw new Error('Path ' + handledSoFar.join('->') + ' is undefined');
}
element = object[property];
}
return element;
}
var mydata = {
key1: '',
key2: {
subkey1: {
subkey1_1: {
value1: '',
value2: 'hi'
}
},
subkey2: ''
}
};
// Prints 'hi'
console.log(getPath(mydata, ['key2', 'subkey1', 'subkey1_1', 'value2']));
// Throws error 'Path key2->subkey2->subkey1_1 is undefined'
console.log(getPath(mydata, ['key2', 'subkey1', 'subkey1_1', 'value2']));
Of course keeping track of the search in handledSoFar is optional but might be useful for development / debugging.
You can also use the lodash deep field selector: lodash.get (documentation)
const get = require('lodash.get');
const set = require('lodash.set');
if (!get(mydata, 'key2.subkey1.subkey1_1.value1')) {
set(mydata, 'key2.subkey1.subkey1_1.value1', 'test');
}
You could split the path and make a check if the following element exist. If not assign an object to the new property.
Return then the value of the property.
At the end assign the value.
function setValue(object, path, value) {
var fullPath = path.split('.'),
way = fullPath.slice(),
last = way.pop();
way.reduce(function (r, a) {
return r[a] = r[a] || {};
}, object)[last] = value;
}
var object = { key1: '', key2: { subkey1: { subkey1_1: { value1: '', value2: '' } }, subkey2: '' } };
setValue(object, 'key2.subkey1.subkey1_1.value1', 'test');
console.log(object);
The problem with the example function that you proposed:
safeSet(mydata.key2.subkey1.subkey1_1.value1, 'test');
or
is_undefined(mydata.key2.subkey1.subkey1_1.value1)
Is that the mydata.key2.subkey1... part is run before the function is called. So if one of the subkeys does not exist, an exception will be thrown before your code is reached.
You could get something similar using a callback though...
safeSet(function(val) { mydata.key2.subkey1.subkey1_1.value1 = val; }, 'test')
the implementation of safeSet would then be:
var safeSet = function(setterFunc, val) {
try {
setterFunc(val);
} catch (e) {
if (e instanceof TypeError) {
return false;
} else {
throw e;
}
}
return true;
}
safeSet returns true if the value was set, and false otherwise.