javascriptarraystypescriptmultidimensional-arrayeval

Add entry to array of objects using array of indexes


I have the following scenario: I have an array of objects (TreeItems) which could contain nested arrays. And I have another Array with indexes. Now I would like to change the TreeItems array using the indexes array to access a specific tree item and depending on if there is a nested array called "items" I would like to either append the existing one or add a new one. And mostly important, I would like to get rid of the evil eval.

So in my example I try to add a new entry at TreeItems[4][0][0] but the indexes in the brackets should be added dynamically from the indexes array but I'm not quite shure how to do this.

I have searched of course and found this one, but it doesn't quite work for me.

Any help would be greatly appreciated.

const TreeItems = [
  { text: "Lvl 0", items: [{ text: "Lvl 0-0" }, { text: "Lvl 0-1" }] },
  { text: "Lvl 1" },
  { text: "Lvl 2", items: [{ text: "Lvl 2-0" }, { text: "Lvl 2-1" }] },
  { text: "Lvl 3" },
  {
    text: "Lvl 4",
    items: [
      {
        text: "Lvl 4-0",
        items: [
          {
            text: "Lvl 4-0-0",
            items: [{ text: "Lvl 4-0-0-0" }, { text: "Lvl 4-0-0-1" }, { text: "Lvl 4-0-0-2" }],
          },
          { text: "Lvl 4-0-1" },
          { text: "Lvl 4-0-2" },
        ],
      },
    ],
  },
];

const indexes = [4,0,0];
let comString = '';
for(let i = 0; i < indexes.length; i++) {
  if(i === 0) {
    comString = "TreeItems[" + indexes[i] + "]";
  } else {
    comString += ".items[" + indexes[i] + "]";
  }
}

if(eval(comString).items !== undefined) {
  eval(comString).items.push({"text": "Test-item-appended"});
} else {
  eval(comString).items = [{"text": "Test-items-added"}]
}
console.log(comString);
console.log(eval(comString).items);
console.log(TreeItems);


Solution

  • You don't have to use eval or any kind of dynamic code generation to do this. As a matter of fact, there are no programming problems that would warrant the use of eval unless you're explicitly looking to execute a block of foreign JavaScript code.

    If you want to access the nested children of TreeItems using an array of indices as the path, you can create a variable for storing the child at depth n, starting from 0, and then repeatedly overwrite it with the child at depth n + 1, until you have accounted for all the indices in the path:

    function getTreeItem(path) {
        // get first child, whose index is at path[0]
        let currentChild = TreeItems[path[0]];
        
        // for indices at path[1] and further, overwrite currentChild with the next child
        for (let i = 1; i < path.length; i++)
            currentChild = currentChild.items[path[i]];
        
        return currentChild;
    }
    

    Note that using for loops and let statements is a bit verbose and goes against the coding style for modern JavaScript. The proper way to do this is by using Array.prototype.reduce, as suggested in the answer you found. The original question was about arrays of arrays, whereas in this case you're accessing an array of objects with a property named items, and your function must account for that.

    The following code is functionally equivalent to the above code:

    function getTreeItem(path) {
        return path.reduce(
            (currentChild, currentPathIndex, depth) => {
                // if currentPathIndex is path[0], set currentChild to the currentPathIndex-th child of TreeItems
                if (depth == 0)
                    return TreeItems[currentPathIndex];
            
                // otherwise, set currentChild to the currentPathIndex-th child of currentChild.items
                return currentChild.items[currentPathIndex];
            },
            // let the initial currentChild be null and the initial depth be 0
            null
        );
    }
    

    Read more about Array.prototype.reduce at MDN.

    Once you have obtained a reference to the parent node, you may add items to it in exactly the same manner as proposed in your question:

    function addTreeItem(parentPath, text) {
        const parent = getTreeItem(parentPath);
        
        if (parent.items)
            parent.items.push({text})
        else
            parent.items = [{text}];
    }
    
    addTreeItem([4, 0, 0, 0], "added item");
    addTreeItem([4, 0, 0, 0], "appended item");