JS recursive function with nested children array - javascript

i have tree array of nested objects. Depending on the type of element I want to give it the necessary icon.
const treeData = [
{
id: 1,
type: "FOLDER",
children: [
{
id: 2,
type: "FILE"
},
{
id: 2,
type: "FOLDER",
children: []
},
]
}
]
Unlimited number of nesting possible in folders. Output should be like that.
const treeData = [
{
id: 1,
type: "FOLDER",
icon: "folder-icon"
children: [
{
id: 2,
type: "FILE",
icon: "file-icon"
},
{
id: 2,
type: "FOLDER",
children: []
icon: "file-icon"
},
]
}
]
As i understand i should use recursive map function with CHILDREN check. But i can't reach the proper result.

You could use map method and create a recursive function that will take type and convert it lowercase to create icon name and add it to the new object.
const data = [{"id":1,"type":"FOLDER","children":[{"id":2,"type":"FILE"},{"id":2,"type":"FOLDER","children":[]}]}]
function addIcons(data) {
return data.map(({ type, children = [], ...rest }) => {
const o = { ...rest, type }
if(type) o.icon = `${type.toLowerCase()}-icon`;
if (children.length) o.children = addIcons(children)
return o
})
}
console.log(addIcons(data))

You can create a function like this:
const iconCalculator = object => {
if (object.children) object.children = object.children.map(child=>iconCalculator(child))
return {...object, icon: 'whatever'}
}
And then map your tree like this treeData.map(child=>iconCalculator(child))

You could map by using a new object with recursive children.
const
addIcon = o => ({
...o,
icon: `${o.type.toLowerCase()}-icon`,
...(Array.isArray(o.children)
? { children: o.children.map(addIcon) }
: {}
)
}),
treeData = [{ id: 1, type: "FOLDER", children: [{ id: 2, type: "FILE" }, { id: 2, type: "FOLDER", children: [] }] }],
result = treeData.map(addIcon);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

You can use forEach method something like this
const treeData = [{id: 1,type: "FOLDER",children: [{id: 2,type: "FILE"},{id: 2,type: "FOLDER",children: []},]}]
function addIcon(data) {
if (Array.isArray(data)) {
data.forEach(value => {
value.icon = value.type.toLowerCase() + '-icon'
if (Array.isArray(value.children)) {
addIcon(value.children)
}
})
}
}
addIcon(treeData)
console.log(treeData)
P.S:- This mutates original array if you don't want you can make a copy every time and return the newly created copy

Related

How to transform nested object of objects into array of objects

