Javascript Recurse JSON array with children objects - javascript

Is there a way to recurse the following JSON without for-looping the nested children?
My recursive function must be missing a case as it's not returning everything.
iterateTree(node, children) {
console.log(node.name)
if(node.children.length == 0) {
return;
}
for(var i = 0; i < children.length; i++) {
var child_node = children[i];
return this.iterateTree(child_node, child_node.children);
}
}
for(var i = 0; i < sample.length; i++) {
var node = sample[i];
this.iterateTree(node, node.children);
}
var sample = [
{
"name": "hello world",
"children": [
{
"name": "fruits",
"children": []
},
{
"name": "vegetables",
"children": []
},
{
"name": "meats",
"children": [
{
"name": "pork",
"children": []
},
{
"name": "beef",
"children": []
},
{
"name": "chicken",
"children": [
{
"name": "organic",
"children": []
},
{
"name": "farm raised",
"children": []
}
]
},
]
}
]
},
{
"name": "second folder",
"children": []
},
{
"name": "third folder",
"children": [
{
"name": "breads",
"children": []
},
{
"name": "coffee",
"children": [
{
"name": "latte",
"children": []
},
{
"name": "cappucino",
"children": []
},
{
"name": "mocha",
"children": []
},
]
},
]
}
]
Aiming to achieve the following output (similiar to file structure)
hello world
-fruits
-vegetables
-meats
--pork
--beef
--chicken
---organic
---farm raised
second folder
third folder
-breads
-coffee
--latte
--cappucino
--mocha

You could create recursive function using reduce method to iterate through your nested data structure, return array and then use join method on that array.
var sample = [{"name":"hello world","children":[{"name":"fruits","children":[]},{"name":"vegetables","children":[]},{"name":"meats","children":[{"name":"pork","children":[]},{"name":"beef","children":[]},{"name":"chicken","children":[{"name":"organic","children":[]},{"name":"farm raised","children":[]}]}]}]},{"name":"second folder","children":[]},{"name":"third folder","children":[{"name":"breads","children":[]},{"name":"coffee","children":[{"name":"latte","children":[]},{"name":"cappucino","children":[]},{"name":"mocha","children":[]}]}]}]
function tree(data, prev = '') {
return data.reduce((r, e) => {
r.push(prev + e.name)
if (e.children.length) r.push(...tree(e.children, prev + '-'));
return r;
}, [])
}
const result = tree(sample).join('\n')
console.log(result)
To create same structure in HTML you could use forEach method instead.
var sample = [{"name":"hello world","children":[{"name":"fruits","children":[]},{"name":"vegetables","children":[]},{"name":"meats","children":[{"name":"pork","children":[]},{"name":"beef","children":[]},{"name":"chicken","children":[{"name":"organic","children":[]},{"name":"farm raised","children":[]}]}]}]},{"name":"second folder","children":[]},{"name":"third folder","children":[{"name":"breads","children":[]},{"name":"coffee","children":[{"name":"latte","children":[]},{"name":"cappucino","children":[]},{"name":"mocha","children":[]}]}]}]
function tree(data, parent) {
const ul = document.createElement('ul');
data.forEach(el => {
const li = document.createElement('li');
li.textContent = el.name;
ul.appendChild(li);
if (el.children.length) {
tree(el.children, li)
}
})
parent.appendChild(ul)
}
const parent = document.getElementById('root')
tree(sample, parent)
<div id="root"></div>

var sample = [{"name":"hello world","children":[{"name":"fruits","children":[]},{"name":"vegetables","children":[]},{"name":"meats","children":[{"name":"pork","children":[]},{"name":"beef","children":[]},{"name":"chicken","children":[{"name":"organic","children":[]},{"name":"farm raised","children":[]}]}]}]},{"name":"second folder","children":[]},{"name":"third folder","children":[{"name":"breads","children":[]},{"name":"coffee","children":[{"name":"latte","children":[]},{"name":"cappucino","children":[]},{"name":"mocha","children":[]}]}]}]
level = 0;
var hyphens = '';
function recursive_loop(s) {
console.log(hyphens + s.name);
var c = s.children;
if (c.length) hyphens += '-';
var empty = false;
for (let i = 0; i < c.length; i++) {
if (c[i].children) {
recursive_loop(c[i]);
}
if (c[i].children.length)
empty = true;
}
if (empty) hyphens = '';
}
for (let i = 0; i < sample.length; i++) {
recursive_loop(sample[i]);
}

