6

I have a problem on a project using vuejs. I have an array of nested object like this :

Data

data: [
  {
    "id":1,
    "parent_id":null,
    "title":"First folder",
    "children":[
       {
          "id":3,
          "parent_id":1,
          "title":"First folder in parents",
          "children":[]
       },
       {
          "id":4,
          "parent_id":1,
          "title":"Second folder in parents",
          "children":[
             {
               "id":5,
               "parent_id":4,
               "title":"test",
               "children":[]
             }
           ],
       }
    ]
  },
  {
    "id":2,
    "parent_id":null,
    "title":"Second folder",
    "children":[],
  }
]

And I would like to get an array of all parents for a specific id in a computed property

    computed: {
      parents() { 
        this.getAllParents(data, currentId);
      },
    },

And my function

        getParents(array, id, parentsId) {
        for (let i = 0; i < array.length; i++) {
            const child = array[i];
            if (child.id === id && ((child.children.length > 0 && child.children.some((el) => el.id === parentsId)) || !parentsId)) {
                return [child];
            } if (child.children.length > 0) {
                const x = this.getParents(child.children, child.id, id);

                if (x) return Array.isArray(x) ? [child, ...x] : [child, x];
            }
        }
        return [];
    },

For example, if my currentId is 3, I would like this in my computed:

[
  {"id": 1, "parent_id": null....},
  {"id": 3, "parent_id": 1....}
]

If my currentId is 1, I would like this :

[
  {"id": 1, "parent_id": null....},
]

If my currentId is 5, I would like this :

[
  {"id": 1, "parent_id": null....},
  {"id": 4, "parent_id": 1....},
  {"id": 5, "parent_id": 4....},
]

Now, my function

return [
  {"id": 1, "parent_id": null....},
  {"id": 4, "parent_id": 1....}
] 

if my current id is 3, not id:3 and I can't understand why

How to do this please ?

Thanks

4 Answers 4

5

You can recursively loop over the children array and keep collecting the parents.

Refer to the solution below:

const data = [
  {
    id: 1,
    parent_id: null,
    title: "First folder",
    children: [
      { id: 3, parent_id: 1, title: "First folder in parents", children: [] },
      {
        id: 4,
        parent_id: 1,
        title: "Second folder in parents",
        children: [{ id: 5, parent_id: 4, title: "test", children: [] }],
      },
    ],
  },
  { id: 2, parent_id: null, title: "Second folder", children: [] },
];

const getAncestors = (target, children, ancestors = []) => {
  for (let node of children) {
    if (node.id === target) {
      return ancestors.concat(node.id);
    }
    const found = getAncestors(target, node.children, ancestors.concat(node.id));
    if (found) {
      return found;
    }
  }
  return undefined;
};

console.log(getAncestors(5, data));

Note: I've just pushed the ids for brevity, you can update the solution to push the entire nodes.

Sign up to request clarification or add additional context in comments.

Comments

2

Imagine you had a magic genie that could give you helper functions as you needed. What would you wish for in order to make this function simple to write?

To my mind, if we had a function ancestry, which accepted a predicate to check against a node and simply returned the whole ancestry tree up to that node when it matched, then we could just write this:

const findAncestryById = (targetId) => 
  ancestry (({id}) => id == targetId)

Well, how would we write ancestry? Again, with a magic genie helping out, we can imagine that we had a function to walk all the elements of an array, recurring on their children nodes, and supplying the full ancestry path as it goes, then ancestry could build upon that. We probably want this to be a generator function, since those are easy to stop traversing once we've found our target values. So if we imagine we have walk, then ancestry might look like this:

const ancestry = (pred) =>  (obj) => {
  for (let nodes of walk (obj)) {
    if (pred (nodes .at (-1))) {
      return nodes
    }
  }
}

So now we need a walk function that walks an array and all its (recursive) descendents, yielding the full path to each node on every step. Well it turns out that this is relatively simple to do, and we can put it all together like this:

function * walk (xs, path = []) {
  for (let x of xs) {
    const newPath = path .concat (x)
    yield newPath
    yield * walk (x .children || [], newPath)
  }
}

const ancestry = (pred) =>  (obj) => {
  for (let nodes of walk (obj)) {
    if (pred (nodes .at (-1))) {
      return nodes
    }
  }
  return []
}

const findAncestryById = (targetId) => 
  ancestry (({id}) => id == targetId)


const data = [{id: 1, parent_id: null, title: "First folder", children: [{id: 3, parent_id: 1, title: "First folder in parents", children: []}, {id: 4, parent_id: 1, title: "Second folder in parents", children: [{id: 5, parent_id: 4, title: "test", children: []}]}]}, {id: 2, parent_id: null, title: "Second folder", children: []}]


console .log (findAncestryById (5) (data))