Hi all I have the following code
the data that I want to transform.
const obj = {
numbers: {
label: "main numbers",
pageTitle: "Numbers",
key: "1",
items: {
firstNumber: {
label: "first number",
pageTitle: "first",
key: "first"
},
secondNumber: {
label: "second number",
pageTitle: "second",
key: "second"
}
}
},
letters: {
label: "main Letters",
pageTitle: "Letters",
key: "2",
items: {
firstLetter: {
label: "first Letter",
pageTitle: "first",
key: "first"
}
}
},
signs: {
label: "main sign",
pageTitle: "Sign",
key: "3"
}
};
In my obj variable I have 3 other objects
numbers object which has items property which includes 2 other objects.
letters object which has items property which includes only one object.
signs object.
I need to transform my obj to the following way.
[
{
label:"main numbers",
pageTitle:"Numbers",
key:1,
children: [{label,pageTitle,key},{label,pageTitle,key}]
},
{
label:"main Letters",
pageTitle:"Letters",
key:1,
children: [{label,pageTitle,key}]
},
{
label:"main sign",
pageTitle:"Sign",
key:1,
children: []
},
]
for that transformation, I wrote the following code.
const transformedData = Object.values(obj).map((menuitem) => menuitem);
const data = [];
transformedData?.map((x) => {
const newData = {};
newData.label = x.label;
newData.pageTitle = x.pageTitle;
newData.key = x.key;
newData.children = x?.Object?.values(items)?.map((el) => {
newData.children.label = el.label;
newData.children.pageTitle = el.pageTitle;
newData.children.key = el.key;
});
data.push(newData);
});
Everything was working, but for children instead of printing an array it prints undefined.
Please help me to resolve this issue.
I created a function for your case.
const convert = data =>
Object.values(data)?.map(x => ({
label: x.label,
pageTitle :x.pageTitle ,
key: x.pathname,
children: x.items
? Object.values(x.items || {}).map(el => ({ label: el.label,
key:el.pathname,pageTitle:el.pageTitle }))
: null,
}));
You can use like const items = convert(obj).
xdoesn't have Objects. Change it to:
newData.children = Object.values(x.items)?.map(/*...*/);
Is this what you're after?
const transformedData = Object.values(obj).map((menuitem) => menuitem);
const data = [];
transformedData?.map((x) => {
const newData = {};
newData.label = x.label;
newData.pageTitle = x.pageTitle;
newData.key = x.key;
if(x.hasOwnProperty('items')){
newData.children = Object.values(x.items).map((el) => {
const obj={
label:el.label,
pageTitle:el.pageTitle,
key:el.key
}
return obj
})};
data.push(newData);
});
console.log(data)
Your code return undefined because inside map you didn't return anything so newData.children was never populated with anything.
Also, I think accessing and assigning newData.children.label was problematic since there was no newData.children yet. So we declare a temp obj inside map and we return it
Lastly we need to check if items is a property that exists in the first place.

Find nested object by Id using for loop in Javascript

