Ad

Angular: Bind Method + Param With Async Pipe In Template

- 1 answer

I'm playing with firebase and the many-to-many middle man collection pattern. see: https://angularfirebase.com/lessons/firestore-nosql-data-modeling-by-example/#Many-to-Many-Middle-Man-Collection

But I'm struggling for a pattern to "complete the join" after I get an array of ids in a collection.

I want to render the 'joined' elements in a template with async pipe like this:

<div *ngFor="let id of itemIds">
  <div *ngIf="(getById$(id) | async) as item">{{item.name}}</div>
</div>

but Angular change detection calls getById$() multiple times, I get a new Observable each time, and ...the browser freezes.

I hacked a solution by caching the Observable, but this doesn't seem right.

  getById$(uuid:string):Observable<T>{
    this['get$_'] = this['get$_'] || {};      // cache in component
    const cached$ = this['get$_'][uuid];
    if (cached$) return cached$ as Observable<T>;
    return this['get$_'][uuid] = this.get(uuid) as Observable<T>;
  }

Is there a better pattern?

Ad

Answer

Let's play it out logically. Whatever items are used in the *ngFor collection have to stay the same in order to prevent change detection from re-rendering item views and thus from re-subscribing to the observables. Consequently, you have to keep a collection of observables somewhere and not create them on-the-fly. The way you've done it is pretty much what you actually need. We can improve it, although not by much.

When you get itemIds collection all you need is to perform one time mapping them to some collection containing observables and put that collection into the template instead of just source ids. Something like this:

private _items: any[] = []; // put your type here instead of <any>

get items$(): any[] {
    return this._items;
}

set itemIds(value: any[]) {

    if (!value) {
        this._items = [];
        return;
    }

    // I know that it's pretty ugly and inefficient implementation,
    // but it'll do for explaining the idea.
    this._items = value.map(id => {
        // this is the essential part: if observable had already been created for the id
        // then you _must_ preserve the same object instance in order to make change detection happy
        // you can also try to use trackBy function in the *ngFor definition
        // but I think that it will not help you in this case
        let item$ = this._items.find(i => i.id === id);
        if (!item$) {
            item$ = {
                id: id,
                obs$: this.getById(id)
            };
        }
        return item$;
    });

}

And then in the template:

<div *ngFor="let item$ of items$">
    <div *ngIf="(item$.obs$ | async) as item">{{item.name}}</div>
</div>

This way observables will stay the same for their respective ids and the whole thing will work normally with change detection.

So, to sum this up: you need to keep a collection of observables and update it only when source collection of ids changes, and you need to keep observable instances for the ids they were created for.

Ad
source: stackoverflow.com
Ad