Вам когда-нибудь хотелось отобразить состояние загрузки, пока ngIf ждет ответа от async-пайпа? Или, может, вы мечтали передать в ngFor шаблон для пустого массива? Возможно, вы бросили это, потому что вам не хотелось реализовывать базовую логику этих директив самому. На самом деле в этом нет нужды! Один и тот же селектор может подцепить несколько директив, что позволяет расширить функциональность встроенных директив дополнительной логикой.

NgForOf, более известная как ngFor, — знакомая всем директива для перебора списка. Довольно часто список бывает пустым и необходимо отображать сообщение «Ничего не найдено». Эту возможность уже просили на github и даже набрали 169 лайков. В итоге ее завернули, объясняя решение нежеланием раздувать core-пакет. Мы и так можем использовать ngIf для проверки длины. Но тогда это раздует бандл самого приложения. Так что давайте попробуем добавить эту возможность, дописывая минимум кода.

* — это синтаксический сахар для передачи инпутов

Если вы прочли и поняли описание синтаксиса выше, то ваш IQ измеряется четырехзначным числом. Но на деле все не так сложно, как кажется. По сути, если бы у нас был инпут ngForEmpty, мы могли бы написать: *ngFor="let item of items empty template" и передать template reference-переменную для пустого состояния. Так что давайте добавим свою директиву с селектором ngFor, у которой будет такой инпут:

@Directive({
   selector: '[ngFor][ngForOf][ngForEmpty]',
})
export class TuiForDirective implements OnChanges {
   @Input()
   ngForOf = [];

   @Input()
   ngForEmpty: TemplateRef<{}>;

   private ref?: EmbeddedViewRef<{}>;

   constructor(private readonly vcr: ViewContainerRef) {}

   ngOnChanges() {
       this.ref?.destroy();

       if (this.ngForOf?.length === 0) {
           this.ref = this.vcr.createEmbeddedView(this.ngForEmpty);
       }
   }
}

Вот и все! Когда ngFor меняется, мы уничтожаем наш шаблон пустого списка, и если массив пустой, создаем его. Такое количество кода наш бандл потянет!

Небинарный ngIf

NgIf позволяет показывать разные шаблоны исходя из доступности данных. Но иногда этого недостаточно. Допустим, мы хотим различать присутствие и отсутствие данных, загрузку и ошибку. Что если я скажу вам, что этому безумному синтаксису для работы не хватает всего нескольких строк?

<p *ngIf="value$ | async as value else default or loading but error">
  {{value}}
</p>

Давайте расширим ngIf с помощью or для загрузки и but для ошибки. Ниже я привел заготовку такой директивы с комментариями:

@Directive({
  selector: '[ngIf][ngIfOr],[ngIf][ngIfBut]'
})
export class NgIfAugmentedDirective<T> implements OnChanges {
  // Следим за значением
  @Input()
  ngIf: unknown = false;

  // Следим за оригинальным шаблоном
  @Input()
  ngIfThen: TemplateRef<NgIfContext<T>> = this.templateRef;

  // Следим за шаблоном “else”
  @Input()
  ngIfElse: TemplateRef<NgIfContext<T>> | null = null;

  // Шаблон состояния загрузки
  @Input()
  ngIfOr: TemplateRef<NgIfContext<T>> | null = null;

  // Шаблон состояния ошибки
  @Input()
  ngIfBut: TemplateRef<NgIfContext<T>> | null = null;

  constructor(
    // Оригинальная директива *ngIf 
    private readonly directive: NgIf<T>,
    private readonly templateRef: TemplateRef<NgIfContext<T>>
  ) {}
}

Мы инжектим ngIf, чтобы он делал всю работу за нас. Используем null как индикатор загрузки, потому что его выдает async-пайп, пока данные еще не прилетели. Для ошибки будем проверять значение через instanceof Error.

Все, что нужно, будет происходить внутри ngOnChanges. Мы рассматриваем значение и передаем нужный шаблон в ngIf. При необходимости сбрасываем его на исходное значение:

ngOnChanges() {
  // При загрузке данных нет, поэтому мы
  // передаем шаблон в ngIfElse и выходим
  if (this.ngIf === null && this.ngIfOr) {
    this.directive.ngIfElse = this.ngIfOr;

    return;
  }

  // Данные типа Error означают, что мы передаем
  // шаблон в ngIfThen и делаем ранний выход
  if (this.ngIf instanceof Error && this.ngIfBut) {
    this.directive.ngIfThen = this.ngIfBut;

    return;
  }

  // Если ни то ни то не выполнилось,
  // сбрасываем шаблоны
  if (this.directive.ngIfThen !== this.ngIfThen) {
    this.directive.ngIfThen = this.ngIfThen;
  }

  if (this.directive.ngIfElse !== this.ngIfElse) {
    this.directive.ngIfElse = this.ngIfElse;
  }
}

Четыре условия и четыре строчки кода — этого достаточно. Обратите внимание на селектор. Директива создастся, если мы предоставим тот или иной шаблон через сахар или явные параметры.

Результат

NgFor часто используется вместе с async-пайпом. Добавить состояние загрузки к нашей директиве потребует всего пару строк. Итоговый результат можно увидеть на этом StackBlitz.

Директивы Angular предоставляют удобный способ разбить логику на части. Возможность повесить несколько директив на один селектор позволяет это делать еще более гибко. Даже структурные директивы и синтаксический сахар работают так же. Имейте это в виду, когда в следующий раз понадобится сархитектурить какое-то сложное решение ????

Комментарии (2)


  1. DNbazh
    27.09.2021 21:30
    +1

    Крутой и лаконичный подход к работе с загружаемыми данными. Спасибо за статью) Я обнаружил небольшую ошибку. В примере с кодом ngFor перед ngForEmpty: TemplateRef<{}>; нехватает декоратора Input().


    1. Waterplea Автор
      28.09.2021 10:33

      Спасибо, поправил!