I'm trying to find a specific Object in a nested Object by id and wrote this function, which works like a charm:
const findNestedObjById = (tree, myFunction, id) => {
if(tree.attributes.node_id === id){
myFunction(tree)
} else{
if(tree.children){
tree.children.forEach(child => {
findNestedObjById(child, myFunction, id)
});
}
}
};
const doThat = (tree) => {
console.log("Got it: " + tree.name)
}
findNestedObjById(myObj, doThat, "0.1.2.1");
But i want to be able to get the "path" of the object (e.g. myObj.children[0].children[2]) (The children property of my object is an array)
So I wanted to rewrite the function using a fori loop instead of a foreach, so that I could later add the index of the array (saved in i of the fori loop at the time) to a path string.
So I wanted to start with this function:
const findWithFori = (tree, myFunction, id) => {
if(tree.attributes.node_id === id){
myFunction(tree)
} else{
if(tree.children){
for (let i = 0; i < tree.length; i++) {
const child = tree.children[i];
findNestedObjById(child, myFunction, id)
}
}
}
};
But it doenst work, it's able to locate the object by id, if the inital myObj already has the right id, but it doesn't find nested objects, like the first function does and I don't understand why.
If it helps answerign the question, myObj looks like this btw.:
const myObj = {
name: "Mein zweiter Baum",
attributes: {
node_id: "0"
},
children: [
{
name: "Lorem",
attributes: {
node_id: "0.1",
done: true
},
children: [
{
name: "Ipsum",
attributes: {
node_id: "0.1.1",
done: true
},
children: [
{
name: "Dolor",
attributes: {
node_id: "0.1.1.1",
done: false
}
}
]
},
{
name: "Sit",
attributes: {
node_id: "0.1.2",
done: false
},
children: [
{
name: "Anet",
attributes: {
node_id: "0.1.2.1"
}
}
]
}
]
}
]
};
You could return the indices.
If an item is found return an empty array, or undefined. Inside of some get the result of children and if not undefined add the actual index in front of the array.
const
findNestedObjById = (tree, id, callback) => {
if (tree.attributes.node_id === id) {
callback(tree);
return [];
}
if (tree.children) {
let path;
tree.children.some((child, index) => {
path = findNestedObjById(child, id, callback);
if (path) {
path.unshift(index);
return true;
}
});
return path;
}
},
doThat = tree => {
console.log("Got it: " + tree.name);
},
data = { name: "Mein zweiter Baum", attributes: { node_id: "0" }, children: [{ name: "Lorem", attributes: { node_id: "0.1", done: true }, children: [{ name: "Ipsum", attributes: { node_id: "0.1.1", done: true }, children: [{ name: "Dolor", attributes: { node_id: "0.1.1.1", done: false } }] }, { name: "Sit", attributes: { node_id: "0.1.2", done: false }, children: [{ name: "Anet", attributes: { node_id: "0.1.2.1" } }] }] }] }
console.log(findNestedObjById(data, "0.1.2.1", doThat)); // [0, 1, 0]
.as-console-wrapper { max-height: 100% !important; top: 0; }
I would do this by building atop some reusable functions. We can write a function that visits a node and then recursively visits all its children's nodes. To use this for a find, however, we want to be able to stop once its found, so a generator function would make sense here. We can extend a basic version of this 1 to allow each stop to include not only the values, but also their paths.
Then we can layer on a generic find-path-by-predicate function, testing each node it generates until one matches the predicate.
Finally we can easily write a function using this to search by node_id. It might look like this:
function * visit (value, path = []) {
yield {value, path}
for (let i = 0; i < (value .children || []) .length; i ++) {
yield * visit (value .children [i], path .concat (i))
}
}
const findDeepPath = (fn) => (obj) => {
for (let o of visit (obj)) {
if (fn (o .value)) {return o .path}
}
}
const findPathByNodeId = (id) =>
findDeepPath (({attributes: {node_id}}) => node_id === id)
const myObj = {name: "Mein zweiter Baum", attributes: {node_id: "0"}, children: [{name: "Lorem", attributes: {node_id: "0.1", done: true}, children: [{name: "Ipsum", attributes: {node_id: "0.1.1", done: true}, children: [{name: "Dolor", attributes: {node_id: "0.1.1.1", done: false}}]}, {name: "Sit", attributes: {node_id: "0.1.2", done: false}, children: [{name: "Anet", attributes: {node_id: "0.1.2.1"}}]}]}]}
console .log (findPathByNodeId ('0.1.2.1') (myObj)) //=> [0, 1, 0]
If we want to return the node and the path, it's simply a matter of replacing
if (fn (o .value)) {return o .path}
with
if (fn (o .value)) {return o}
and we would get back something like:
{
value: {attributes: {node_id: "0.1.2.1"}, name: "Anet"},
path: [0, 1, 0],
}
1 A basic version for nodes without their paths might look like this:
function * visit (obj) {
yield obj
for (let child of (obj .children || [])) {
yield * visit (child)
}
}
and we might write a generic search for values matching a predicate with
const findDeep = (fn) => (obj) => {
for (let o of visit (obj)) {
if (fn (o)) {return o}
}
}
Layering in the path handling adds some complexity, but not a great deal.

Combining arrays of objects by matching values

