Having a senior-moment, and struggling to get a recursive method to work correctly in Javascript.
There are similar Q&A's here, though nothing I see that has helped me so far.
That being said, if there is indeed a duplicate, i will remove this question.
Given the following array of objects:
var collection = [
{
id: 1,
name: "Parent 1",
children: [
{ id: 11, name: "Child 1", children: [] },
{ id: 12, name: "Child 2", children: [] }
]
},
{
id: 2,
name: "Parent 2",
children: [
{
id: 20,
name: "Child 1",
children: [
{ id: 21, name: "Grand Child 1", children: [] },
{ id: 22, name: "Grand Child 2", children: [] }
]
}
]
},
{
id: 3,
name: "Parent 3",
children: [
{ id: 31, name: "Child 1", children: [] },
{ id: 32, name: "Child 2", children: [] }
]
},
];
I've gone through a few attempts though my method seems to return early after going through one level only.
My latest attempt is:
Can someone please point me in the right direction.
function findType(col, id) {
for (i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
if (col[i].children.length > 0) {
return findType(col[i].children, id);
}
}
return null;
}
I am trying to find an object where a given id matches, so looking for id 1 should return the whole object with the name Parent 1. If looking for id 31 then the whole object with the id 31 and name Child 1 should be returned.
This would translate into
var t = findType(collection, 1);
or
var t = findType(collection, 31);
Note I would like help with a pure JavaScript solution, and not a plugin or other library. Though they may be more stable, it won't help with the learning curve. Thanks.
You was close, you need a variable to store the temporary result of the nested call of find and if found, then break the loop by returning the found object.
Without, you return on any found children without iterating to the end of the array if not found at the first time.
function findType(col, id) {
var i, temp;
for (i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
if (col[i].children.length > 0) {
temp = findType(col[i].children, id); // store result
if (temp) { // check
return temp; // return result
}
}
}
return null;
}
var collection = [{ id: 1, name: "Parent 1", children: [{ id: 11, name: "Child 1", children: [] }, { id: 12, name: "Child 2", children: [] }] }, { id: 2, name: "Parent 2", children: [{ id: 20, name: "Child 1", children: [{ id: 21, name: "Grand Child 1", children: [] }, { id: 22, name: "Grand Child 2", children: [] }] }] }, { id: 3, name: "Parent 3", children: [{ id: 31, name: "Child 1", children: [] }, { id: 32, name: "Child 2", children: [] }] }];
console.log(findType(collection, 31));
console.log(findType(collection, 1));
.as-console-wrapper { max-height: 100% !important; top: 0; }
const findType = (ar, id) => {
return ar.find(item => {
if (item.id === id) {
return item;
}
return item.children.find(cItem => cItem.id === id)
})
}
I think this suffice your needs
You need to ask for the "found" object
let found = findType(col[i].children, id);
if (found) {
return found;
}
Look at this code snippet
var collection = [{ id: 1, name: "Parent 1", children: [{ id: 11, name: "Child 1", children: [] }, { id: 12, name: "Child 2", children: [] } ] }, { id: 2, name: "Parent 2", children: [{ id: 20, name: "Child 1", children: [{ id: 21, name: "Grand Child 1", children: [] }, { id: 22, name: "Grand Child 2", children: [] } ] }] }, { id: 3, name: "Parent 3", children: [{ id: 31, name: "Child 1", children: [] }, { id: 32, name: "Child 2", children: [] } ] }];
function findType(col, id) {
for (let i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
if (col[i].children.length > 0) {
let found = findType(col[i].children, id);
if (found) {
return found;
}
}
}
return null;
}
var t = findType(collection, 31);
console.log(t);
.as-console-wrapper {
max-height: 100% !important
}
1.
Actually your function findType return the value null for every id parameter at the node
{ id: 11, name: "Child 1", children: [] }
When you hit return, it stops all the recursion.
You have to check the return value from the nested call of findType function.
2.
Your for loop should looke like
for (let i = 0; i < col.length; i++)
instead of
for (i = 0; i < col.length; i++)
Because without the let you share a same variable i in the nested call of the function findType and the value will be changed for the dad calling function.
The function could be :
function findType(col, id) {
for (let i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
var nested = findType(col[i].children, id);
if (nested) return nested;
}
return null;
}
Related
I'm trying to filter a nested structure, based on a search string.
If the search string is matched in an item, then I want to keep that item in the structure, along with its parents.
If the search string is not found, and the item has no children, it can be discounted.
I've got some code working which uses a recursive array filter to check the children of each item:
const data = {
id: '0.1',
children: [
{
children: [],
id: '1.1'
},
{
id: '1.2',
children: [
{
children: [],
id: '2.1'
},
{
id: '2.2',
children: [
{
id: '3.1',
children: []
},
{
id: '3.2',
children: []
},
{
id: '3.3',
children: []
}
]
},
{
children: [],
id: '2.3'
}
]
}
]
};
const searchString = '3.3';
const filterChildren = (item) => {
if (item.children.length) {
item.children = item.children.filter(filterChildren);
return item.children.length;
}
return item.id.includes(searchString);
};
data.children = data.children.filter(filterChildren);
console.log(data);
/*This outputs:
{
"id": "0.1",
"children": [
{
"id": "1.2",
"children": [
{
"id": "2.2",
"children": [
{
"id": "3.3",
"children": []
}
]
}
]
}
]
}*/
I'm concerned that if my data structure becomes massive, this won't be very efficient.
Can this be achieved in a 'nicer' way, that limits the amount of looping going on? I'm thinking probably using a reducer/transducer or something similarly exciting :)
A nonmutating version with a search for a child.
function find(array, id) {
var child,
result = array.find(o => o.id === id || (child = find(o.children, id)));
return child
? Object.assign({}, result, { children: [child] })
: result;
}
const
data = { id: '0.1', children: [{ children: [], id: '1.1' }, { id: '1.2', children: [{ children: [], id: '2.1' }, { id: '2.2', children: [{ id: '3.1', children: [] }, { id: '3.2', children: [] }, { id: '3.3', children: [] }] }, { children: [], id: '2.3' }] }] },
searchString = '3.3',
result = find([data], searchString);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have to create a tree structure using tree array, but I'm unable to traverse the array correctly. I used the following code:
function fnAppend(param) {
var tree= [ {
"name": "A","children": [
{
"name": "A1","children": [
{
"name": "A2","children": []
},
{
"name": "A3","children": []
}
]
},
{
"name": "B1","children": [
{
"name": "B2","children": []
}
]
}
]
},
{
"name": "B","children": []
},
{
"name": "C","children": [
{
"name": "C1","children": [
{
"name": "C2","children": []
}
]
}
]
}
];
for(var i = 0; i < tree.length; i++){
console.log("Mother : "+tree[i].name);
var childArray = tree[i].children;
for(var j = 0; j < childArray.length; j++){
console.log("Child :"+childArray[j].name);
}
}
}
You could take a recursion and hand over the parent name.
const iter = parent => ({ name, children }) => {
console.log('parent', parent, 'name', name);
if (children.length) children.forEach(iter(name));
};
var tree = [{ name: "A", children: [{ name: "A1", children: [{ name: "A2", children: [] }, { name: "A3", children: [] }] }, { name: "B1", children: [{ name: "B2", children: [] }] }] }, { name: "B", children: [] }, { name: "C", children: [{ name: "C1", children: [{ name: "C2", children: [] }] }] }];
tree.forEach(iter());
.as-console-wrapper { max-height: 100% !important; top: 0; }
A simple recursion method will work.
function children(list){
for(var i = 0; i < list.length; i++){
var name = list[i].name;
console.log(name);
if(list[i].children != undefined){
children(list[i].children);
}
}
}
What it does is: it loops the passed level of array then checks if any of the objects have children if so it calls itself and does that again until no children are found then it continues with next object.
Here's a full JSFiddle.
This is where you need a recursion function.
function traverse(arr) {
for (const branch of arr) {
console.log('Mother:', branch.name);
if (Array.isArray(branch.children) && branch.children.length > 0) {
console.log('Children:', branch.children.map(i => i.name).join(', '));
traverse(branch.children);
}
}
}
traverse(tree);
I have a nested array of objects like this:
let data = [
{
id: 1,
title: "Abc",
children: [
{
id: 2,
title: "Type 2",
children: [
{
id: 23,
title: "Number 3",
children:[] /* This key needs to be deleted */
}
]
},
]
},
{
id: 167,
title: "Cde",
children:[] /* This key needs to be deleted */
}
]
All I want is to recursively find leaves with no children (currently an empty array) and remove the children property from them.
Here's my code:
normalizeData(data, arr = []) {
return data.map((x) => {
if (Array.isArray(x))
return this.normalizeData(x, arr)
return {
...x,
title: x.name,
children: x.children.length ? [...x.children] : null
}
})
}
You need to use recursion for that:
let data = [{
id: 1,
title: "Abc",
children: [{
id: 2,
title: "Type 2",
children: [{
id: 23,
title: "Number 3",
children: [] /* This key needs to be deleted */
}]
}]
},
{
id: 167,
title: "Cde",
children: [] /* This key needs to be deleted */
}
]
function traverse(obj) {
for (const k in obj) {
if (typeof obj[k] == 'object' && obj[k] !== null) {
if (k === 'children' && !obj[k].length) {
delete obj[k]
} else {
traverse(obj[k])
}
}
}
}
traverse(data)
console.log(data)
Nik's answer is fine (though I don't see the point of accessing the children key like that), but here's a shorter alternative if it can help:
let data = [
{id: 1, title: "Abc", children: [
{id: 2, title: "Type 2", children: [
{id: 23, title: "Number 3", children: []}
]}
]},
{id: 167, title: "Cde", children: []}
];
data.forEach(deleteEmptyChildren = o =>
o.children.length ? o.children.forEach(deleteEmptyChildren) : delete o.children);
console.log(data);
If children is not always there, you can change the main part of the code to:
data.forEach(deleteEmptyChildren = o =>
o.children && o.children.length
? o.children.forEach(deleteEmptyChildren)
: delete o.children);
Simple recursion with forEach is all that is needed.
let data = [{
id: 1,
title: "Abc",
children: [{
id: 2,
title: "Type 2",
children: [{
id: 23,
title: "Number 3",
children: [] /* This key needs to be deleted */
}]
}, ]
},
{
id: 167,
title: "Cde",
children: [] /* This key needs to be deleted */
}
]
const cleanUp = data =>
data.forEach(n =>
n.children.length
? cleanUp(n.children)
: (delete n.children))
cleanUp(data)
console.log(data)
This assumes children is there. If it could be missing than just needs a minor change to the check so it does not error out on the length check. n.children && n.children.length
You can do it like this using recursion.
So here the basic idea is in removeEmptyChild function we check if the children length is non zero or not. so if it is we loop through each element in children array and pass them function again as parameter, if the children length is zero we delete the children key.
let data=[{id:1,title:"Abc",children:[{id:2,title:"Type2",children:[{id:23,title:"Number3",children:[]}]},]},{id:167,title:"Cde",children:[]},{id:1}]
function removeEmptyChild(input){
if( input.children && input.children.length ){
input.children.forEach(e => removeEmptyChild(e) )
} else {
delete input.children
}
return input
}
data.forEach(e=> removeEmptyChild(e))
console.log(data)
If I have an array with a bunch of posts sorted any old how:
[
{
id: 1,
name: "first parent post"
},
{
id: 2,
name: "second child of first parent post"
},
{
id: 3,
name: "second parent post"
},
{
id: 4,
name: "first child of first parent post"
},
{
id: 5,
name: "first child of second parent post"
}
]
However, another array decides the structure of the first array based on the ids of the first array:
[
{
id: 1,
parent: 0
},
{
id: 4,
parent: 1
},
{
id: 2,
parent: 1
},
{
id: 3,
parent: 0
},
{
id: 5,
parent: 3
}
]
What would be the most efficient way to sort these so that the first array is sorted by the second array?
I would expect the resulting array to look something like this:
[
{
id: 1,
name: "first parent post",
indent: 0
},
{
id: 4,
name: "first child of first parent post",
indent: 1
},
{
id: 2,
name: "second child of first parent post",
indent: 1
},
{
id: 3,
name: "second parent post",
indent: 0
},
{
id: 5,
name: "first child of second parent post",
indent: 1
}
]
You can order your data array by index and then order it by the order array;
to get the indentation you'll have to track the parent's indentation upstream and add one:
var data = [
{
id: 1,
name: "first parent post"
},
{
id: 2,
name: "second child of first parent post"
},
{
id: 3,
name: "second parent post"
},
{
id: 4,
name: "first child of first parent post"
},
{
id: 5,
name: "first child of second parent post"
}
]
var ord = [
{
id: 1,
parent: 0
},
{
id: 4,
parent: 1
},
{
id: 2,
parent: 1
},
{
id: 3,
parent: 0
},
{
id: 5,
parent: 3
}
]
// first you arrange the data element by index
dataByIndex = data.reduce((ac, x) => {
ac[x.id] = x;
return ac
},[])
// then you order them by the ord array
var res = ord.reduce((ac, x, i) => {
var item = dataByIndex[x.id]
item.indent = x.parent === 0 ? 0 : getParentIndentPlusOne(ac, x.parent)
return [...ac, item]
}, [] )
function getParentIndentPlusOne(ac, id){
var i = ac.length
while (--i > -1){
if (id === ac[i].id) return ac[i].indent + 1
}
}
console.log(res)
You need to generate a tree with the dependencies first and then render the indent for all items of the orginal array.
This proposal works for unsorted data/relations as well.
function getDataWithIndent(data, relation) {
var hash = Object.create(null),
tree = function (data, root) {
var r = [], o = {};
data.forEach(function (a) {
a.children = o[a.id] && o[a.id].children;
o[a.id] = a;
if (a.parent === root) {
r.push(a);
} else {
o[a.parent] = o[a.parent] || {};
o[a.parent].children = o[a.parent].children || [];
o[a.parent].children.push(a);
}
});
return r;
}(relation, 0),
result = [];
data.forEach(function (a) {
hash[a.id] = a;
});
tree.forEach(function iter(indent) {
return function (a) {
hash[a.id].indent = indent;
result.push(hash[a.id]);
Array.isArray(a.children) && a.children.forEach(iter(indent + 1));
};
}(0));
return result;
}
var data = [{ id: 1, name: "first parent post" }, { id: 2, name: "second child of first parent post" }, { id: 3, name: "second parent post" }, { id: 4, name: "first child of first parent post" }, { id: 5, name: "first child of second parent post" }],
relation = [{ id: 1, parent: 0 }, { id: 4, parent: 1 }, { id: 2, parent: 1 }, { id: 3, parent: 0 }, { id: 5, parent: 3 }],
result = getDataWithIndent(data, relation);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
For the ordering, convert the first array into a Map, then pull the information from that into the first array.
function combineArrays(arr1, arr2) {
let names = new Map(arr1.map(({id, name}) => [id, name]));
return arr2.map(({id, parent}) => ({id: id, name: names.get(id), parent: parent}));
}
This will give you name, id, and parent in the order you want. Calculating indent is left as an exercise to the reader (or should probably be a different question).
I have an array of json like this:
var tree = [
{
text: "Parent 1",
id: 1,
nodes: [
{
text: "Child 1",
id: 2,
nodes: [
{
text: "Grandchild 1"
id: 3,
},
{
text: "Grandchild 2"
id: 4,
nodes: [
{
text: "Grandchild 3"
id: 10,
},
{
text: "Grandchild 4"
id: 11,
nodes: [
{
text: "Grandchild 5"
id: 12,
},
{
text: "Grandchild 6"
id: 13,
}
]
}
]
}
]
},
{
text: "Child 2"
id: 5,
}
]
},
{
text: "Parent 2"
id: 6,
},
{
text: "Parent 3"
id: 7,
},
{
text: "Parent 4"
id: 8,
},
{
text: "Parent 5"
id: 9,
}
];
I'm trying to create a function that would take as parameter the tree, and id, and a newText parameter, that would find the node with the given id, replace the text by newText, and return the modified json.
Ex:
editTree(tree, 11, "Granchild 13435")
Is there a way to achieve this ?
I don't know how to solve this since I need the path to the key in order to edit the tree.
You can use recursive function for this.
var tree = [{"text":"Parent 1","id":1,"nodes":[{"text":"Child 1","id":2,"nodes":[{"text":"Grandchild 1","id":3},{"text":"Grandchild 2","id":4,"nodes":[{"text":"Grandchild 3","id":10},{"text":"Grandchild 4","id":11,"nodes":[{"text":"Grandchild 5","id":12},{"text":"Grandchild 6","id":13}]}]}]},{"text":"Child 2","id":5}]},{"text":"Parent 2","id":6},{"text":"Parent 3","id":7},{"text":"Parent 4","id":8},{"text":"Parent 5","id":9}]
function editTree(tree, id, val) {
for (var i in tree) {
if (i == 'id') {
if (tree[i] == id) {
tree.text = val
return 1;
}
}
if (typeof tree[i] == 'object') editTree(tree[i], id, val)
}
return tree;
}
console.log(editTree(tree, 11, "Granchild 13435"))
You could use an iterative and recursive approach for searching the node. If found stop iteration and return.
This proposal uses Array#some, which allowes to exit the iteration.
If a node from an actual node exists and the node is an array, then this node gets iterated.
function editTree(tree, id, text) {
tree.some(function iter(o) {
if (o.id === id) {
o.text = text;
return true;
}
return Array.isArray(o.nodes) && o.nodes.some(iter);
});
}
var tree = [{ text: "Parent 1", id: 1, nodes: [{ text: "Child 1", id: 2, nodes: [{ text: "Grandchild 1", id: 3, }, { text: "Grandchild 2", id: 4, nodes: [{ text: "Grandchild 3", id: 10, }, { text: "Grandchild 4", id: 11, nodes: [{ text: "Grandchild 5", id: 12, }, { text: "Grandchild 6", id: 13, }] }] }] }, { text: "Child 2", id: 5, }] }, { text: "Parent 2", id: 6, }, { text: "Parent 3", id: 7, }, { text: "Parent 4", id: 8, }, { text: "Parent 5", id: 9, }];
editTree(tree, 11, "Granchild 13435");
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Here is a simple function to find a node matching a property and a value :
function findNode(nodes, prop, value) {
if(!value || !(nodes instanceof Array)) return;
for(var i=0; i<nodes.length; i++) {
if(node = (value == nodes[i][prop]) ? nodes[i] : findNode(nodes[i]['nodes'], value)) {
return node;
}
}
}
Then simply call :
// Find node with id = 10
var node = findNode(tree, 'id', 10);
if(node) {
// Yeah! we found it, now change its text
node['text'] = 'Changed!';
}
// Ensure tree has been updated
console.log(tree);
Sample snippet (check out that text of node with id = 10 has changed) :
var tree=[{text:"Parent 1",id:1,nodes:[{text:"Child 1",id:2,nodes:[{text:"Grandchild 1",id:3},{text:"Grandchild 2",id:4,nodes:[{text:"Grandchild 3",id:10},{text:"Grandchild 4",id:11,nodes:[{text:"Grandchild 5",id:12},{text:"Grandchild 6",id:13}]}]}]},{text:"Child 2",id:5}]},{text:"Parent 2",id:6},{text:"Parent 3",id:7},{text:"Parent 4",id:8},{text:"Parent 5",id:9}];
function findNode(nodes, prop, value) {
if(!value || !(nodes instanceof Array)) return;
for(var i=0; i<nodes.length; i++) {
var node = (value == nodes[i][prop]) ? nodes[i] : findNode(nodes[i]['nodes'], prop, value);
if(node ) {
return node;
}
}
}
var node = findNode(tree, 'id', 10);
if(node) {
node['text'] = 'Changed!';
}
console.log(tree)
I've created library that uses recursive walk and one of its method is exactly what you need.
https://github.com/dominik791/obj-traverse
Use findFirst() method. The first parameter is a root object, not array, so you should create it at first:
var tree = {
text: 'rootObj',
nodes: [
{
text: 'Parent 1',
id: 1,
nodes: [
{
'text': 'Child 1',
id: 2,
nodes: [ ... ]
},
{
'name': 'Child 2',
id: 3,
nodes: [ ... ]
}
]
},
{
text: 'Parent2',
id: 6
}
};
Then:
var objToEdit = findFirst(tree, 'nodes', { id: 11 });
Now objToEdit is a reference to the object that you want to edit. So you can just:
objToEdit.text = 'Granchild 13435';
And your tree is updated.