AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

Кент С. Доддс написал статью об использовании prop getters в React. Наряду с реквизитами рендеринга (см. TemplateRefs являются реквизитами рендеринга Angular), геттеры свойств позволяют авторам библиотек компонентов предоставлять пользователям максимально возможный контроль над рендерингом - компоненту нужно только выполнять свою работу.

Если я правильно понимаю статью Кента, есть 3 вещи, которые вы можете сделать с помощью проп-геттеров:

  1. Сообщите компоненту, где применять атрибуты / слушатели
  2. Зарегистрируйте несколько слушателей событий
  3. Изменить примененные атрибуты

Я собираюсь показать, как вы можете использовать директивы содержимого для выполнения тех же трех задач. Директивы содержимого - это директивы Angular, которые помещаются в содержимое (в отличие от представления / шаблона) компонента. Затем компонент может взаимодействовать с этими директивами, используя @ContentChildren.

Для этого я буду использовать в качестве примера понижающую передачу. Вот пример кода React, который использует понижающую передачу (напрямую скопирован из связанной статьи):

import React from 'react'
import {render} from 'react-dom'
import Downshift from 'downshift'
const items = ['apple', 'pear', 'orange', 'grape', 'banana']
render(
  <Downshift onChange={selection => alert(`You selected ${selection}`)}>
    {({
      getInputProps,
      getLabelProps,
      getItemProps,
      isOpen,
      inputValue,
      highlightedIndex,
      selectedItem,
    }) =>
      <div>
        <label {...getLabelProps()}>Enter a fruit</label>
        <input {...getInputProps()} />
        {isOpen ? (
          <div>
            {items
              .filter(i => !inputValue || i.includes(inputValue))
              .map((item, index) =>
                <div
                  {...getItemProps({
                    key: item,
                    index,
                    item,
                    style: {
                      backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
                      fontWeight: selectedItem === item ? 'bold' : 'normal',
                    },
                  })}
                >
                  {item}
                </div>,
              )}
          </div>
        ) : (
          null
        )}
      </div>}
  </Downshift>,
  document.getElementById('root'),
)

А вот тот же пример с использованием мифического компонента ngx-downshift, который использует директивы содержимого в Angular:

==== html file ====
<ngx-downshift #downshift="ngx-downshift" (change)="alert('You selected ' + $event)">
  <label downshiftLabel>Enter a fruit</label>
  <input type="text" downshiftInput
    (change)="filteredItems =
              items.filter(i => !$event || i.includes($event)">
  <div *ngIf="downshift.isOpen">
    <div *ngFor="item of filteredItems; let i = index"
      [style.backgroundColor]="downshift.highlightedIndex === i ? 'lightgray' : 'white'"
      [style.fontWeight]="downshift.selectedItem === item ? 'bold' : 'normal'"
      downshiftItem>
      {{item}}
    </div>
  </div>
</ngx-downshift>

==== ts file ====
@Component({ /* something here */ })
export class SomeComponent {
  items = ['apple', 'pear', 'orange', 'grape', 'banana'];
  alert(message: any) {
    console.log(message);
  }
}

Если бы мне пришлось написать этот пример с нуля, я бы поместил большую часть логики в файл ts вместо того, чтобы помещать так много в шаблон, но я написал его таким образом, чтобы попытаться отразить стиль примера с пониженной передачей.

1. Сообщите компоненту, где применять атрибуты / слушатели.

Вся обработка компонента ngx-downshift оставлена ​​на усмотрение пользователя компонента. Вы можете применить директивы downshiftLabel, downshiftInput и downshiftItem к любым элементам, которые хотите, и ngx-downshift затем применит все атрибуты, которые требуются к этим элементам.

2. Зарегистрируйте несколько прослушивателей событий.

На самом деле это не проблема Angular, поскольку Angular Essential автоматически выполняет логику callAll. Слушатели событий всегда добавляются, но не заменяются. Вы можете увидеть это даже без директив:

<button (click)="alert('hi')" (click)="alert('bye')">Say hi and bye</button>

Этот код покажет два предупреждения.

То же самое и для более сложного сценария директивы:

<button clickHi (click)="alert('bye')">Say hi and bye</button>
=== clickHiDirective.ts ===
@Directive({ selector: '[clickHi]' })
export class ClickHiDirective {
  @HostListener('click')
  alertHi() {
    alert('hi');
  }
}

Этот код также покажет два предупреждения.

3. Измените применяемые атрибуты.

Один из атрибутов, для которых понижающая передача применяется к входным данным, - aria-autocomplete="list". Понижение передачи делает это через getInputProps. Если бы мы хотели изменить это на aria-autocomplete="both", мы могли бы сделать это getInputProps({ 'aria-autocomplete': 'both' }). ngx-downshift обновит атрибут директивой downshiftInput. Нравится:

@Directive({ selector: '[downshiftInput]' })
export class DownshiftInputDirective {
  @HostBinding('attr.aria-autocomplete')
  @Input('attr.aria-autocomplete')
  ariaAutocomplete: 'list';
}

@HostBinding применяет атрибут к элементу хоста директивы. @Input вступает в игру, когда вы хотите перезаписать значение, например:

<input downshiftInput attr.aria-autocomplete="both" />

Обратите внимание, что порядок директивы и переопределения не имеет значения. Если пользователь указывает значение переопределения в любом месте элемента, оно переопределит версию директивы.

Мифический компонент ngx-downshift

ngx-downshift на самом деле не существует, но если вы хотите его создать, пожалуйста, создайте. Однако автозаполнение Angular Material использует многие те же концепции. Вот пример использования автозаполнения:

<mat-form-field>
   <input type="text" matInput [formControl]="myControl" [matAutocomplete]="auto">
</mat-form-field>

<mat-autocomplete #auto="matAutocomplete">
   <mat-option *ngFor="let option of options" [value]="option">
      {{ option }}
   </mat-option>
</mat-autocomplete> 

Как видите, matInput - это директива содержимого для mat-form-field. Затем он ссылается на компонент mat-autocomplete с помощью директивы matAutocomplete. Компонент mat-autocomplete действительно обрабатывает часть отрисовки списка автозаполнения за вас, но большая часть отрисовки остается на усмотрение пользователя.

Джереми Элборн и Майлз Малерба выступили на ng-conf 2017 с докладом, в котором отстаивали многие из тех же идей. Перейдите примерно к 5:00 и перейдите к тому моменту, на который я ссылаюсь.

Это для меня. Спасибо за прочтение!