[Vuejs]-JS map object names recursively

3👍

Often generator functions will simplify such traversals. Here we can write a quite simple generator then wrap it in a function that creates an array from that generator:

const getPaths = function * (xs, ps = []) {
  for (let x of xs) {
    yield [... ps, x .attributes .name] .join (' > ')
    yield * getPaths (x .children, [...ps, x .attributes .name])
  }
}

const categoryNames = (categories) => 
  [... getPaths (categories .data)]

const categories = {data: [{type: "categories", id: "1", attributes: {name: "Top Level"}, children: [{type: "categories", id: "2", attributes: {name: "Sub 1"}, children: [{type: "categories", id: "4", attributes: {name: "Sub 1-2"}, children: []}]}, {type: "categories", id: "3", attributes: {name: "Sub 2"}, children: []}]}]};

console .log (
  categoryNames(categories)
)

getPaths might be made a little more generic by removing the join (' > ') call and adding it inside a map call at the end of categoryNames. But since the children and attributes.names is already fairly problem-specific, I probably wouldn’t bother.


Explanation

You said you didn’t entirely follow this code. This is an attempt to explain it. Please don’t be offended if I explain something you already understand. It’s not clear to me exactly what needs to be explained.

The outer function, categoryNames is a very small wrapper around getPaths. That one does all the work.

There are two important features to note about getPaths:

  • It is a generator function, noted by the * between the function keyword and the list of arguments. Generator functions create Generator objects, which, because they conform to the [iterable protocol], can be used inside such constructs as let x of generator and [...generator]. That is how categoryNames converts the output of getPaths into an array. (Incidentally, the iterable protocol is also how the for-of loop in getPaths converts an array into a sequence of values.) Generator functions work by yielding individual values or using yield * anotherGenerator to individually produce each of the values yielded by another generator. In between these yield calls the function is suspended until the next value is requested.

  • It is a recursive function; the body of the function invokes that same function again, with simpler parameters. Most often in recursive functions you will see an explicit base case, where the answer is returned directly without a recursive call when the input is simple enough. Here that base case is implicit. When xs is an empty array, the body of the for-loop is never invoked, so the recursion ends.

getPaths accepts an array of values (xs is my default name for a list of values of an unknown type; nodes would also be a good name) and it accepts an array of strings representing the paths in the hierarchy up to the current node. So it might for instance contain ["Top Level", "Sub 1"]. Note that this is a default parameter; if you don’t supply it, it will be given an empty array.

We loop over the values supplied to us. For each one, we first yield the result of combining the current paths and the the name property of the attribute property of our current object by interspersing " > " between them. Then we recur by passing the child and an array of names including the current one, yielding each of its children in turn. This version might be both slightly more performant and a bit easier to read:

const getPaths = function * (xs, paths = []) {
  for (let x of xs) {
    const newPaths = [... paths, x .attributes .name]
    yield newPaths .join (' > ')
    yield * getPaths (x .children, newPaths)
  }
}

And if you wanted, you could define newPaths like this instead:

    const newPaths = paths .concat (node .attributes .name)

I hope that helps. If you have questions about this, please add a comment.

0👍

Look at this block

categories.forEach(c => {
  this.res[stackCounter] = { name: c.attributes.name, id: c.id };
});

You can see that res[stackCounter] is always overridden by last element of categories. To fix this, res[stackCounter] should be an array also.

parseCategories(categories, stackCounter = 0) {
  // init res[stackCounter]
  if (this.res[stackCounter]) {
    this.res[stackCounter] = [];
  }
  categories.forEach(c => {
    this.res[stackCounter].push({ name: c.attributes.name, id: c.id }); // push to res[stackCounter]
    if(c.children.length >= 1) {
      this.parseCategories(c.children, stackCounter + 1);
    }
  });
  return this.res;
}

Leave a comment