Build a Tree from Flat Array - javascript

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));

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; }

Javascript Recurse JSON array with children objects

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

Create an array based on two arrays

I have two arrays details and options that coming from different sources (web api requests)
details: [
{ id: 'groups', option: true },
{ id: 'category', option: false }
]
options: {
groups: [
{ id: 'g1' },
{ id: 'g2' }
],
category: [
{ id: 'c1' },
{ id: 'c2' }
],
other: [
{ id: 'o1' },
{ id: 'o2' }
],
}
I want to combine these tow arrays like
combined: [
groups:
{
options:[
{ id: 'g1' },
{ id: 'g2' }
],
details: { option: true}
},
category:
{
options: [
{ id: 'c1' },
{ id: 'c2' }
],
details: { option: false}
},
]
Basically if any id from details is matching to options property it should go in to new array under the same property name and all details except id goes to related details property.
What is the best way of doing that? Is lodash can handle that ?
If you only want the items in both options and details (intersection):
var details = [
{ id: 'groups', option: true },
{ id: 'category', option: false }
]
var options = {
groups: [
{ id: 'g1' },
{ id: 'g2' }
],
category: [
{ id: 'c1' },
{ id: 'c2' }
],
other: [
{ id: 'o1' },
{ id: 'o2' }
]
}
var combined = {};
details.forEach(({id: id, option: option}) => {
if (options[id]) {
combined[id] = combined[id] || {};
combined[id].options = options[id];
combined[id].details = {option: option};
}
})
console.log(JSON.stringify(combined, null, "\t"))
/*
{
"groups": {
"options": [
{
"id": "g1"
},
{
"id": "g2"
}
],
"details": {
"option": true
}
},
"category": {
"options": [
{
"id": "c1"
},
{
"id": "c2"
}
],
"details": {
"option": false
}
}
}
*/
If you want to retain all items from options and details whether or not they match (union):
var details = [
{ id: 'groups', option: true },
{ id: 'category', option: false }
]
var options = {
groups: [
{ id: 'g1' },
{ id: 'g2' }
],
category: [
{ id: 'c1' },
{ id: 'c2' }
],
other: [
{ id: 'o1' },
{ id: 'o2' }
]
}
var combined = {};
Object.keys(options).forEach(id => {
combined[id] = {};
combined[id].options = options[id];
})
details.forEach(({id: id, option: option}) => {
combined[id] = combined[id] || {};
combined[id].details = {option: option};
})
console.log(JSON.stringify(combined, null, "\t"))
/*
{
"groups": {
"options": [
{
"id": "g1"
},
{
"id": "g2"
}
],
"details": {
"option": true
}
},
"category": {
"options": [
{
"id": "c1"
},
{
"id": "c2"
}
],
"details": {
"option": false
}
},
"other": {
"options": [
{
"id": "o1"
},
{
"id": "o2"
}
]
}
}
*/
You need to use [] notation to push details values.
options['groups']['details'] = true
options['groups']['category'] = false
Here's the solution for your problem
var details= [
{ id: 'groups', option: true },
{ id: 'category', option: false }
];
var options= {
groups: [
{ id: 'g1' },
{ id: 'g2' }
],
category: [
{ id: 'c1' },
{ id: 'c2' }
],
other: [
{ id: 'o1' },
{ id: 'o2' }
],
};
var combined = {};
for (var i= 0;i<details.length;i++){
var obj = {}
obj.options=options[details[i].id];
obj.details = {};
obj.details.option=details[i].option;
combined[details[i].id] = obj;
}
console.log(combined)

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);

How can I convert a JSON to another

