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>

Похожие записи

@Attribute() декоратор

Аналогично @Input() позволяет получить значение атрибута с хоста компонента/директивы, но не отслеживает дальнейшее изменение атрибута.

15 сентября 2019 г. в Angular

Angular. Functions & getters в шаблонах

На каждый цикл механизма обнаружения изменений выполняется метод из шаблона. Если этого надо избежать, то следует использовать pure pipe или результат выполнения присвоить свойству компонента