Asynchronous pipes in Angular
Angular expects pipes to be synchronous. They should return a resolved value, not a Promise
or an Observable
. To use a pipe that returns an unresolved value, you can use Angular's async
pipe. If that's not an option, you can resolve the asynchronous value inside the pipe if you make it impure, as I describe below.
As an example, I am using a pipe to format a boolean value into a localized string (using ngx-translate to translate the string). This pipe may need to be asynchronous because this is how translations can be accessed: with the get
method returning an Observable<string>
.
To avoid the problem altogether and make the pipe synchronous, it can return the translation key instead of the translation:
@Pipe({
name: "formatBool",
})
export class FormatBoolPipe implements PipeTransform {
transform(value: boolean): string {
return value ? "BOOLEAN.TRUE" : "BOOLEAN.FALSE";
}
}
You can then use the translate
pipe from ngx-translate to get the translation in the template:
<div>Value: {{ boolValue | formatBool | translate }}</div>
As long as the pipe returns a translation key for each input, this is probably the best approach. If it does not, it may be necessary for the pipe to return actual translations. As mentioned earlier, this requires the pipe to be asynchronous:
@Pipe({
name: "formatBool",
})
export class FormatBoolPipe implements PipeTransform {
constructor(private readonly translate: TranslateService) {}
transform(value: boolean): Promise<string> {
const translationKey = value ? "BOOLEAN.TRUE" : "BOOLEAN.FALSE";
return this.translate.get(translationKey).toPromise();
}
}
Interpolating a promise in the template will not work as expected. The renderer will not resolve the value. Fortunately, the built-in async
pipe can be used for this:
<div>Value: {{ boolValue | formatBool | async }}</div>
If for some reason you need to make your pipe impure, that will not work either. When the async
pipe resolves the Promise
, the change triggers the impure pipe again, which in turn returns a new Promise
for the async
pipe to resolve. Eventually, the browser will suggest to the user to stop your application.
This infinite loop can only be avoided if your impure pipe resolves the Promise
itself. The approach can be similar to the one I described in my previous blog post about efficient impure pipes. The pipe must cache the input and output to return the cached output value if the input matches the cached input value.
@Pipe({
name: "formatBool",
pure: false,
})
export class FormatBoolPipe implements PipeTransform {
private lastValue: boolean | undefined;
private formattedValue: string | undefined;
constructor(
private readonly translate: TranslateService,
private readonly ref: ChangeDetectorRef
) {}
transform(value: boolean): string | undefined {
if (value === this.lastValue) {
return this.formattedValue;
}
const translationKey = value ? "BOOLEAN.TRUE" : "BOOLEAN.FALSE";
this.translate
.get(translationKey)
.toPromise()
.then((formattedValue) => {
this.formattedValue = formattedValue;
this.lastValue = value;
this.ref.markForCheck();
});
return this.formattedValue;
}
}
If the input does not match the cached value, the pipe cannot return the output for the new input because it does not have it ready in time. It still returns the cached output for the previous input.
When the Promise
is finally resolved, it updates the values in the cache. But in order for the new output to be rendered, it must tell the renderer that the value has changed. This can be accomplished by calling the markForCheck
method on the injected ChangeDetectorRef
instance.
In response, the renderer will call the pipe again with the same input as before, since it has not changed. This means that the newly updated cached output value is returned immediately. And the pipe is not called again until the input changes, so there is no danger of an infinite loop.
You can see the final pipe in action in the sample project I pushed to my GitHub repository. Earlier commits include other pipe implementations mentioned in this post.
If your pipe needs to return a Promise
or an Observable
, you should just pass its result to the built-in async
pipe unless it is impure. If you can not avoid making it impure and still need to include asynchronous code, you are out of luck with the async
pipe. In that case, you need to resolve the asynchronous values within the pipe. By caching inputs and outputs in the pipe, you can do this without too much overhead.