Повторно используемые компоненты в Angular

Согласно принципу единой ответственности, все данные вашего приложения должны иметь одну цель. Если вы будете следовать этому принципу при создании вашего приложения Angular, его будет проще разрабатывать и тестировать.

В Angular есть NgTemplateOutlet, который позволяет легко модифицировать компоненты для различных вариантов использования без необходимости изменять сам компонент.

В этом мануале мы возьмем существующий компонент и перепишем его для поддержки NgTemplateOutlet.

Требования

Локальная установка Node.js. Следуйте инструкциям по установке Node.js и созданию локальной среды разработки для вашего дистрибутива: Mac OSUbuntuCentOSDebian..

Базовое понимание настройки проекта Angular.

Это руководство протестировано на Node v16.6.2, npm v7.20.6 и @angular/core v12.2.0.

1: Создание CardOrListViewComponent

Давайте рассмотрим компонент CardOrListViewComponent, который отображает элементы в формате ‘card’ или ‘list’ в зависимости от режима (mode).

Он состоит из файла card-or-list-view.component.ts:

import {
  Component,
  Input
} from '@angular/core';

@Component({
  selector: 'card-or-list-view',
  templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {

  @Input() items: {
    header: string,
    content: string
  }[] = [];

  @Input() mode: string = 'card';

}

И шаблона card-or-list-view.component.html:

<ng-container [ngSwitch]="mode">
  <ng-container *ngSwitchCase="'card'">
    <div *ngFor="let item of items">
      <h1>{{item.header}}</h1>
      <p>{{item.content}}</p>
    </div>
  </ng-container>
  <ul *ngSwitchCase="'list'">
    <li *ngFor="let item of items">
      {{item.header}}: {{item.content}}
    </li>
  </ul>
</ng-container>

Вот пример использования этого компонента:

import { Component } from '@angular/core';

@Component({
  template: `
    <card-or-list-view
        [items]="items"
        [mode]="mode">
    </card-or-list-view>
`
})
export class UsageExample {
  mode = 'list';
  items = [
    {
      header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
      content: 'The single responsibility principle...'
    } // ... more items
  ];
}

Этот компонент не очень гибкий и не соответствует принципу единой ответственности. Он должен отслеживать свой mode и знать, как отображать элементы в виде card или list. И он может отображать только элементы с заголовком и содержимым.

Давайте изменим это, разбив компонент на отдельные представления с помощью шаблонов.

2: Как работает ng-template и NgTemplateOutlet

Чтобы позволить компоненту CardOrListViewComponent отображать элементы любого типа, нужно указать ему, как их отображать. Мы можем предоставить ему шаблон, который он может использовать для штамповки элементов, items.

Шаблоны будут основаны на TemplateRefs с использованием <ng-template>, а штампы будут основаны на EmbeddedViewRefs, созданными из TemplateRefs. EmbeddedViewRef – это представления в Angular с собственным контекстом, они являются наименьшим строительным блоком.

Angular позволяет удалять представления из шаблонов с помощью NgTemplateOutlet.

NgTemplateOutlet — это директива, которая принимает TemplateRef и контекст и создает EmbeddedViewRef с предоставленным контекстом. Доступ к контексту в шаблоне осуществляется через атрибуты let-{{templateVariableName}}=”contextProperty” для создания переменной, которую может использовать шаблон. Если имя свойства контекста не указано, будет установлено свойство $implicit.

Вот пример:

import { Component } from '@angular/core';

@Component({
  template: `
    <ng-container *ngTemplateOutlet="templateRef; context: exampleContext"></ng-container>
    <ng-template #templateRef let-default let-other="aContextProperty">
      <div>
        $implicit = '{{default}}'
        aContextProperty = '{{other}}'
      </div>
    </ng-template>
`
})
export class NgTemplateOutletExample {
  exampleContext = {
    $implicit: 'default context property when none specified',
    aContextProperty: 'a context property'
  };
}

Пример вернет такой результат:

<div>
  $implicit = 'default context property when none specified'
  aContextProperty = 'a context property'
</div>

Переменные default и other предоставляются реквизитами let-default и let-other=”aContextProperty”.

3: Рефакторинг CardOrListViewComponent

Чтобы обеспечить гибкость компонента CardOrListViewComponent и позволить ему отображать элементы любого типа, мы создадим две структурные директивы для чтения в качестве шаблонов. Эти шаблоны будут представлять элементы card и list.

Так выглядит элемент card-item.directive.ts:

import { Directive } from '@angular/core';

@Directive({
  selector: '[cardItem]'
})
export class CardItemDirective {

  constructor() { }

}

А вот элемент list-item.directive.ts:
import { Directive } from '@angular/core';

@Directive({
  selector: '[listItem]'
})
export class ListItemDirective {

  constructor() { }

}
CardOrListViewComponent (card-or-list-view.component.ts) импортирует CardItemDirective и ListItemDirective:
import {
  Component,
  ContentChild,
  Input,
  TemplateRef
} from '@angular/core';
import { CardItemDirective } from './card-item.directive';
import { ListItemDirective } from './list-item.directive';

@Component({
  selector: 'card-or-list-view',
  templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {

  @Input() items: {
    header: string,
    content: string
  }[] = [];

  @Input() mode: string = 'card';

  @ContentChild(CardItemDirective, {read: TemplateRef}) cardItemTemplate: any;
  @ContentChild(ListItemDirective, {read: TemplateRef}) listItemTemplate: any;

}

Этот код будет читаться в структурных директивах как TemplateRefs.

<ng-container [ngSwitch]="mode">
  <ng-container *ngSwitchCase="'card'">
    <ng-container *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="cardItemTemplate"></ng-container>
    </ng-container>
  </ng-container>
  <ul *ngSwitchCase="'list'">
    <li *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="listItemTemplate"></ng-container>
    </li>
  </ul>
</ng-container>

Вот пример использования этого компонента:

import { Component } from '@angular/core';

@Component({
  template: `
    <card-or-list-view
        [items]="items"
        [mode]="mode">
      <div *cardItem>
        Static Card Template
      </div>
      <li *listItem>
        Static List Template
      </li>
    </card-or-list-view>
`
})
export class UsageExample {
  mode = 'list';
  items = [
    {
      header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
      content: 'The single responsibility principle...'
    } // ... more items
  ];
}

После этих изменений компонент CardOrListViewComponent может отображать любой тип элемента в форме card или list на основе предоставленного шаблона. В настоящее время шаблоны статичны.

Последнее, что нам осталось сделать, это сделать шаблоны динамическими, задав им контекст:

<ng-container [ngSwitch]="mode">
  <ng-container *ngSwitchCase="'card'">
    <ng-container *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="cardItemTemplate; context: {$implicit: item}"></ng-container>
    </ng-container>
  </ng-container>
  <ul *ngSwitchCase="'list'">
    <li *ngFor="let item of items">
      <ng-container *ngTemplateOutlet="listItemTemplate; context: {$implicit: item}"></ng-container>
    </li>
  </ul>
</ng-container>

Вот так можно использовать этот компонент:

import { Component } from '@angular/core';

@Component({
  template: `
    <card-or-list-view
        [items]="items"
        [mode]="mode">
      <div *cardItem="let item">
        <h1>{{item.header}}</h1>
        <p>{{item.content}}</p>
      </div>
      <li *listItem="let item">
        {{item.header}}: {{item.content}}
      </li>
    </card-or-list-view>
`
})
export class UsageExample {
  mode = 'list';
  items = [
    {
      header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
      content: 'The single responsibility principle...'
    } // ... more items
  ];
}

Здесь важно отметить, что мы используем  микросинтаксис в качестве синтаксического сахара. Это то же самое, что:

<ng-template cardItem let-item>
  <div>
    <h1>{{item.header}}</h1>
    <p>{{item.content}}</p>
  </div>
</ng-template>

Теперь у компонента есть его исходная функциональность, в дополнение к которой мы можем отображать все, что захотим, путем изменения шаблонов. А еще у CardOrListViewComponent теперь меньше ответственности. Мы можем добавить больше данных в контекст элемента (например, first или last, как в ngFor) и отображать совершенно разные типы элементов.

Заключение

В этом мануале мы переписали существующий компонент с помощью NgTemplateOutlet.

Tags:

Добавить комментарий