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 thefunction
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 aslet x of generator
and[...generator]
. That is howcategoryNames
converts the output ofgetPaths
into an array. (Incidentally, the iterable protocol is also how thefor-of
loop ingetPaths
converts an array into a sequence of values.) Generator functions work byyield
ing individual values or usingyield * anotherGenerator
to individually produce each of the values yielded by another generator. In between theseyield
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 thefor-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;
}