I have a Firebase Firestore with \"Components\" as a root collection. Each document (a \"Component\") in the collection may have an array called \"children\", with each ite
You may look at a solution along these lines
// simulates an async data fetch from a remote db
function getComponent(id) {
return of(components.find(comp => comp.id === id)).pipe(delay(10));
}
function expandComp(component: Observable<any>) {
return component.pipe(
tap(d => console.log('s', d.name)),
mergeMap(component => {
if (component.childrenIds) {
return concat(
of(component).pipe(tap(comp => comp['children'] = [])),
from(component.childrenIds).pipe(
mergeMap(childId => expandComp(getComponent(childId)))
)
)
.pipe(
reduce((parent: any, child) => {
parent.children.push(child);
return parent;
})
)
}
else {
return of(component);
}
})
)
}
.subscribe(d => // do stuff, e.g. console.log(JSON.stringify(d, null, 3)))
I have tested the above code with the following test data
const componentIds: Array<string> = [
'1',
'1.1.2'
]
const components: Array<any> = [
{id: '1', name: 'one', childrenIds: ['1.1', '1.2']},
{id: '1.1', name: 'one.one', childrenIds: ['1.1.1', '1.1.2']},
{id: '1.2', name: 'one.two'},
{id: '1.1.1', name: 'one.one.one'},
{id: '1.1.2', name: 'one.one.two', childrenIds: ['1.1.2.1', '1.1.2.2', '1.1.2.3']},
{id: '1.1.2.1', name: 'one.one.two.one'},
{id: '1.1.2.2', name: 'one.one.two.two'},
{id: '1.1.2.3', name: 'one.one.two.three'},
]
The basic idea is to call recursively the expandComp
function, while the looping along the children of each component is obtained using the from
function provided by RxJS.
The grouping of the children within the parent component is provided using the reduce
operator of RxJS, used within expandComp
function.
I have tried initially to look at the expand
operator of RxJS, but I was not able to find a solution using it. The solution proposed by @ggradnig leverages expand
.
Recursion in RxJS is best tackled with the use of the expand
operator. You provide it with a projection function that returns an Observable, that, on notification, calls the projection function again with the emitted value. It does this for as long as your inner Observable is not emitting EMPTY
or complete
.
While it does that, every notification is also forwarded to the subscribers of expand
, unlike with a traditional recursion where you'll only get the result at the very end.
From the official docs on expand
:
Recursively projects each source value to an Observable which is merged in the output Observable.
Let's look at your example. Without RxJS, if we had a synchronous datasource that gave us each child of a node (let's call it getChildById
), the function could look like this:
function createFamilyTree(node) {
node.children = node.childrenIds.map(childId =>
createFamilyTree(
getChildById(childId)
)
);
return node;
}
Now, we'll translate it to RxJS with the use of the expand
operator:
parentDataSource$.pipe(
map(parent => ({node: parent})),
expand(({node}) => // expand is called for every node recursively
(i.e. the parent and each child, then their children and so on)
!node ? EMPTY : // if there is no node, end the recursion
from(node.childrenIds) // we'll convert the array of children
ids to an observable stream
.pipe(
mergeMap(childId => getChildById(childId) // and get the child for
the given id
.pipe(
map(child => ({node: child, parent: node}))) // map it, so we have
the reference to the
parent later on
)
)
),
// we'll get a bunch of notifications, but only want the "root",
that's why we use reduce:
reduce((acc, {node, parent}) =>
!parent ?
node : // if we have no parent, that's the root node and we return it
parent.children.push(node) && acc // otherwise, we'll add children
)
);
The used operators are explained in detail on the official RxJS documentation page: from, expand, reduce
EDIT: A clean and tested version of the above code can be found here: https://stackblitz.com/edit/rxjs-vzxhqf?devtoolsheight=60