Search code examples
angularrxjsrxjs6rxjs-observables

Angular template binding with Observable async pipe issue


Note I have created a simplified version of this question at Template binding with function return Observable and async pipe

Template:

<div *ngIf="entity?.ext.insuredDetails.insuredType$() | async as insuredType">
 {{insuredType}}
</div>

insuredType$ definition:

@NeedsElement(sp(115621),ap(116215))
insuredType$(): Observable<string> {
  return empty();
}

NeedsElement decorator:

export function NeedsElement(...mappings: NeedsElementMapping[]) {
  if (mappings.length === 0) {
    throw new Error('needs mapping expected');
  }

  let lookup = new Map<ProductId, number>();
  mappings.forEach((mapping) => {
    lookup.set(mapping.productId, mapping.elementId);
  });

  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    descriptor.value = function (...args: any[]) {
      Logger.info("bbbbb");
      let entity = UcEntityStoreContext.currentEntity;
      let productId = entity['productId'];
      if (!productId) {
        throw new Error(`Cannot get product Id from host entity: ${entity.ucId}`);
      }
      let elementId: number = lookup.get(entity['productId']);
      if (!elementId) {
        throw new Error(`Cannot locate needs element ID by productId ${productId}`);
      };
      let enitityStore = UcEntityStoreContext.current;
      let entityApi = enitityStore.apiService as QuotePolicyApiBase<any>;
      let needsDefApi = NeedsDefinitionApi.instance;

      return needsDefApi.fetchOne(productId, elementId).pipe(
        concatMap(
          nd => {
            return entityApi.fetchNeedsElementValue(entity.ucId, elementId).pipe(
              concatMap(needsVal => {
                if (!needsVal) {
                  return of("");
                }
                if (nd.lookupId) {
                  return LookupApi.instance.getByPrimaryValueId(nd.lookupId, needsVal).pipe(
                    map(res => res.primaryValue)
                  );
                } else {
                  return of(needsVal);
                }
              })
            )
          }
        )
      );
    };
  };
}

The problem is the the decorator is called multiple times:

enter image description here

And if it goes this branch:

enter image description here

then it keep sending requests to the backend servcie and the binding never output anything:

enter image description here

It looks like it will always keep trying evaluate the observable without ending if it is an async obserable, say this one: enter image description here


Updates 14/May/2020

I got the answer from Template binding with function return Observable and async pipe

In the end I changed the Method Decorator to Property Decorator and issue fixed.


Solution

  • when you use things like insuredType$() | async it means that angular will call this function every time when change detection is happening. therefore it calls needsDefApi.fetchOne(productId, elementId) every time too.

    To avoid it you need to mark your component OnPush. What is actually a lifehack to reduce amount of calls, because it will be called only in case of changed inputs or triggered outputs of the component. If it happens often - it won't help.

    Or you need to restructure the decorator to return the same Observable on any call for the same entity so entity?.ext.insuredDetails.insuredType$() === entity?.ext.insuredDetails.insuredType$() would be true.

    Not sure if it works but it should be similar to it:

    export function NeedsElement(...mappings: NeedsElementMapping[]) {
        if (mappings.length === 0) {
            throw new Error('needs mapping expected');
        }
    
        let lookup = new Map<ProductId, number>();
        mappings.forEach((mapping) => {
            lookup.set(mapping.productId, mapping.elementId);
        });
    
        Logger.info("bbbbb");
        let entity = UcEntityStoreContext.currentEntity;
        let productId = entity['productId'];
        if (!productId) {
            throw new Error(`Cannot get product Id from host entity: ${entity.ucId}`);
        }
        let elementId: number = lookup.get(entity['productId']);
        if (!elementId) {
            throw new Error(`Cannot locate needs element ID by productId ${productId}`);
        };
        let enitityStore = UcEntityStoreContext.current;
        let entityApi = enitityStore.apiService as QuotePolicyApiBase<any>;
        let needsDefApi = NeedsDefinitionApi.instance;
    
        const stream$ = needsDefApi.fetchOne(productId, elementId).pipe(
            concatMap(
                nd => {
                    return entityApi.fetchNeedsElementValue(entity.ucId, elementId).pipe(
                        concatMap(needsVal => {
                            if (!needsVal) {
                                return of("");
                            }
                            if (nd.lookupId) {
                                return LookupApi.instance.getByPrimaryValueId(nd.lookupId, needsVal).pipe(
                                    map(res => res.primaryValue)
                                );
                            } else {
                                return of(needsVal);
                            }
                        })
                    )
                }
            )
        );
    
        return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
            descriptor.value = function (...args: any[]) {
                return stream$; // <- returns the same stream every time.
            };
        };
    }