I'm receiving a JSON from a Laravel API in this way:
[
{
"id":48,
"parentid":0,
"title":"Item 1",
"child_content":[
{
"id":49,
"parentid":48,
"title":"Itema 1.1",
},
{
"id":52,
"parentid":48,
"title":"Item 1.2",
}
]
},
{
"id":58,
"parentid":0,
"title":"Item 2",
"child_content":[
{
"id":59,
"parentid":58,
"title":"Itema 2.1",
},
{
"id":60,
"parentid":58,
"title":"Item 2.2",
}
]
}
]
and what I need is change the JSON into this:
{
"data":
[
{
"data":
{
"id":68,
"parentid":0,
"title":"Item 1"
},
"children":
[
{
"data":
{
"id":69,
"parentid":68,
"title":"Item 1.1"
},
},
{
"data":
{
"id":69,
"parentid":68,
"title":"Item 1.2"
}
}
]
}
]
}
I've been dealing with this... but I'm not able to find the way to do this properly...
How can I do this in PHP or Javascript / TypeScript (Angular 2).
Thank you in advance.
This should achieve your goal. Basically I'm just grabbing child_content, renaming it to children and copying the 3 other attributes. The children.map iteration is putting the existing data inside an object with a key of data:
const input = [{"id":48,"parentid":0,"title":"Item 1","child_content":[{"id":49,"parentid":48,"title":"Itema 1.1"},{"id":52,"parentid":48,"title":"Item 1.2"}]},{"id":58,"parentid":0,"title":"Item 2","child_content":[{"id":59,"parentid":58,"title":"Itema 2.1"},{"id":60,"parentid":58,"title":"Item 2.2"}]}]
const output = {
data: input.map((data) => {
const {
child_content: children,
id,
parentId,
title,
} = data;
return {
id,
parentId,
title,
children: children.map(data => ({data})),
};
})
}
console.log(output);
You can use JavaScript Array.prototype.map():
var json = [{"id": 48,"parentid": 0,"title": "Item 1","child_content": [{"id": 49,"parentid": 48,"title": "Itema 1.1",}, {"id": 52,"parentid": 48,"title": "Item 1.2",}]}, {"id": 58,"parentid": 0,"title": "Item 2","child_content": [{"id": 59,"parentid": 58,"title": "Itema 2.1",}, {"id": 60,"parentid": 58,"title": "Item 2.2",}]}],
result = {
data: json.map(function (item) {
return {
data: {
id: item.id,
parentid: item.parentid,
title: item.title
},
children: item.child_content.map(function (childItem) {
return {
data: {
id: childItem.id,
parentid: childItem.parentid,
title: childItem.title
}
}
})
};
})
};
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Assuming that the differences in the two datasets are due to copy/paste from different datasets, you can use the array map method to transform the data for you.
map works by iterating through each item of an array, and allows you to return a new item in the shape which you'd like.
var input = [{"id":48,"parentid":0,"title":"Item 1","child_content":[{"id":49,"parentid":48,"title":"Itema 1.1"},{"id":52,"parentid":48,"title":"Item 1.2"}]},{"id":58,"parentid":0,"title":"Item 2","child_content":[{"id":59,"parentid":58,"title":"Itema 2.1"},{"id":60,"parentid":58,"title":"Item 2.2"}]}];
var output = {
data: input.map(function(parent) {
// return a new object which contains the properties which you need
return {
data: {
id: parent.id,
parentid: parent.parentid,
title: parent.title
},
// children is an array, so we can use map again to transform them
children: parent.child_content.map(function(child) {
return {
data: {
id: child.id,
parentid: parent.id,
title: child.title
}
};
})
}
})
}
console.log(output);
You could convert the structure without mutating the original object with iterating and recursive calls of the convert function.
It works for any depth.
function convert(o) {
var temp = { data: {} };
Object.keys(o).forEach(function (k) {
if (k === 'child_content') {
temp.children = o[k].map(convert);
} else {
temp.data[k] = o[k];
}
});
return temp;
}
var data = [{ id: 48, parentid: 0, title: "Item 1", child_content: [{ id: 49, parentid: 48, title: "Itema 1.1" }, { id: 52, parentid: 48, title: "Item 1.2" }] }, { id: 58, parentid: 0, title: "Item 2", child_content: [{ id: 59, parentid: 58, title: "Itema 2.1" }, { id: 60, parentid: 58, title: "Item 2.2" }] }],
result = { data: data.map(convert) };
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Ok, what I finally did is maybe not too much elegant... but it works, probably I should create a recursive function to manage different nested levels. I took the #RobM solution and replicated the children functionality at the second level like this:
convert(input)
{
const output =
{
data: input.map((data) =>
{
const { children: children } = data;
delete data.children;
return {
data,
children: children.map(data =>
{
const { children: children } = data;
delete data.children;
return {
data,
children: children.map(data => ({data})),
};
}),
};
})
}
return output.data;
}

Categories