RXJS recursively expand a tree

Viewed 329

I have a tree of nodes, each time user "expands" a node, an http request is invoked to get it's children.

tree of nodes

I'm looking for an RXJS pipeline to recursively expand all tree nodes and emit the expanded tree.

( StackBlitz demo )

The way I do it now, is to mutate the source and output the mutated object when done. Is there a way for me to simplify this hell ? possibly without mutating source .

// 'ROOT' nodes
const rootNodes = [{ id: 0, parent: true }, { id: 1000, parent: true }];

// map the root nodes to a recursive function that mutate node and returns its children observables
const getChildrenOfRoot$ = rootNodes.map(node => getChildren(node));

// call the observables, in parallels, when done, print the *** mutated *** source.
forkJoin(...getChildrenOfRoot$).subscribe(() => console.log(rootNodes));


function getChildren(node: Node): Observable<Node[]> {

  return getChildrenFromServer(node.id).pipe(

    // if no children returned, don't continue
    filter((children: Node[]) => !!(children?.length)),

    // mutate argument's .children property with the returned children array.
    tap((children: Node[]) => (node.children = children)),

    mergeMap((children: Node[]) => {
      // mape children to observables returning either their children, or, an empty array, based on 'parent' property.
      const getChildrenOfChildren$ = children.map(c => (c.parent ? getChildren(c) : of([])));

      return forkJoin(...getChildrenOfChildren$);
    })
  );
}
1 Answers

TLDR;

StackBlitz app.


Detailed explanation

Since we're dealing with recursion, I thought it would be easier if we started from a base case.
Let's see what we have initially:

const rootNodes = [{ id: 0, parent: true }, { id: 1000, parent: true }];

Assuming these are the only parents in this tree(i.e their children will all be leaf nodes), our problem becomes easier: for each node in the above array, we must fetch its children and add a new property to their nodes, called children.
For this, let's create the processNodes children:

function processNodes (nodes: Node[]): Observable<Node[]> {
  return nodes.length 
    ? forkJoin(
        nodes.map(node => node.parent ? processNode(node) : of(node))
      )
    : of([]);
}

Note that we're also considering the case where the nodes array is empty, so, in that case, we simply return an empty array(of([])). Let's now focus on this part:

forkJoin(
  nodes.map(node => node.parent ? processNode(node) : of(node))
)

If the argument of processNodes was:

[{ id: 0, parent: true }, { id: 1000, parent: true }, { id: 7, parent: false }]

then the expected output would be:

[
  { id: 0, parent: true, children: [/* ... */] },
  { id: 1000, parent: true, children: [/* ... */] },
  { id: 7, parent: false }
]

So, in case a node is a parent, then we have separated logic encapsulated in the processNode function:

function processNode (node: Node) {
  return getChildrenFromServer(node.id).pipe(
    mergeMap((children: Node[]) => processNodes(children)),
    map((children: Node[]) => ({ ...node, ...children.length && { children } })),
  );
}

and here is where the recursion happens. Given the children nodes of some parent node, we repeat the process and at the end we add the children property to the parent node.

So, to put all the parts together:

processNodes(rootNodes).subscribe(console.log)

function processNodes (nodes: Node[]): Observable<Node[]> {
  return nodes.length 
    ? forkJoin(
        nodes.map(node => node.parent ? processNode(node) : of(node))
      )
    : of([]);
}

function processNode (node: Node) {
  return getChildrenFromServer(node.id).pipe(
    mergeMap((children: Node[]) => processNodes(children)),
    map((children: Node[]) => ({ ...node, ...children.length && { children } })),
  );
}
Related