// or to see it without the StackOverflow id-ref pairs:
console .log (JSON .stringify(
  findAncestryById (5) (data)
, null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}

This magic-genie style has some strong advantages. It lets you abstract your specific problem into more generic ones. The generic ones are often easier to solve. But even if they're not, when you're done, you've built some reusable functions applicable to many other situations.


A side note: your data seems to have redundant information. The parent_id property is already encoded by the tree hierarchy. I personally would look to remove it, as there's always a worry about such data getting out of sync.

5 Comments

hello from inside the magic lamp! love the analogy and thanks for awakening me to Array.prototype.at. JS really needed this! great answer Scott <3
and yes, i brainlessly overlooked the redundancy of parent_id; one-hundred percent remove this from the input tree.
@Mulan: I almost credited you with the magic genie, as I'm pretty sure I've seen you use the same analogy, but I didn't recall where and was too lazy to search. But the world should understand that this is not original! at is much nicer than xs .slice (-1) [0] or xs [xs .length - 1]. I haven't looked yet to see how universal it is.
☺️ i have used similar analogies but not the genie, which evokes playful imagery. x.at(y) is the functional form for x[y] but with support for negative indicies. works on strings, too! amazing.
I pretty much only use at for at (-1), but at that it's quite nice.
1

generic find

Recursion is a functional heritage and so by using functional style we yield the best results. Your code is quite good but there is some room for improvement. By decomposing the problem, we isolate concerns and make it easier for us to write individual solutions to the smaller sub-problems.

We start with higher-order find(t, func) that searches t using function, func, and returns all paths to the matched nodes.

  1. If t is an array, for all child of t recur on the sub-problem find(child, func)
  2. (inductive) t is not an array. If t is an object, then if the user-supplied function returns true for f, yield the singleton output, [t], otherwise for each path of the sub-problem find(t.children, func), prepend t to the path and yield.
  3. (inductive) t is neither an array nor an object. Stop iteration.
// find (node array ∪ node, (node -> boolean)) -> (node array) generator
function* find(t, func) {
  switch (true) {
    case is(t, Array):               // 1
      for (const child of t)
        yield* find(child, func)
      break
    case is(t, Object):              // 2
      if (Boolean(func(t)))
        yield [t]
      else for (const path of find(t.children, func))
        yield [t, ...path]
      break
    default:                         // 3
      return
  }
}

Which depends on is -

function is(t, T) { 
  return t?.constructor === T
}

specialized find

Writing findById is a matter of specializing find. We assume id are unique and so return the path to the first matched node, if any. Otherwise return the empty path, [].

// findById : (node array ∪ node, number) -> node array
function findById(t, id) {
  for (const path of find(t, node => node.id === id))
    return path
  return []
}

demo

To demonstrate proper behavior, I wrote a demo function which prunes some of the fields from the returned nodes. This allows us to us to easily confirm the output -

function demo(t, id) {
  console.log(
    `== id: ${id} ==`,
    JSON.stringify(
      findById(data, id).map(p =>
        ({ id: p.id, parent_id: p.parent_id, title: p.title })
      )
    )
  )
}
demo(data, 1)
demo(data, 3)
demo(data, 5)
== id: 1 ==
[{"id":1,"parent_id":null,"title":"First folder"}]

== id: 3 ==
[{"id":1,"parent_id":null,"title":"First folder"},{"id":3,"parent_id":1,"title":"First folder in parents"}]

== id: 5 ==
[{"id":1,"parent_id":null,"title":"First folder"},{"id":4,"parent_id":1,"title":"Second folder in parents"},{"id":5,"parent_id":4,"title":"test"}]

You can verify the output below -

function is(t, T) { 
  return t?.constructor === T
}

function* find(t, func) {
  switch (true) {
    case is(t, Array):
      for (const child of t)
        yield* find(child, func)
      break
    case is(t, Object):
      if (Boolean(func(t)))
        yield [t]
      else
        for (const path of find(t.children, func))
          yield [t, ...path]
      break
    default:
      return
  }
}

function findById(t, id) {
  for (const path of find(t, node => node.id === id))
    return path
  return []
}

function demo(t, id) {
  console.log(`== id: ${id} ==`, JSON.stringify(findById(data, id).map(p =>
    ({ id: p.id, parent_id: p.parent_id, title: p.title })
  )))
}

const data = [{id:1,parent_id:null,title:"First folder",children:[{id:3,parent_id:1,title:"First folder in parents",children:[]},{id:4,parent_id:1,title:"Second folder in parents",children:[{id:5,parent_id:4,title:"test",children:[]}],}]},{id:2,parent_id:null,title:"Second folder",children:[]}]

demo(data, 1)
demo(data, 3)
demo(data, 5)

3 Comments

Very nice. I love that this abstracts from the specific problem as mine does, but goes in an entirely different -- yet complementary -- direction.
it's fun to imagine us working on this problem simultaneously, each sipping our tea 🧞‍♀️🧞‍♂️
No tea for me today, just water, but I'll try to remember to put the kettle on next time!
0

const data = [{id: 1, parent_id: null, title: "First folder", children: [{id: 3, parent_id: 1, title: "First folder in parents", children: []}, {id: 4, parent_id: 1, title: "Second folder in parents", children: [{id: 5, parent_id: 4, title: "test", children: []}]}]}, {id: 2, parent_id: null, title: "Second folder", children: []}]


function getAllParents(arr, id, res) {
  for (const el of arr) {
    if (el.id == id) {
      res.unshift(el);
      return true;
    } else {
      let is_parent = getAllParents(el.children, id, res);
      if (is_parent) {
        res.unshift(el);
        return true;
      }
    }
  }
}
let res;
for (const i of [1, 2, 3, 4, 5]) {
  res = [];
  getAllParents(data, i, res);
  console.log(
    res.map((path) => {
      const { id, parent_id, title } = path;
      return { id, parent_id, title };
    })
  );
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.