I want to combine two arrays of objects, to make it easier for me to display in HTML. The function should find matching values of keys called "id" in arr1, and "source" in arr2. Here's what it looks like:
let arr1 = [
{id = 1,
name = "Anna"},
{id = 2,
name = "Chris"}
]
let arr2 = [
{childName = "Brian",
{source = 1}},
{childName = "Connie",
{source = 2}}
{childName = "Dory",
{source = 1}}
]
I tried different approaches, with best one being using forEach and filter on the arrays. I'm trying to set up a new property in arr1 objects called "children".
arr1.forEach(el => el.children = arr2.filter(checkMatch));
function checkMatch(child){
for(let i=0;i<arr1.length;i++){
child.childName.source === arr1[i].id
}
}
And this results in adding appropriate children to the first object(Anna has Brian and Dory now) so it's correct, but it also adds the same children to the second object (so Chris has also Brian and Dory).
Where is my mistake here? I'm guessing that the loop doesn't work the way I want it to work, but I don't know which one and how it happens.
Since your syntax for creating the objects of arr1 and arr2 are not valid i tried to guess the structure of your objects.
let arr1 = [
{
id: 1,
name: "Anna"
},
{
id: 2,
name: "Chris"
}
];
let arr2 = [
{
childName: "Brian",
source: 1
},
{
childName: "Connie",
source: 2
},
{
childName: "Dory",
source: 1
}
];
arr2.map((child) => {
for (let parent of arr1) {
if (parent.id == child.source) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(child);
}
}
});
console.log(arr1);
There were problems with your JSON, but I tidied and here is option using map and filter
const arr1 = [{
id: 1,
name: "Anna"
},
{
id: 2,
name: "Chris"
}];
const arr2 = [{
childName: "Brian",
parent: {
source: 1
}
},
{
childName: "Connie",
parent: {
source: 2
}
},
{
childName: "Dory",
parent: {
source: 1
}
}];
let merge = arr1.map(p => {
p.children = arr2.filter(c => c.parent.source === p.id).map(c => c.childName);
return p;
});
console.log(merge);
Your json have some problems you should use
:
instead of
=
Also some Braces make the structure incorrect, but I think what you want to do here is fill a children sub array with the childNames of the subject here is my approach:
var json =
[
{
"id" : 1,
"name" : "Anna"
},
{
"id" : 2,
"name" : "Chris"
}
];
var subJson = [
{
"childName" : "Brian",
"source" : 1
},
{
"childName" : "Connie",
"source" : 2
},
{"childName" : "Dory",
"source" : 1
}
];
var newJson = [];
$.each(json,function(index1,item){
newJson.push({"id":item.id,"name":item.name, "children": []});
$.each(subJson,function(index2,subitem){
if(subitem.source == item.id){
newJson[newJson.length - 1].children.push({"childName":subitem.childName}) ;
}
})
})
console.log(newJson);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Hope it helps
The below uses Map for storage and convenient lookup of parents.
const parents = [
{
id: 1,
name: "Anna"
},
{
id: 2,
name: "Chris"
}
]
const children = [
{
childName: "Brian",
source: 1
},
{
childName: "Connie",
source: 2
},
{
childName: "Dory",
source: 1
}
]
// Create a map for easy lookup of parents.
const parentMap = new Map()
// Add parents to the map, based on their id.
parents.forEach(parent => parentMap.set(parent.id, parent))
// Add children to their parents.
children.forEach((child) => {
// Get the parent from the map.
const parent = parentMap.get(child.source)
// Handle parent not found error.
if (!parent) { return console.error('parent not found for', child.childName)}
// Create the children array if it doesn't already exist.
parent.children = parent.children || []
// Add the child to the parent's children array.
parent.children.push(child)
})
// Output the result.
Array.from(parentMap).forEach(parent => console.log(parent[1]))
Result:
{
id: 1,
name: 'Anna',
children: [
{ childName: 'Brian', source: 1 },
{ childName: 'Dory', source: 1 }
]
}
{
id: 2,
name: 'Chris',
children: [
{ childName: 'Connie', source: 2 }
]
}

Tree Structure From Underscore Delimited Strings

