Search code examples
typescriptrxjsflattenbehaviorsubject

Flatten a parent child structure to a BehaviorSubject that resolves to an array


I have a BehaviourSubject that resolves into a collection where each item of has a property children that also is a BehaviourSubject resolving into a collection of items. Now I want to subscribe to my subject and flatten the whole thing into an array where I have a row for each item and each child.

So something in the line of:

interface Parent = {
  children: BehaviorSubject<Child[]>
}

Now I want to make another behavior subject out of this that resolves into an array where I have an item for each parent followed by its children.

Starting with:

const collection: BehaviorSubject<Parent[]>;

Want to result to:

const behaviourSubject: BehaviorSubject<(Parent|Child)[]>;

behaviourSubject.subscribe((result: (Parent|Child)[]) => { 
  ...desired result... 
});

So let's say the first Parent in the collection has 3 children, and second has 2 children then if I subscribe to the resulting behaviourSubject the output should look like this.

result: [Parent, Child, Child, Child, Parent, Child, Child];

How do I achieve this.

I tried to figure out how to do this, by reading in the documentation of RxJS and checking examples, but I guess the recent lack of sleep seems to make my brain work like a dry sponge and I thought the best right now would be to call out for some help on StackOverflow.

Note: if the parent collection or the children get a new value, the result should be updated as well...


Solution

  • Something like below show work, but I wonder why you use BehaviorSubject to add up the complexity

    import { first, switchMap, toArray, map } from 'rxjs/operators';
    import { pipe, BehaviorSubject, from } from 'rxjs';
    
    //...
    
    const parent1={children:new BehaviorSubject([{name:'john'},{name:'may'},{name:'betty'}])}
    
    const parent2={children:new BehaviorSubject([{name:'key'},{name:'tom'}])}
    
    const collection=new BehaviorSubject([parent1,parent2]);
    
    collection.pipe(
      first(),
      switchMap(parent=>from(parent)),
      switchMap(p=>p.children.pipe(first(),map(c=>[p,...c]))),
      toArray(),
      map(arr=>arr.flat())
    ).subscribe(console.log)
    

    if you dont mind make use the .value property from BehaviourSubject the code can be reduced to below

    collection.value.map(p=>[p,...p.children.value]).flat()
    

    EDIT (BY OP):

    I will edit this question for those wondering about the different steps in this answer I broke it down in pieces.
    It can also be found in this StackBlitz to play around with.

    Constructing the data:

    import { first, switchMap, toArray, map } from 'rxjs/operators';
    import { pipe, BehaviorSubject, from } from 'rxjs';
    
    class Child {
      constructor(public name: string){
      }
    }
    
    class Parent {
      constructor(public children: BehaviorSubject<Child[]>){
      }
    }
    
    const collection = new BehaviorSubject([
      new Parent(new BehaviorSubject([
        new Child('john'),
        new Child('may'),
        new Child('betty'),
      ])),
      new Parent(new BehaviorSubject([
        new Child('key'),
        new Child('tom'),
      ])),
    ]);
    

    Here the final observable with its different operations in the pipes with comments added to explain what they exactly do when subscribing to the result:

    collection.pipe(
      //Emit the first value 
      first(),
      // Complete previous inner observable, emit values
      switchMap((parents: Parent[]) => { 
        // Emit array as a sequence of values
        return from(parents)
      }),
      // complete previous inner observable, emit values
      switchMap((parent: Parent) => { 
        return parent.children.pipe(
          // Emit the first value 
          first(),
          // Apply projection with each value from source
          map(
            (children: Child[]) => {
              // return desired array for each parent
              return [parent, ...children];
            }
          )
        )
      }),
      //Collects all source emissions and emits them as an array when the source completes
      toArray(),
      //map(arr=>arr.flat())
      map((array: (Parent|Child)[][] ) => {
        // Flatten the array
        return array.flat();
      }),
    ).subscribe(console.log)