0

I have been asked to create a stock list using a hierarchal system with parent ID's. I'm having trouble displaying children under their parents. I know I need to use a recursive function of some kind but my brain just won't let me work out how it would go together to accommodate for infinite amounts of indenting.

Example JavaScript data...

[
    {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"}
]

Which the HTML should to look like...

  • Row 1
    • Row 2
      • Row 3
      • Row 4

If anyone could help me it would be awesome as I've been stuck on this for almost 4 hours and I can't find anything that is relevant to my specific goal.

Mulan
  • 129,518
  • 31
  • 228
  • 259
Brad Bird
  • 737
  • 1
  • 11
  • 30
  • Related if not dupe :) https://stackoverflow.com/questions/18017869/build-tree-array-from-flat-array-in-javascript – giorgio79 Jun 25 '18 at 14:52

3 Answers3

3

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

  1. 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

  2. index each node by id using map.i[node.id] = node

  3. If the node is a root node (parent_id === null), add it to the result with map.result.push(node)

  4. 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;
Community
  • 1
  • 1
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • How are you ensuring that the node has been placed on `map.i` before assigning children to it? – Daniel Weiner Jan 19 '15 at 22:01
  • @DanielWeiner, I'm (purposefully) not. If the data is coming in unsorted, a sort could be applied beforehand. – Mulan Jan 19 '15 at 22:02
  • That might only work if we know for sure that the `parent_id` of any given node is greater than or equal to its `id`, which might not always be the case. – Daniel Weiner Jan 19 '15 at 22:09
  • 1
    @DanielWeiner I've provided an alternate solution that handles arbitrarily sorted input. – Mulan Jan 19 '15 at 22:25
  • @DanielWeiner don't worry, **the OP wasted everyone's time** by not specifying he/she wanted HTML output instead of JavaScript output. – Mulan Jan 19 '15 at 22:30
  • 1
    I wouldn't consider it wasted time. I hope that we were able to educate the masses that you don't need a recursive algorithm to create recursive structures. – Daniel Weiner Jan 19 '15 at 22:32
  • @naomik Thanks for the solution. Just to be clear, I assumed people would know that I wanted UL and LI elements from the way I laid out the result structure rather than displaying it like a multidimensional JSON object. I'll make it more clear next time. – Brad Bird Jan 20 '15 at 09:35
1

Inspired by Naomik, the code will fail when the parent_ids aren't in the correct position. Added a sorting function that will set them in the correct order.

obj = [
    {id: 2, parent_id: 1, title: "Row 2"},
    {id: 3, parent_id: 2, title: "Row 3"},
    {id: 4, parent_id: 2, title: "Row 4"},
    {id: 1, parent_id: null, title: "Row 1"}
]

obj.sort(function(a, b){
    return (a.parent_id == null ? 0 : a.parent_id) - (b.parent_id == null ? 0 : b.parent_id);
});

var tree = document.getElementById("tree");
for (var i = 0; i < obj.length; ++i)
  {

    if (obj[i].parent_id == null)
      {
        createTreeElement("li", obj[i].id, obj[i].title, tree);
      }
    else
      {
         var treeChildNode = document.getElementById("t" + obj[i].parent_id).getElementsByTagName("ul");
        if (treeChildNode.length)
          {
            createTreeElement("li", obj[i].id, obj[i].title, treeChildNode[0]);
          }
        else
          {
            createTreeElement("ul", obj[i].parentId, "", document.getElementById("t" + obj[i].parent_id));
            createTreeElement("li", obj[i].id, obj[i].title, document.getElementById("t" + obj[i].parent_id).getElementsByTagName("ul")[0]);
          }
      }
  }

function createTreeElement(name, id, text, parent)
{
  var node = document.createElement(name);
  node.id = "t" + id;
  node.innerHTML = text;
  parent.appendChild(node);
}
<ul id="tree">
  
</ul>

