I am converting the nested data object to the arrays, for the UI Library to show the relationship between the data.
Original
// assume that all object key is unique
{
"top":{
"test":{
"hello":"123"
},
"test2":{
"bye":"123"
"other":{
...
...
...
}
}
}
}
Preferred Result
[
{
id:"top",
parent: null,
},
{
id:"test",
parent: "top",
},
{
id:"hello",
parent: "test",
},
{
id:"test2",
parent: "top",
},
]
To do this, I write the code like this:
const test = []
const iterate = (obj, parent = null) => {
Object.keys(obj).forEach(key => {
const id = typeof obj[key] === 'object' ? key : obj[key]
const loopObj = {
id,
parent
}
test.push(loopObj)
if (typeof obj[key] === 'object') {
iterate(obj[key], id)
}
})
}
iterate(data)
console.log(test) // Done!!
It works.
However, I miss one important things, the library need the layers from the original data, to determine the type/ what function to do.
// The key name maybe duplicated in different layer
{
"top":{ // Layer 1
"test":{ // Layer 2
"hello":"123", // Layer 3
"test":"123" // Layer 3
// Maybe many many layers...
}
}
}
[
{
id:"top",
display:"0-top",
parent: null,
layer: 0
},
{
id: "1-top-test", // To prevent duplicated id, `${layer}-${parentDisplay}-${display}`
display:"test",
parent: "0-top",
parentDisplay: "top",
layer: 1
},
{
id: "3-test-test", // To prevent duplicated id,`${layer}-${parentDisplay}-${display}`
display:"test",
parent: "2-top-test",
parentDisplay: "test",
layer: 3
}
]
Editing the display or id format is very simple, just edit the function and add the field, but I don't know how to get the layer easily.
I tried to add the let count = 0 outside and do count++ when iterate function called.But I realized that it hit when the object detected, no by layers.
The original data may be very big,
So I think editing the original data structure or searching the parent id in the test[] every loop may be not a good solution.
Is there any solution to do this?
Just add the current depth as an argument that gets passed down on every recursive call (as well as the parent name).
const input = {
"top":{
"test":{
"hello":"123"
},
"test2":{
"bye":"123",
"other":{
}
}
}
};
const iterate = (obj, result = [], layer = 0, parentId = null, parentDisplay = '') => {
Object.entries(obj).forEach(([key, value]) => {
const id = `${layer}-${key}`;
result.push({
id,
display: key,
parentId,
parentDisplay,
layer,
});
if (typeof value === 'object') {
iterate(value, result, layer + 1, id, key);
}
});
return result;
}
console.log(iterate(input));
That said, your desired approach can still produce duplicate entries, if there exist two objects at the same level, with different grandparent objects, but whose parent objects use the same key, eg:
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const iterate = (obj, result = [], layer = 0, parentId = null, parentDisplay = '') => {
Object.entries(obj).forEach(([key, value]) => {
const id = `${layer}-${key}`;
result.push({
id,
display: key,
parentId,
parentDisplay,
layer,
});
if (typeof value === 'object') {
iterate(value, result, layer + 1, id, key);
}
});
return result;
}
console.log(iterate(input));
If that's a problem, consider passing down the entire accessor string needed to access the property - eg top1.test.hello and top2.test.hello, which is guaranteed to be unique.
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const iterate = (obj, result = [], parentAccessor = '') => {
Object.entries(obj).forEach(([key, value]) => {
const accessor = `${parentAccessor}${parentAccessor ? '.' : ''}${key}`;
result.push({
id: key,
accessor,
});
if (typeof value === 'object') {
iterate(value, result, accessor);
}
});
return result;
}
console.log(iterate(input));
Related
Im totally clueless how to build this. I am not even sure it is even possible and I've been scratching my head for way too long now.
Lets say I have an object:
const myObj = {
simple: "test",
nested: {
obj: "alright"
}
}
Now I have found a function that lets me set a value anywhere by specifying a path in that tree. If a key is not already existing in that object, it will be created:
const set = (obj: any, path: any, val: any) => {
const keys = path.split(".");
const lastKey = keys.pop();
const lastObj = keys.reduce((obj: any, key: any) => obj[key] = obj[key] || {}, obj);
lastObj[lastKey] = val;
};
Example:
set(myObj, "nested.another.iCanEvenGoDeeper", "very deep value");
Result:
const myObj = {
simple: "test",
nested: {
obj: "alright",
another: {
iCanEvenGoDeeper: "very deep value"
}
}
}
So far so good, but now its required that I can also define a path like this to dynamically build arrays. So that I can call these:
set(myObj, "nested.myArray[0].propInsideArrayElement", "first element")
set(myObj, "nested.myArray[1].propInsideArrayElement", "second element")
that will result in an object that looks like this:
{
simple: "test",
nested: {
obj: "alright",
myArray: [
{ propInsideArrayElement: "first element" },
{ propInsideArrayElement: "second element" }
]
}
}
It needs to be recursive and work with all scenarios, but I am like I said clueless on if it is even possible. Is there by any chance some utility scripts out there that does this already? If not, can anyone point me in the right direction?
In a next step, I would like to flatten the object to have a one dimensional object again, for the last example it would then look like this:
flatten(myObj);
would then turn to
{
"simple": "test",
"nested.obj": "alright",
"nested.myArray[0].propInsideArrayElement": "first element",
"nested.myArray[1].propInsideArrayElement": "second element"
}
I have totally reworked the deepSet function now. It now supports multiple arrays and gaps in the arrays etc. I think this covers now every usecase. In the end it was way easier to figure the logic out when I started over without the reduce function
export const deepSet = (obj: any, path: string, val: any) => {
path = path.replaceAll("[", ".[");
const keys = path.split(".");
for (let i = 0; i < keys.length; i++) {
let currentKey = keys[i] as any;
let nextKey = keys[i + 1] as any;
if (currentKey.includes("[")) {
currentKey = parseInt(currentKey.substring(1, currentKey.length - 1));
}
if (nextKey && nextKey.includes("[")) {
nextKey = parseInt(nextKey.substring(1, nextKey.length - 1));
}
if (typeof nextKey !== "undefined") {
obj[currentKey] = obj[currentKey] ? obj[currentKey] : (isNaN(nextKey) ? {} : []);
} else {
obj[currentKey] = val;
}
obj = obj[currentKey];
}
};
Looks interesting :)
Here is an example for array support based on your own code.
flatten the object is also included (Using recursive calls)
const myObj = {
simple: "test",
nested: {
obj: "alright"
}
}
const getTypeVal = (currentIndex, length, val) => {
}
const set = (obj, path, val) => {
path = path.replace('[', '.[')
const keys = path.split(".");
const lastKey = keys.pop();
let lastObj = keys.reduce((obj, key, currentIndex) => {
if(key.includes('[')) {
return obj[key.substring(1, key.length-1)]
}
if(obj[key] && obj[key].length && (keys[currentIndex+1] && keys[currentIndex+1].includes('['))) {
let nextKey = keys[currentIndex+1]
nextKey = nextKey.substring(1, nextKey.length-1)
!obj[key][nextKey] && obj[key].push({})
}
return obj[key] = obj[key] || ((keys[currentIndex+1] && keys[currentIndex+1].includes('[')) ? [{}] : keys[currentIndex+1] ? {} : val)
}
, obj);
lastObj[lastKey] = val;
};
const flatternObj = (obj, result = {}, key ='') =>{
if(Array.isArray(obj)) {
obj.forEach((d,i) => {
result = flatternObj(d, result, key + `[${i}]`)
})
}
else if(typeof obj === 'object') {
for (const i of Object.keys(obj)) {
result = flatternObj(obj[i], result, key ? key + `.${i}` : `${i}`)
}
}
else {
result[key] = obj
}
return result;
}
set(myObj, "nested.myArray[0].propInsideArrayElement", "first element")
set(myObj, "nested.myArray[0].propInsideArrayElement2", "first element - 2 ")
set(myObj, "nested.myArrayTwo[0]", 'test')
set(myObj, "nested.myArray[1].propInsideArrayElement", "second element")
set(myObj, "nested.myArray[2]", 'test')
console.log(myObj)
console.log(flatternObj(myObj))
I've created an update object API that receives new update data of an existing document.
Let's say, I have two objects oldData and newData
oldData = {
me:{
name:{
short:'will'
long:'william'
}
},
friends:[
{
id: 1,
name:{
short:'mike'
long:'michael'
},games:[]
},
{
id: 2,
name:{
short:'fred'
long:'freddy'
}
},
],
favoriteGames:[
'gta',
'animal crossing',
'mortal kombat'
],
favoriteFood:['bacon'],
}
newData = {
me:{
name:{
long:'willy'
longer:'william'
}
},
friends:[
{
id:3,
name:{
short:'max',
long:'maxwell'
}
},
{
id:1,
name:{
short:'mic',
}
},
],
favoriteGames:[
'tekken'
]
}
calling applyUpdate(oldData, newData)should return
result = {
me:{
name:{
short:'will',
long:'willy',
longer:'william'
}
},
friends:[
{
id:3,
name:{
short:'max',
long:'maxwell'
}
},
{
id: 1,
name:{
short:'mic'
long:'michael'
},games:[]
}
],
favoriteGames:[
'tekken'
],
favoriteFood:['bacon'],
}
Basically, the rules for merging are:
If a key in an object is specified with new data, it overrides the
value of the same key in old data.
If a key is not specified, the
value of the same key in old data is kept.
If the value of a key in new data is an array of objects:
Each object must be merged BY id with elements in the array of the same key in old data.
Elements not included in the arrays of newData are removed from the result.
The order of elements in the arrays of newData should be preserved.
Merging must be done deeply, since nested arrays and objects of unspecified depth should be possible.
I've actually successfully implemented this with a horrendously long and ugly recursive function. But am worried about performance and readability issues. I am open to suggestions using lodash or underscore.
Thanks!
Try this. It's hard to write this in a readable way.
function customizer(oldProp, newProp) {
if (Array.isArray(newProp)) {
// check if `newProp` is an array of objects which has property `id`
if (typeof newProp[0] === 'object' && newProp[0].hasOwnProperty('id')) {
if (!Array.isArray(oldProp)) {
return newProp;
}
// merge objects of 2 arrays in `oldProp` and `newProp`
const mergedArr = [];
for (const objNewArr of newProp) {
const objOldArr = oldProp.find(o => o.id === objNewArr.id);
if (objOldArr) {
mergedArr.push(_.merge(objOldArr, objNewArr));
} else {
mergedArr.push(objNewArr);
}
}
return mergedArr;
}
return newProp;
}
if (typeof newProp === 'object') {
return _.merge(oldProp, newProp);
}
return newProp;
}
_.mergeWith(oldData, newData, customizer); // returns the merged object
Here's what worked. Thanks Duc.
function customizer(oldProp, newProp) {
if (Array.isArray(newProp)) {
if (typeof newProp[0] === 'object') {
const mergedArr = [];
for (const objNewArr of newProp) {
const objOldArr = oldProp.find(o => o._id === objNewArr._id);
if (objOldArr) {
mergedArr.push(_.mergeWith(_.cloneDeep(objOldArr), _.cloneDeep(objNewArr), customizer));
} else {
mergedArr.push(objNewArr);
}
}
return mergedArr;
}else{
return newProp;
}
}else if (typeof newProp === 'object') {
return _.merge(oldProp, newProp);
}else{
return undefined;
}
}
var result = _.mergeWith(_.cloneDeep(oldData), _.cloneDeep(newData), customizer); // returns the merged object
I got the following problem,
I need to iterate through a big Json object ( child nodes consist of array's, strings and objects with at least 4-5 layers of depth in terms of nested properties ).
In some parts across the big Json file there is a specific object structure, it has a property named "erpCode". I need to scan the Json and find all the objects with that property, take the value use that code to ask a different API for details and once I get the details insert them into the object with the current 'erpCode'.
Just to clarify, in my case the parent node property name in the Json always equals the value in 'typeSysname' field which located on the same 'level' as the erpCode property.
A simple example :
{
"cars": [
{
"name": "X222",
"carType": {
"erpCode": "skoda",
"value": null,
"typeSysName": "carType"
}
}
],
"model": {
"year": 1999,
"details": {
"erpCode": "112"
"value": null,
"typeSysName": "details"
}
}
}
In this example I need to find 2 properties get the values skoda and 112 out of them and get the value and description data from a different API and set it into this Json in the right location.
P.S. Any chance there is a good npm package which can help me with that?
Edit:
I got a solution in C# from a few months ago which runs in a generic way on the Json and handles the complexity of the structure in a generic way.
But I now need to convert this into Javascript and I am a bit lost.
public static string TranslateDocErpCodes(string jsonString, string topRetailerSysName)
{
try
{
var doc = JObject.Parse(jsonString);
var erpCodeList = doc.SelectTokens("$..erpCode").ToList();
foreach (var erpCodeJToken in erpCodeList)
{
var value = erpCodeJToken?.Value<string>();
var erpCodeParent = erpCodeJToken?.Parent.Parent;
var erpCodeProperty = erpCodeParent?.Path.Split(".").Last();
var result =
_dataService.GetLovFromErpCode(topRetailerSysName, erpCodeProperty, value);
if (result == null)//reset lov obj
{
if (erpCodeParent?.Parent is JProperty prop)
prop.Value = JObject.FromObject(new LovObject { ErpCode = value });
}
else//set lov obj
{
result.ErpCode = value;
if (erpCodeParent?.Parent is JProperty prop)
prop.Value = JObject.FromObject(result);
}
}
return JsonConvert.SerializeObject(doc);
}
catch (Exception e)
{
throw new Exception("ErpConvert.TranslateDocErpCodes() : " + e);
}
}
mb something like;
function processObject(jsonData) {
for (prop in jsonData) {
if (jsonData.hasOwnProperty(prop)) {
// We get our prop
if (prop === 'code') {
let codeValue = jsonData[prop]
doSomeAsync(codeValue)
.then(response => {
jsonData[prop] = response;
})
}
let curValue = jsonData[prop];
if (Array.isArray(curValue)) {
// Loop through the array, if array element is an object, call processObject recursively.
processArray(curValue);
} else if (typeof curValue === 'object') {
processObject(curValue);
}
}
}
}
I took the answer from Aravindh as a starting point and managed to reach what seems to be a complete solution.
I will share it here,
async function convertErpCodes(jsonData, orgName, parentPropertyName){
for (let prop in jsonData) {
if (jsonData.hasOwnProperty(prop)) {
if (prop === 'erpCode') {
const erpCodeValue = jsonData[prop]
const req = {"query": {"erpCode": erpCodeValue, "orgName": orgName, "typeSysName": parentPropertyName}};
const result = await viewLookupErpService.findOne(req);
if(result)
return result;
}
const curValue = jsonData[prop];
if (Array.isArray(curValue)) {
for(let i in curValue){
const res = await convertErpCodes(curValue[i], orgName, prop);
}
} else if (curValue && typeof curValue === 'object') {
const response = await convertErpCodes(curValue, orgName, prop);
if(response){
jsonData[prop] = response;
}
}
}
}
}
P.S.
I set up the values only if I get a response from the third party API ( this is the reason for the result and response logic in the recursion.
I'd use object-scan and lodash.set in combination
// const objectScan = require('object-scan');
// const lodash = require('lodash');
const stats = { cars: [{ name: 'X222', carType: { erpCode: 'skoda', value: null, typeSysName: 'carType' } }], model: { year: 1999, details: { erpCode: '112', value: null, typeSysName: 'details' } } };
const entries = objectScan(['**.erpCode'], { rtn: 'entry' })(stats);
console.log(entries);
// => [ [ [ 'model', 'details', 'erpCode' ], '112' ], [ [ 'cars', 0, 'carType', 'erpCode' ], 'skoda' ] ]
// where you would query the external api and place results in entries
entries[0][1] = 'foo';
entries[1][1] = 'bar';
entries.forEach(([k, v]) => lodash.set(stats, k, v));
console.log(stats);
// => { cars: [ { name: 'X222', carType: { erpCode: 'bar', value: null, typeSysName: 'carType' } } ], model: { year: 1999, details: { erpCode: 'foo', value: null, typeSysName: 'details' } } }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
<script src="https://bundle.run/lodash#4.17.20"></script>
Disclaimer: I'm the author of object-scan
I have original nested object which contains the huge tree kind of structure. This is is basically JSON string which is converted into JavaScript object.
Structure is like -
original = {
type : "table",
children :[
{
type : "cell",
children : [
{
type : "label",
children : []
}
]
}
{
type : "cell",
children : []
}
]
}
I have selected item as -
var select = original.children[1].children[0];
What I want is get the parent of selected item.
Here is sample demo - https://stackblitz.com/edit/angular-v5m9ua
Note : I need to trace over the original object to find the parent. I had looked at the other answers but they had mentioned how to design the object structure to get the parent but I don't want to change the original object.
You could create recursive function with for...in loop and return last parent element that was of object type.
const data = {
type: "table",
children: [{
type: "cell",
children: [{
type: "label",
children: []
}]
}, {
type: "cell",
children: []
}]
}
var select = data.children[0].children[0];
function getParent(data, obj, parent = null) {
let result = null;
(function loop(data, obj, parent) {
if (typeof data == 'object' && !Array.isArray(data)) {
parent = data
}
for (let i in data) {
if (select == data[i]) {
result = parent;
break;
}
if (typeof data[i] == 'object') {
loop(data[i], obj, parent)
}
}
})(data, obj, parent)
return result;
}
let parent = getParent(data, select)
console.log(parent)
You could search the tree:
findParent(root, toFind) {
for(const child of root.children || []) {
if(child === toFind){
return root;
}
const result = this.findParent(child, toFind);
if(result){
return result;
}
}
}
That can be used as:
findParent(original, select)
I am trying to return a specific node in a JSON object structure which looks like this
{
"id":"0",
"children":[
{
"id":"1",
"children":[...]
},
{
"id":"2",
"children":[...]
}
]
}
So it's a tree-like child-parent relation. Every node has a unique ID.
I'm trying to find a specific node like this
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
currentNode.children.forEach(function (currentChild) {
findNode(id, currentChild);
});
}
}
I execute the search for example by findNode("10", rootNode). But even though the search finds a match the function always returns undefined. I have a bad feeling that the recursive function doesn't stop after finding the match and continues running an finally returns undefined because in the latter recursive executions it doesn't reach a return point, but I'm not sure how to fix this.
Please help!
When searching recursively, you have to pass the result back by returning it. You're not returning the result of findNode(id, currentChild), though.
function findNode(id, currentNode) {
var i,
currentChild,
result;
if (id == currentNode.id) {
return currentNode;
} else {
// Use a for loop instead of forEach to avoid nested functions
// Otherwise "return" will not work properly
for (i = 0; i < currentNode.children.length; i += 1) {
currentChild = currentNode.children[i];
// Search in the current child
result = findNode(id, currentChild);
// Return the result if the node has been found
if (result !== false) {
return result;
}
}
// The node has not been found and we have no more options
return false;
}
}
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
var result;
currentNode.children.forEach(function(node){
if(node.id == id){
result = node;
return;
}
});
return (result ? result : "No Node Found");
}
}
console.log(findNode("10", node));
This method will return the node if it present in the node list. But this will loop through all the child of a node since we can't successfully break the forEach flow. A better implementation would look like below.
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
for(var index in currentNode.children){
var node = currentNode.children[index];
if(node.id == id)
return node;
findNode(id, node);
}
return "No Node Present";
}
}
console.log(findNode("1", node));
I use the following
var searchObject = function (object, matchCallback, currentPath, result, searched) {
currentPath = currentPath || '';
result = result || [];
searched = searched || [];
if (searched.indexOf(object) !== -1 && object === Object(object)) {
return;
}
searched.push(object);
if (matchCallback(object)) {
result.push({path: currentPath, value: object});
}
try {
if (object === Object(object)) {
for (var property in object) {
if (property.indexOf("$") !== 0) {
//if (Object.prototype.hasOwnProperty.call(object, property)) {
searchObject(object[property], matchCallback, currentPath + "." + property, result, searched);
//}
}
}
}
}
catch (e) {
console.log(object);
throw e;
}
return result;
}
Then you can write
searchObject(rootNode, function (value) { return value != null && value != undefined && value.id == '10'; });
Now this works on circular references and you can match on any field or combination of fields you like by changing the matchCallback function.
Since this old question has been brought back up, here's a different approach. We can write a fairly generic searchTree function which we then use in a findId function. searchTree does the work of traversing the object; it accepts a callback as well as the tree; the callback determines if a node matches. As well as the node, the callback is supplied two functions, next and found, which we call with no parameters to signal, respectively, that we should proceed or that we've found our match. If no match is found, we return null.
It looks like this:
const searchTree = (fn) => (obj) =>
Array.isArray(obj)
? obj.length == 0
? null
: searchTree (fn) (obj [0]) || searchTree (fn) (obj .slice (1))
: fn (
obj,
() => searchTree (fn) (obj .children || []),
() => obj
)
const findId = (target, obj) => searchTree (
(node, next, found) => node.id == target ? found () : next(),
) (tree)
const tree = {id: 1, name: 'foo', children: [
{id: 2, name: 'bar', children: []},
{id: 3, name: 'baz', children: [
{id: 17, name: 'qux', children: []},
{id: 42, name: 'corge', children: []},
{id: 99, name: 'grault', children: []}
]}
]}
console .log (findId (42, tree))
console .log (findId (57, tree))
This code is specific to the structure where subnodes are found in an array under the property children. While we can make this more generic as necessary, I find this a common structure to support.
There is a good argument that this would be better written with mutual recursion. If we wanted, we could get the same API with this version:
const searchArray = (fn) => ([x, ...xs]) =>
x === undefined
? null
: searchTree (fn) (x) || searchArray (fn) (xs)
const searchTree = (fn) => (obj) =>
fn (
obj,
() => searchArray (fn) (obj .children || []),
(x) => x
)
This works the same way. But I find the code cleaner. Either should do the job, though.
We use object-scan for our data processing needs. It's conceptually very simple, but allows for a lot of cool stuff. Here is how you could solve your question
// const objectScan = require('object-scan');
const findNode = (id, input) => objectScan(['**'], {
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.id === id
})(input);
const data = { id: '0', children: [{ id: '1', children: [ { id: '3', children: [] }, { id: '4', children: [] } ] }, { id: '2', children: [ { id: '5', children: [] }, { id: '6', children: [] } ] }] };
console.log(findNode('6', data));
// => { id: '6', children: [] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
Similar questions were answered several times, but I just want to add a universal method that includes nested arrays
const cars = [{
id: 1,
name: 'toyota',
subs: [{
id: 43,
name: 'supra'
}, {
id: 44,
name: 'prius'
}]
}, {
id: 2,
name: 'Jeep',
subs: [{
id: 30,
name: 'wranger'
}, {
id: 31,
name: 'sahara'
}]
}]
function searchObjectArray(arr, key, value) {
let result = [];
arr.forEach((obj) => {
if (obj[key] === value) {
result.push(obj);
} else if (obj.subs) {
result = result.concat(searchObjectArray(obj.subs, key, value));
}
});
console.log(result)
return result;
}
searchObjectArray(cars, 'id', '31')
searchObjectArray(cars, 'name', 'Jeep')
I hope this helps someone
I really liked a tree search! A tree is an extremely common data structure for most of today's complex structured tasks. So I just had similar task for lunch too. I even did some deep research, but havent actually found anything new! So what I've got for you today, is "How I implemented that in modern JS syntax":
// helper
find_subid = (id, childArray) => {
for( child of childArray ) {
foundChild = find_id( i, child ); // not sub_id, but do a check (root/full search)!
if( foundChild ) // 200
return foundChild;
}
return null; // 404
}
// actual search method
find_id = (id, parent) => (id == parent.id) : parent : find_subid(id, parent.childArray);
Recursive structure search, modification, keys/values adjustments/replacement.
Usage Example:
const results = []; // to store the search results
mapNodesRecursively(obj, ({ v, key, obj, isCircular }) => {
// do something cool with "v" (or key, or obj)
// return nothing (undefined) to keep the original value
// if we search:
if (key === 'name' && v === 'Roman'){
results.push(obj);
}
// more example flow:
if (isCircular) {
delete obj[key]; // optionally - we decide to remove circular links
} else if (v === 'Russia') {
return 'RU';
} else if (key.toLocaleLowerCase() === 'foo') {
return 'BAR';
} else if (key === 'bad_key') {
delete obj[key];
obj['good_key'] = v;
} else {
return v; // or undefined, same effect
}
});
Tips and hints:
You can use it as a search callback, just return nothing (won't affect anything) and pick values you need to your Array/Set/Map.
Notice that callback is being run on every leaf/value/key (not just objects).
Or you can use the callback to adjust particular values and even change keys. Also it automatically detects circular loops and provides a flag for you to decide how to handle them.
The code
(uses ES6)
Function itself + some example demo data
function mapNodesRecursively(obj, mapCallback, { wereSet } = {}) {
if (!wereSet) {
wereSet = new Set();
}
if (obj && (obj === Object(obj) || Array.isArray(obj))) {
wereSet.add(obj);
for (let key in obj) {
if (!obj.hasOwnProperty(key)){
continue;
}
let v = obj[key];
const isCircular = wereSet.has(v);
const mapped = mapCallback({ v, key, obj, isCircular });
if (typeof (mapped) !== 'undefined') {
obj[key] = mapped;
v = mapped;
}
if (!isCircular) {
mapNodesRecursively(v, mapCallback, { wereSet });
}
}
}
return obj;
}
let obj = {
team: [
{
name: 'Roman',
country: 'Russia',
bad_key: 123,
},
{
name: 'Igor',
country: 'Ukraine',
FOO: 'what?',
},
{
someBool: true,
country: 'Russia',
},
123,
[
1,
{
country: 'Russia',
just: 'a nested thing',
a: [{
bad_key: [{
country: 'Russia',
foo: false,
}],
}],
},
],
],
};
// output the initial data
document.getElementById('jsInput').innerHTML = JSON.stringify(obj, null, 2);
// adding some circular link (to fix with our callback)
obj.team[1].loop = obj;
mapNodesRecursively(obj, ({ v, key, obj, isCircular }) => {
if (isCircular) {
delete obj[key]; // optionally - we decide to remove circular links
} else if (v === 'Russia') {
return 'RU';
} else if (key.toLocaleLowerCase() === 'foo') {
return 'BAR';
} else if (key === 'bad_key') {
delete obj[key];
obj['good_key'] = v;
} else {
return v;
}
});
// output the result - processed object
document.getElementById('jsOutput').innerHTML = JSON.stringify(obj, null, 2);
.col {
display: inline-block;
width: 40%;
}
<div>
<h3>Recursive structure modification, keys/values adjustments/replacement</h3>
<ol>
<li>
Replacing "Russia" values with "RU"
</li>
<li>
Setting the value "BAR" for keys "FOO"
</li>
<li>
Changing the key "bad_key" to "good_key"
</li>
</ol>
<div class="col">
<h4>BEFORE</h4>
<pre id="jsInput"></pre>
</div>
<div class="col">
<h4>AFTER</h4>
<pre id="jsOutput"></pre>
</div>
</div>