Home > database >  Find nested object by Id using for loop in Javascript
Find nested object by Id using for loop in Javascript

Time:03-07

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"
              }
            }
          ]
        }
      ]
    }
  ]
};

CodePudding user response:

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; }

CodePudding user response:

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.

  • Related