Я уже писал несколько статей об использовании NgTemplateOutlet или службы на уровне компонентов для реализации компонента с динамическими дочерними компонентами, например:

<google-map [key]="apiKey">
  <google-map-marker *ngFor="let marker of markers"
    [lat]="marker.lat"
    [lng]="marker.lng">
  </google-map-marker>
</google-map>

<google-map> — это родительский компонент, который инициализирует и отображает карту Google. <google-map-marker> — это дочерний компонент, который можно динамически добавлять для создания маркеров на карте. Если вам не нравятся как NgTemplateOutlet, так и способы обслуживания на уровне компонентов, здесь у меня есть еще одна хакерская альтернатива, в которой используются @ContentChildren и ngAfterContentChecked.

Что такое ContentChildren?

@ContentChildren — это встроенный декоратор Angular, позволяющий создавать дочерние элементы контента, что означает, что ваши дочерние компоненты находятся внутри вашего <ng-content>. Вы можете просто представить его как ванильный getElementsByTagName, но он работает немного по-другому.

Что такое ngAfterContentChecked?

ngAfterContentChecked — это хук жизненного цикла Angular, который вызывается после того, как обнаружение изменений проверило проецируемый контент. Следовательно, если вы поместите здесь свою логику обновления представления, можно гарантировать, что вы используете последнее состояние своих дочерних элементов для обновления своего представления. Тем не менее, этот крючок на самом деле очень хитрый. Это относится к проверенному, но не измененному содержимому. Он запускается всякий раз, когда происходит обнаружение изменений, включая обнаружение изменений, инициированное вашим родителем. Когда вы помещаете логику обновления в этот хук, вы можете легко привести к такому бесконечному циклу: ngAfterContentChecked › обновление, которое вы просматриваете › некоторые другие функции запускают обнаружение изменений во время обновления › ngAfterContentChecked › … Я попытаюсь объяснить это более практично позже в реализации пример.

Пример с API карты Google

Я хотел бы использовать этот подход для реализации API карт Google в качестве демонстрации. Основная логика состоит в том, чтобы добавить обратный вызов в файл ngAfterContentChecked. Всякий раз, когда он срабатывает, мы получаем последнюю версию @ContentChildren. После этого мы обновляем наш родительский компонент на основе состояния наших дочерних элементов. Однако одна проблема здесь заключается в том, что метод добавления маркера на карту также запускает обнаружение изменений и, следовательно, хук ngAfterContentChecked, что приводит к бесконечному циклу. Таким образом, нам нужно проверить, есть ли фактические изменения в дочерних элементах, прежде чем обновлять представление. Если изменений нет, то никаких обновлений не делаем.

import { 
  Component, 
  ContentChildren, 
  ElementRef, 
  Input,
  QueryList, 
  ViewChild } from '@angular/core';
import GoogleMapsApiLoader from "google-maps-api-loader";
@Component({
  selector: 'google-map-marker',
  template: ``
})
export class GoogleMapMarkerComponent {
  @Input() lat: number;
  @Input() lng: number;
}
@Component({
  selector: 'google-map',
  template: `
    <div #mapContainer style="height: 500px"></div>
    <div #content><ng-content></ng-content></div>
  `
})
export class GoogleMapComponent {
  @Input() key: string;
  @ViewChild('mapContainer') 
  mapContainer: ElementRef;
  @ViewChild("content") 
  contentWrapper: ElementRef;
  content = null;
  @ContentChildren(GoogleMapMarkerComponent) 
  markers: QueryList<GoogleMapMarkerComponent>;
  markerObjs = [];
  
  google;
  map;
  ngAfterViewInit() {
    GoogleMapsApiLoader({
      apiKey: this.key
    }).then(googleMapApi => {
      this.google = googleMapApi;
      const mapContainer = this.mapContainer.nativeElement;
      this.map = new this.google.maps.Map(mapContainer, {
        zoom: 0,
        center: {lat: 0, lng: 0}
      });
      this.contentChanged();
    })
  }
  ngAfterContentChecked() {
    if (this.contentWrapper) {
      let current = this.contentWrapper.nativeElement.innerHTML;
      if (this.content != current) {
        this.content = current;
        this.contentChanged();
      }
    }
  }
  contentChanged() {
    this.updateMarker();
  }
  updateMarker() {
    if (!this.google || !this.map) 
      return;
    this.markerObjs.forEach(marker => { 
      marker.setMap(null);
    });
    const Marker = this.google.maps.Marker;
    this.markerObjs = this.markers
      .map(({ lat, lng }) => new Marker({
        position: { lat, lng },
        map: this.map
      }));
  }
}

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

Плюсы и минусы

Этот подход полностью отличается от NgTemplateOutlet или сервиса на уровне компонентов, поскольку вся логика хранится в родительском компоненте. Вот мое сравнение:

Плюсы:

  1. Вам не нужно поддерживать дополнительные службы, что означает меньше кода.
  2. В отличие от подхода NgTemplateOutlet, вам не нужна какая-то странная оболочка, например <ng-template>.

Минусы:

  1. Все в родительском компоненте, никакого разделения задач. Не подходит для сложных компонентов.
  2. Вы должны работать над проверкой изменения состояния самостоятельно, что может быть очень громоздким.
  3. Добавление слишком большого количества логики в ngAfterContentChecked может привести к снижению производительности.

Спасибо за чтение! Любые комментарии будут высоко оценены. :D

Читать далее