We use object-scan foe many data processing / traversal tasks. It's powerful once you wrap your head around it. Here is how you could solve your question
// const objectScan = require('object-scan');
const display = (input) => objectScan(['**'], {
reverse: false,
rtn: 'entry',
filterFn: ({ value }) => typeof value === 'string'
})(input)
.map(([k, v]) => `${'-'.repeat(k.length / 2 - 1)}${v}`);
const sample = [{ name: 'hello world', children: [{ name: 'fruits', children: [] }, { name: 'vegetables', children: [] }, { name: 'meats', children: [{ name: 'pork', children: [] }, { name: 'beef', children: [] }, { name: 'chicken', children: [{ name: 'organic', children: [] }, { name: 'farm raised', children: [] }] }] }] }, { name: 'second folder', children: [] }, { name: 'third folder', children: [{ name: 'breads', children: [] }, { name: 'coffee', children: [{ name: 'latte', children: [] }, { name: 'cappucino', children: [] }, { name: 'mocha', children: [] }] }] }];
const result = display(sample);
result.forEach((l) => console.log(l));
// => hello world
// => -fruits
// => -vegetables
// => -meats
// => --pork
// => --beef
// => --chicken
// => ---organic
// => ---farm raised
// => second folder
// => third folder
// => -breads
// => -coffee
// => --latte
// => --cappucino
// => --mocha
.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

Related

Filter with different levels of depth

I have a menu with this structure
item1
item2
childrenOfItem2
childrenOfchildren1
childrenOfchildren2
HELLOchildrenOfchildren3
childrenOfItem2
childrenOfItem2
HELLOitem3
item4
childrenOfItem4
HELLOchildrenOfItem4
item5
childrenOfItem5
So, Id' like to get all the items that have the word "HELLO" and what I'm doing is a loop over the first items, then, another loop and then another loop, is there any other way of doing it? Since let's say that if we add another level of depth in the menu it will not work,
Thank you!
Edited: adding JS for better understanding
const matchName = (item, word) =>
item?.title?.toLowerCase()?.includes(word?.toLowerCase());
const filter = (word = "", arr = []) => {
const listOfItems = [];
arr.forEach((item) => {
if (matchName(item, word)) {
listOfItems.push(item);
} else if (item?.children?.length > 0) {
const newSubItem = [];
item.children.forEach((subItem) => {
if (matchName(subItem, word)) {
newSubItem.push(subItem);
} else if (subItem?.children?.length > 0) {
const newSubSubItems = [];
subItem.children.forEach((subsubItem) => {
if (matchName(subsubItem, word)) {
newSubSubItems.push(subsubItem);
}
});
if (newSubSubItems?.length > 0) {
newSubItem.push({ ...subItem, children: newSubSubItems });
}
}
});
if (newSubItem?.length > 0) {
listOfItems.push({ ...item, children: newSubItem });
}
}
});
return listOfItems;
};
Sample of arr received as parameter in the fnc:
const list = [
{
id: "41",
title: "sample",
children: [
{
id: "42",
title: "sample",
children: [
{
id: "43",
title: "sample",
children: [],
},
{
id: "44",
title: "sample",
children: [],
},
{
id: "45",
title: "sample",
children: [],
},
],
},
{
id: "46",
title: "sample",
children: [
{
id: "47",
title: "sample",
children: [],
},
{
id: "48",
title: "sample",
children: [],
},
],
},
],
},
{
id: "29",
title: "sample",
children: [
{
id: "30",
title: "sample",
children: [],
},
{
id: "49",
title: "sample",
children: [],
},
{
id: "31",
title: "sample",
children: [],
},
],
},
];
If you really don't want to change your structure and flatten your list, you can loop them dynamically, just use a function and call it within itself.
Here's an example using the const list you provided:
let found = false,
loop = (items, filter) => {
found = false;
let results = items.filter(a => filter(a));
if(results.length > 0) {
found = true;
return results;
}
if(!found && items && items.length > 0) {
for(let i = 0; i < items.length && !found; i++) {
if(items[i] && items[i].children && items[i].children.length > 0)
results = loop(items[i].children, filter);
}
}
return results;
};
let items = loop(list, item => item.id && item.id == "48");
In the example above we filter the list dynamically, iterating each and every item in the list and return the first item we find that matches a provided filter (2nd parameter).
You can use this to do pretty much whatever you'd like and can add arguments to add the menu "level" you're currently on, the parents, etc.
Note that this might get a bit slow if the list goes very deep, wrote it quickly and didn't tested outside of your provided list.
Ideally I would change the structure in order to make it easier to work with but if you must keep it this way, this works.
You could find the object (without children) and get a flat array.
const
find = (array, title) => array.flatMap(({ children, ...o }) => [
...(o.title.includes(title) ? [o]: []),
...find(children, title)
]),
list = [{ id: "41", title: "sample", children: [{ id: "42", title: "sample", children: [{ id: "43", title: "sample", children: [] }, { id: "44", title: "sample", children: [] }, { id: "45", title: "sample", children: [] }] }, { id: "46", title: "no sample", children: [{ id: "47", title: "sample", children: [] }, { id: "48", title: "sample", children: [] }] }] }, { id: "29", title: "sample", children: [{ id: "30", title: "sample", children: [] }, { id: "49", title: "no sample", children: [] }, { id: "31", title: "sample", children: [] }] }];
console.log(find(list, 'no sample'));
console.log(find(list, 'ample'));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Build a Tree from Flat Array

