Angular Let Directive
Проблема async
и *ngIf
Допустим имеется 2 компонента, которые должны получать значение из потока Observable
<app-number [number]="observableNumber$ | async"></app-number>
<app-number-special [number]="observableNumber$ | async"></app-number-special>
Чтобы не было 2 запросов, значением потока можно поделиться используя rxjs оператор share. Более подробно на русском
Альтернативная возможность через структурную директиву ngIf
:
<ng-container *ngIf="observableNumber$ | async as n">
<app-number [number]="n"></app-number>
<app-number-special [number]="n"></app-number-special>
</ng-container>
Нюанс в том, что *ngIf
не отображает содержимое в falsy
случаях (0, null, undefined).
Есть пакет @rx-angular/template, направленный на решение этой проблемы + улучшение производительности. Первые наработки этого пакета присутствуют в @ngrx/component
<ng-container *rxLet="observableNumber$; let n">
<app-number [number]="n"></app-number>
<app-number-special [number]="n"></app-number-special>
</ng-container>
<ng-container *rxLet="observableNumber$ as n">
<app-number [number]="n"></app-number>
</ng-container>
Дополнительно умеет получать ошибки и завершение потока
<ng-container *rxLet="observableNumber$; let n; let e = $error, let c = $complete">
<app-number [number]="n" *ngIf="!e && !c"></app-number>
<ng-container *ngIf="e">
There is an error: {{ e }}
</ng-container>
<ng-container *ngIf="c">
Observable completed: {{ c }}
</ng-container>
</ng-container>
Решение из ISPsystem
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
interface LetContext<T> {
ispLet: T;
}
@Directive({
selector: '[ispLet]',
})
export class ISPLetDirective<T> {
private _context: LetContext<T> = { ispLet: null };
constructor(_viewContainer: ViewContainerRef, _templateRef: TemplateRef<LetContext<T>>) {
_viewContainer.createEmbeddedView(_templateRef, this._context);
}
@Input()
set ispLet(value: T) {
this._context.ispLet = value;
}
}
<ng-container *ispLet="stream$ | async as value">
{{ value }}
</ng-container>
<ng-container *ispLet="method(param) as value">
{{ value }}
</ng-container>
Решение из Tinkoff
https://twitter.com/Waterplea/status/1268478931935657985
import {
Directive,
Inject,
Input,
TemplateRef,
ViewContainerRef
} from "@angular/core";
export class LetContext<T> {
constructor(private readonly dir: LetDirective<T>) {}
get ngLet(): T {
return this.dir.ngLet;
}
}
/**
* Works like *ngIf but does not have a condition
* Use it to declare the result of pipes calculation
* (i.e. async pipe)
*/
@Directive({
selector: "[ngLet]"
})
export class LetDirective<T> {
@Input()
ngLet: T;
constructor(
@Inject(ViewContainerRef) viewContainer: ViewContainerRef,
@Inject(TemplateRef) templateRef: TemplateRef<LetContext<T>>
) {
viewContainer.createEmbeddedView(templateRef, new LetContext<T>(this));
}
}
<ng-container *ngLet="stream$ | async as value">
{{ value }}
</ng-container>