This code is just a prove of concept in HTML to @Daniel Weiners answer why recursion isn't needed here based upon the object model.

Mouser
  • 13,132
  • 3
  • 28
  • 54
  • 1
    This is exactly what I was looking for. Thanks! I just wanted to display this from the JSON. I didn't actually want to convert my flat model into another nested JSON object. Just wanted to produce the HTML for it. – Brad Bird Jan 19 '15 at 21:56
  • 3
    @BradBird, you could've avoided wasting everyone's time by telling us "exactly what you wanted". – Mulan Jan 19 '15 at 22:27
  • @naomik I just wrote a prove of concept for dperry to show that you don't need recursion for this. Apparently that was what the guy needed. – Mouser Jan 19 '15 at 22:33
  • @Mouser I know that recursion wasn't required. I don't know when/why everyone felt the need to make this point. The point is the OP didn't specify an `html` tag or say "HTML structure" anywhere. Your answer is fine as is ^.^ – Mulan Jan 19 '15 at 22:35
  • @Nit, **there is not a single "recursive solution" on this page**. Every single posted answer is a simple iterative one. – Mulan Jan 19 '15 at 22:36
  • @naomik I'm not attacking you but dperry was so hell bound that recursion was the way to go, that I just wrote a solution in HTML to demonstrate. – Mouser Jan 19 '15 at 22:37
  • @Mouser, I realize that now. Your answer is a fine one. Nice job picking up on the intended HTML result. – Mulan Jan 19 '15 at 22:37
  • @Nit The OP thought this had to be done recursively, but that wasn't the case with his data model. – Mouser Jan 19 '15 at 22:38
  • There was a recursive one before, but I guess the answer was removed if someone downvoted it. – Etheryte Jan 19 '15 at 22:59
0

You use an object that keeps track of your id's. For example:

var idObj = {};
var root = null;

// first populate the object with elements
for (var i = 0; i < arr.length; i++) {
  var item = arr[i];
  idObj[item.id] = item;
}

// then attach the children to the parents
for (var i = 0; i < arr.length; i++) {
  var item = arr[i];
  var parent = idObj[item.parent_id];
  if (parent) {
    parent.children = parent.children || [];
    parent.children.push(item);
  } else if (item.parent_id === null) { 
    //set the item as root if it has no parents
    root = item;
  }
}

This will add a children property to all those items.

Note: this solution is not recursive, but you can traverse the tree recursively starting from the root variable.

Daniel Weiner
  • 1,874
  • 12
  • 19
  • But this doesn't accommodate for an unlimited amount of sub-items? – Brad Bird Jan 19 '15 at 21:18
  • This will create your entire tree, no matter how deeply nested. I tested the solution and it creates a structure as you've described above. – Daniel Weiner Jan 19 '15 at 21:22
  • @DanielWeiner, could you show how a non-recursive for-loop will create a hierarchy no matter how deeply nested? – dfperry Jan 19 '15 at 21:27
  • His object model doesn't require recursion. It's only one level deep. The parentID decides where the element must go. You just need to traverse the array. – Mouser Jan 19 '15 at 21:30
  • @dperry he's assigning children directly based on ids. No recursion is required. – Mulan Jan 19 '15 at 21:30
  • that's assuming that the id and parent_id fields are both in sorted order, and that an item doesn't show up with a parent_id of an item that hasn't been hit in the array yet – dfperry Jan 19 '15 at 21:32
  • @dperry nope. He creates an entire index of items first. Did you read the code? – Mulan Jan 19 '15 at 21:33
  • yes, his code will create a list of all objects, and their child objects. OP asked for an n-level object. The example set is three levels deep, and he asked for a nested object as such. – dfperry Jan 19 '15 at 21:35
  • This solution works for arbitrarily nested structures, not just three levels deep. It iterates over *every* object and gives it children, no matter how deep in the tree it is. – Daniel Weiner Jan 19 '15 at 21:38