Good day,
I need to convert strings as such:
Process1_Cat1_Cat2_Value1
Process1_Cat1_Cat2_Value2
Process2_Cat1_Cat2_Value1
into a nested array as such:
var d = [{
text: 'Process1',
children: [{
text: 'Cat1',
children: [{
text: 'Cat2',
children: [{
text: 'Value1'
},
{
text: 'Value2'
}]
}]
}]
},
{
text: 'Process2',
children: [{
text: 'Cat1',
children: [{
text: 'Cat2',
children: [{
text: 'Value1'
}]
}]
}]
},
];
The reason why I need to do this is to make use of a treeview to display my data:
https://www.npmjs.com/package/bootstrap-tree-view
I have looked at the following solution but was not able to get it working due to lowdash library throwing errors on the findWhere function:
Uncaught TypeError: _.findWhere is not a function
http://brandonclapp.com/arranging-an-array-of-flat-paths-into-a-json-tree-like-structure/
See below for the code:
function arrangeIntoTree(paths, cb) {
var tree = [];
// This example uses the underscore.js library.
_.each(paths, function(path) {
var pathParts = path.split('_');
pathParts.shift(); // Remove first blank element from the parts array.
var currentLevel = tree; // initialize currentLevel to root
_.each(pathParts, function(part) {
// check to see if the path already exists.
var existingPath = _.findWhere(currentLevel, {
name: part
});
if (existingPath) {
// The path to this item was already in the tree, so don't add it again.
// Set the current level to this path's children
currentLevel = existingPath.children;
} else {
var newPart = {
name: part,
children: [],
}
currentLevel.push(newPart);
currentLevel = newPart.children;
}
});
});
cb(tree);
}
arrangeIntoTree(paths, function(tree) {
console.log('tree: ', tree);
});
Any help will be appreciated!
You could use an iterative by looking for the text at the actual level. If not found create a new object. Return the children array for the next level until the most nested array. Then add the leaf object.
var data = ['Process1_Cat1_Cat2_Value1', 'Process1_Cat1_Cat2_Value2', 'Process2_Cat1_Cat2_Value1'],
result = data.reduce((r, s) => {
var keys = s.split('_'),
text = keys.pop();
keys
.reduce((q, text) => {
var temp = q.find(o => o.text === text);
if (!temp) {
q.push(temp = { text, children: [] });
}
return temp.children;
}, r)
.push({ text });
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

add new data inside redux state tree

I have an app with a tree of nested nodes. all nodes are same type.
{
id: 1,
title: "node_1",
children: [
{
id: 2,
title: "node_2",
children: []
},
{
id: 3,
title: "node_3",
children: []
}
]
}
When user expanded some node (for example node with id === 3) i have to perform request to database and insert response (array children) inside of "children" property of node with id === 3 . So as result app state should be like this:
{
id: 1,
title: "node_1",
children: [
{
id: 2,
title: "node_2",
children: []
},
{
id: 3,
title: "node_3",
children: [
{
id: 4,
title: "node_4",
children: []
},
{
id: 5,
title: "node_5",
children: []
}
]
}
]
}
how can i paste array of children inside node_3 children property?
Given:
const layer1Id = 1;
const layer2Id = 3;
const newArray = [
{
id: 4,
title: "node_4",
children: [],
},
{
id: 5,
title: "node_5",
children: [],
}
];
Then, in the reducer you'll do:
return Object.assign({}, state, { children: state.children.map(child => {
if (child.id !== layer1Id) return child;
return Object.assign({}, child, { children: child.children.map(node => {
if (node.id !== layer2Id) return node;
return Object.assign({}, node, { children: node.children.concat(newArray) });
})});
})});
To make sure you don't mutate the previous state.
If it is dynamically or deeply nested, I'll recommend you to write some recursive function and use that instead.
Edit: here's sample recursive solution (untested). The indices are in order by level (ie: indices[0] refers to first level's id, indices[1] refers to second level's id):
const state = { ... };
const indices = [1, 3, 4, 5, 6];
const newArray = [ ... ];
const recursion = (node, ids, newChildren) => {
let children;
if (ids.length === 0) {
children = newChildren;
} else {
children = node.children.map(child => {
if (child.id !== ids[0]) return child;
return Object.assign({}, child, { children: recursion(child, ids.slice(1), newChildren) });
});
}
return Object.assign({}, node, { children });
};
recursion(state, indecies, newArray);
The suggested approach for relational or normalized data in a Redux store is to organize it in "normalized" fashion, similar to database tables. That will make it easier to manage updates. See http://redux.js.org/docs/FAQ.html#organizing-state-nested-data, How to handle tree-shaped entities in Redux reducers?, and https://github.com/reactjs/redux/pull/1269.
Just iterate through children array and push to correct one .
var id = expandedItemId;
for(var i = 0; i < obj.children.length; i++){
if(obj.id == expandedItemId){
obj.children.push(`data got from server`);
}
}

Categories