问题
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