Angular template binding with Observable async pipe issue [duplicate]

假如想象 提交于 2020-05-28 06:53:08

问题


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:

And if it goes this branch:

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

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


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.


回答1:


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.
        };
    };
}



回答2:


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

The solution is to use Property Decorator instead of Method Decorator, so the insuredType$ is now:

@NeedsElement(sp(115623),ap(116215))
readonly insuredType$: Observable<any>;

And the decorator is now

export function NeedsElement(...mappings: NeedsElementMapping[]) {
  ...
  const observable = of('').pipe(switchMap(() => {
    ...
  })
  return (target: any, propertyKey: string) => {
    const getter = () => {
      return observable;
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      enumerable: true,
      configurable: true,
    });
  };
}

Note it MUST define the observable outside of the returning function, otherwise it will still fall into the endless loop, say the following code won't work:

export function NeedsElement(...mappings: NeedsElementMapping[]) {
  ...
  return (target: any, propertyKey: string) => {
    const getter = () => {
      return of('').pipe(switchMap(() => {
        ...
      });
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      enumerable: true,
      configurable: true,
    });
  };
}


来源:https://stackoverflow.com/questions/61771578/angular-template-binding-with-observable-async-pipe-issue

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!