Heads up: This requires your data to be ordered in a way that parent nodes appear before children reference them. A sort
could be done first, if required.
Edit: a no-sort solution is posted below
Here's a way to do it using Array.prototype.reduce.
var arr = [
{id: 1, parent_id: null, title: "Row 1"},
{id: 2, parent_id: 1, title: "Row 2"},
{id: 3, parent_id: 2, title: "Row 3"},
{id: 4, parent_id: 2, title: "Row 4"}
];
var x = arr.reduce(function(map, node) {
map.i[node.id] = node;
node.children = [];
node.parent_id === null ?
map.result.push(node) :
map.i[node.parent_id].children.push(node);
return map;
}, {i:{}, result:[]}).result;
Explanation. I'll step through the reduce process I used
initialize the reduce with {i:{}, result:[]}
We'll use the i
object as a means of referencing parent nodes and the result
array to store top-level root nodes
index each node by id
using map.i[node.id] = node
If the node is a root node (parent_id === null
), add it to the result with map.result.push(node)
If the node is a child node (parent_id !== null
), add it to the children array of the parent node with map.index[node.parent_id].children.push(node)
Okay, let's check if it worked
// all root nodes
// see output below
console.log(JSON.stringify(x, null, " "));
// first "root" node
console.log(x[0].id); //=> 1
// first child of first root node
console.log(x[0].children[0].id); //=> 2
// first child of first child of first root node
console.log(x[0].children[0].children[0].id); //=> 3
// second child of first child of first root node
console.log(x[0].children[0].children[1].id); //=> 4
All root nodes output
[
{
"id": 1,
"parent_id": null,
"title": "Row 1",
"children": [
{
"id": 2,
"parent_id": 1,
"title": "Row 2",
"children": [
{
"id": 3,
"parent_id": 2,
"title": "Row 3",
"children": []
},
{
"id": 4,
"parent_id": 2,
"title": "Row 4",
"children": []
}
]
}
]
}
]
If your initial data is unsorted...
The reduce
method is a little more difficult in this case. Admittedly, pretty much all elegance is lost with this solution, but I've provided it to show it's still possible.
// this works on arbitrarily sorted data
var x = arr.reduce(function(map, node) {
map.i[node.id] = node;
node.children = [];
if (node.parent_id === null) {
map.result.push(node);
}
else if (node.parent_id in map.i) {
map.i[node.parent_id].children.push(node);
}
else {
(node.parent_id in map.cache) ?
map.cache[node.parent_id].push(node) :
map.cache[node.parent_id] = [node];
}
if (node.id in map.cache) {
node.children = node.children.concat(map.cache[node.id]);
delete map.cache[node.id];
}
return map;
}, {i:{}, cache:{}, result:[]}).result;