This is a list that is returned by API and need to convert this to a Tree which can display so a user can select the required permissions.
const list = [
{
resource: 'User',
action: 'Create',
id: 1,
},
{
resource: 'User',
action: 'Edit',
id: 2,
},
{
resource: 'User',
action: 'Delete',
id: 3,
},
{
resource: 'Utility.Rule',
action: 'Create',
id: 4,
},
{
resource: 'Utility.Rule',
action: 'Edit',
id: 5,
},
{
resource: 'Utility.Config',
action: 'Create',
id: 6,
},
];
And need to get converted to a Tree
{
"id": "root",
"name": "MyTree",
"children": [
{
"id": "User",
"name": "User",
"children": [
{
"id": "1",
"name": "Create"
},
{
"id": "2",
"name": "Edit"
},
{
"id": "3",
"name": "Delete"
}
]
},
{
"id": "Utility",
"name": "Utility",
"children": [
{
"id": "Rule",
"name": "Rule",
"Children": [
{
"id": "4",
"name": "Create"
},
{
"id": "5",
"name": "Edit"
}
]
},
{
"id": "Config",
"name": "Config",
"children": [
{
"id": "6",
"name": "Create"
}
]
}
]
}
]
}
I have tried different methods like Map and reduce but not getting desired output.
const arrayToTree = (arr, parent = 'User') =>
arr.filter(item => item.parent === parent)
.map(child => ({ ...child, children: arrayToTree(arr,
child.index) }));
arrayToTree(list, 'User')
//This gives me stackoverflow error.
Other option I tried was to build first the map
function list_to_tree(list) {
var map = {}, node, roots = [], i;
for (i = 0; i < list.length; i += 1) {
map[list[i].id] = i; // initialize the map
list[i].children = []; // initialize the children
}
for (i = 0; i < list.length; i += 1) {
node = list[i];
if (node.parentId !== "0") {
// if you have dangling branches check that map[node.parentId] exists
list[map[node.parentId]].children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
It would be great if I can get some hit how to construct the main node and then add the children.
The solution I came up with was to loop through the list, splitting each resource by '.' and adding a node for each resource directory if not already within the parent node's children
const list = [
{ resource: 'User', action: 'Create', id: 1 },
{ resource: 'User', action: 'Edit', id: 2 },
{ resource: 'User', action: 'Delete', id: 3 },
{ resource: 'Utility.Rule', action: 'Create', id: 4 },
{ resource: 'Utility.Rule', action: 'Edit', id: 5 },
{ resource: 'Utility.Config', action: 'Create', id: 6 },
];
function arrayToTree(list, rootNode = { id: 'root', name: 'MyTree' }) {
const addChild = (node, child) => ((node.children ?? (node.children = [])).push(child), child);
for (const { id, action, resource } of list) {
const path = resource.split('.');
const node = path.reduce(
(currentNode, targetNodeId) =>
currentNode.children?.find(({ id }) => id === targetNodeId) ??
addChild(currentNode, { id: targetNodeId, name: targetNodeId }),
rootNode
);
addChild(node, { id: String(id), name: action });
}
return rootNode;
}
console.log(arrayToTree(list));

looping through a json tree object and create new array

Is there a way good way JS/ES6 to loop through an object and it's children and creating new object tree array.
I have this json tree object:
[
{
id: "001",
deparmentsIds: [
"002",
"003"
],
details: {
parentDeparmentsId: null,
name: "Top"
}
},
{
id: "002",
deparmentsIds:[
"004"
],
details: {
parentDeparmentsId: ["001"],
name: "Operations"
}
},
{
id: "003",
deparmentsIds:[]
details: {
parentDeparmentsId: ["001"],
name: "Support"
}
},
{
id: "004",
deparmentsIds:[]
details: {
parentDeparmentsId: ["002"],
name: "Support operations"
}
}
]
I want to create new object array tree that looks like this:
You could create recursive function with reduce and map method to create nested object structure.
const data = [{"id":"001","deparmentsIds":["002","003"],"details":{"parentDeparmentsId":null,"name":"Top"}},{"id":"002","deparmentsIds":["004"],"details":{"parentDeparmentsId":"001","name":"Operations"}},{"id":"003","deparmentsIds":[],"details":{"parentDeparmentsId":"001","name":"Support"}},{"id":"004","deparmentsIds":[],"details":{"parentDeparmentsId":"002","name":"Support operations"}}]
function tree(input, parentId) {
return input.reduce((r, e) => {
if (e.id == parentId || parentId == undefined && e.details.parentDeparmentsId == null) {
const children = [].concat(...e.deparmentsIds.map(id => tree(input, id)))
const obj = {
[e.details.name]: children
}
r.push(obj)
}
return r;
}, [])
}
const result = tree(data)
console.log(result)
You could collect all information in an object with a single loop and return only the nodes with no parent.
function getTree(data, root) {
var o = {};
data.forEach(({ id, details: { parentDeparmentsId: parent, name } }) => {
var temp = { id, name };
if (o[id] && o[id].children) {
temp.children = o[id].children;
}
o[id] = temp;
o[parent] = o[parent] || {};
o[parent].children = o[parent].children || [];
o[parent].children.push(temp);
});
return o[root].children;
}
var data = [{ id: "001", deparmentsIds: ["002", "003"], details: { parentDeparmentsId: null, name: "Top" } }, { id: "002", deparmentsIds: ["004"], details: { parentDeparmentsId: ["001"], name: "Operations" } }, { id: "003", deparmentsIds: [], details: { parentDeparmentsId: ["001"], name: "Support" } }, { id: "004", deparmentsIds: [], details: { parentDeparmentsId: ["002"], name: "Support operations" } }],
tree = getTree(data, null);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Create a nested JSON based on an array of JSON objects

I have an array of JSON objects which is something like:
var fileArray = [
{ name: "file1", path: "/main/file1" },
{ name: "file2", path: "/main/folder2/file2" },
{ name: "file4", path: "/main/folder3/file4" },
{ name: "file5", path: "/main/file5" },
{ name: "file6", path: "/main/file6" }
];
What I want it to look like eventually is:
fileTree = [
{
"name": "file1",
"children": []
},
{
"name": "folder1"
"children": [
{
"name": "folder2",
"children": [
{
"name": "file2",
"children": []
}
]
},
{
"name": "folder3",
"children": [
{
"name": "file4",
"children": []
}
]
}
]
},
{
"name": "file5",
"children": []
},
{
"name": "file6",
"children": []
}
];
I tried the solution mentioned in Create a nested UL menu based on the URL path structure of menu items but the first comment to the first answer is exactly the problem I am having. All help is appreciated.
You could use a nested hash table as reference to the same directories and build in the same way the result set.
var fileArray = [{ name: "file1", path: "/main/file1" }, { name: "file2", path: "/main/folder2/file2" }, { name: "file4", path: "/main/folder3/file4" }, { name: "file5", path: "/main/file5" }, { name: "file6", path: "/main/file6" }],
temp = [],
fileTree;
fileArray.forEach(function (hash) {
return function (a) {
a.path.replace('/', '').split('/').reduce(function (r, k) {
if (!r[k]) {
r[k] = { _: [] };
r._.push({ name: k, children: r[k]._ });
}
return r[k];
}, hash);
};
}({ _: temp }));
fileTree = temp[0].children;
console.log(fileTree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
This won't give the exact result you want but I think it's usable:
var fileArray = [
{ name: "file1", path: "/main/file1" },
{ name: "file2", path: "/main/folder2/file2" },
{ name: "file4", path: "/main/folder3/file4" },
{ name: "file5", path: "/main/file5" },
{ name: "file6", path: "/main/file6" }
];
var tree = fileArray.reduce(function (b, e) {
var pathbits = e.path.split("/");
var r = b;
for (var i=1; i < pathbits.length - 1; i++) {
bit = pathbits[i];
if (!r[bit]) r[bit] = {};
r = r[bit];
}
if (!r.files) r.files = [];
r.files.push(e.name);
return b;
}, {});
console.log(tree);

Order array items by property and keep order of original array

I wrote this function to group an array by one or more properties:
var groupBy = function (fields, data) {
var groups = {};
for (var i = 0; i < data.length; i++) {
var item = data[i];
var container = groups;
for (var j = 0; j < fields.length; j++) {
var groupField = fields[j];
var groupName = item[groupField];
if (!container[groupName]) {
container[groupName] = j < fields.length - 1 ? {} : [];
}
container = container[groupName];
}
container.push(item);
}
return groups;
};
For example if I use this input
var animals = [
{type: "cat", name: "Max"},
{type: "dog", name: "Charlie"},
{type: "cat", name: "Zoe"},
{type: "dog", name: "Abby"},
{type: "cat", name: "Molly"}
];
var groupedAnimals = groupBy(["type"], animals);
I get this output:
{
"cat": [
{
"type": "cat",
"name": "Max"
},
{
"type": "cat",
"name": "Zoe"
},
{
"type": "cat",
"name": "Molly"
}
],
"dog": [
{
"type": "dog",
"name": "Charlie"
},
{
"type": "dog",
"name": "Abby"
}
]
}
Everything ok so far... the problem is that I need the keys to reflect the order of the original input-array. So if the first item was a cat, and I iterate over the group-keys I need the first key to be the cats as well. Since objects in JS are explicitly not ordered, I can not guarantee the correct order. How can I achieve that?
Edit:
I guess the result needs to be something like this:
groupBy(["type", "name"], animals)
should yield:
[
{
"group": "cat",
"items": [
{
"group": "max",
"items": [
{
"type": "cat",
"name": "Max"
}
]
},
{
"group": "Zoe",
"items": [
{
"type": "cat",
"name": "Zoe"
}
]
},
{
"group": "Molly",
"items": [
{
"type": "cat",
"name": "Molly"
}
]
}
]
},
{
"group": "dog",
"items": [
{
"group": "Charlie",
"items": [
{
"type": "dog",
"name": "Charlie"
}
]
},
{
"group": "Abby",
"items": [
{
"type": "dog",
"name": "Abby"
}
]
}
]
}
]
You could use a completely dynamic function for grouping various keys and depth.
This proposal works with a hash table and an array for each level.
function groupBy(keys, array) {
var result = [];
array.forEach(function (a) {
keys.reduce(function (r, k) {
if (!r[a[k]]) {
r[a[k]] = { _: [] };
r._.push({ group: a[k], items: r[a[k]]._ });
}
return r[a[k]];
}, this)._.push(a);
}, { _: result });
return result;
}
var animals = [{ type: "cat", name: "Max" }, { type: "dog", name: "Charlie" }, { type: "cat", name: "Zoe" }, { type: "dog", name: "Abby" }, { type: "cat", name: "Molly" }];
console.log(groupBy(["type", "name"], animals));
console.log(groupBy(["type"], animals));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You could alter your function a tiny bit so that it adds a special property at each group level, which lists the keys in the order they should be iterated:
var groupBy = function (fields, data) {
var groups = { _keys: [] };
// ^^^^^^^^^
for (var i = 0; i < data.length; i++) {
var item = data[i];
var container = groups;
for (var j = 0; j < fields.length; j++) {
var groupField = fields[j];
var groupName = item[groupField];
if (!container[groupName]) {
container[groupName] = j < fields.length - 1 ? { _keys: [] } : [];
// ^^^^^^^^^
container._keys.push(groupName);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
container = container[groupName];
}
container.push(item);
}
return groups;
};
var animals = [
{type: "cat", name: "Max"},
{type: "dog", name: "Charlie"},
{type: "cat", name: "Zoe"},
{type: "dog", name: "Abby"},
{type: "cat", name: "Molly"}
];
var groupedAnimals = groupBy(["type"], animals);
console.log(groupedAnimals);
// Output types in order:
console.log('output types in fixed order:');
groupedAnimals._keys.forEach(function (key, i) {
console.log(i, key, groupedAnimals[key]);
});
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can change your result data structure to array of arrays so you can keep order.
var animals = [
{type: "cat", name: "Max"},
{type: "dog", name: "Charlie"},
{type: "cat", name: "Zoe"},
{type: "dog", name: "Abby"},
{type: "cat", name: "Molly"}
];
var result = []
animals.forEach(function(e) {
if(!this[e.type]) {
this[e.type] = [e.type, []]
result.push(this[e.type])
}
this[e.type][1].push(e)
}, {})
console.log(result)
To group by multiple fields you can pass array as first parameter to your function
var animals = [
{type: "cat", name: "Max", i: 2},
{type: "dog", name: "Charlie", i: 2},
{type: "cat", name: "Zoe", i: 2},
{type: "dog", name: "Abby", i: 1},
{type: "cat", name: "Molly", i: 2}
];
function groupBy(fields, data) {
var result = []
data.forEach(function(e) {
var group = fields.map(a => e[a]).join('-')
if (!this[group]) {
this[group] = [group, []]
result.push(this[group])
}
this[group][1].push(e)
}, {})
return result
}
console.log(groupBy(['type', 'i'], animals))

Categories