Angular 9 - рендеринг ограниченного числа дочерних компонентов - PullRequest
2 голосов
/ 30 апреля 2020

У меня есть компонент ButtonGroup, который будет отображать определенное количество компонентов ButtonAction. Я попытался назначить свойство шаблона (TemplateRef) для каждого действия ButtonAction, чтобы я мог включить oop и передать их в шаблон ng (через * ngTemplateOutlet). Я напрямую внедряю TemplateRef в конструктор ButtonAction, но получаю ошибку «Нет поставщика для TemplateRef» . Поскольку моя цель состоит в том, чтобы отобразить только ограниченное число дочерних элементов компонента, другое решение, которое я нашел, - получить доступ к шаблону через директиву. Но я не хочу заставлять нашего пользователя использовать директиву для каждого ребенка.
Итак, как мне это сделать?

@Component({
  selector: 'button-group',
  template: `
    <div>
       <ng-content *ngIf="canDisplayAllChildren; else limitedChildren"></ng-content>

       <ng-template #limitedChildren>
         <ng-container *ngFor="let button of buttons">
           <ng-template [ngTemplateOutlet]="button.template"></ng-template>
         </ng-container>
       </ng-template>

       <button-action (click)="toggle()" *ngIf="shouldLimitChildren">
         <icon [name]="'action-more-fill-vert'"></icon>
       </button-action>
    </div>
  `,
})
export class ButtonGroupComponent {
    @Input()
    public maxVisible: number;

    @ContentChildren(ButtonActionComponent) 
    public buttons: QueryList<ButtonActionComponent>;

    public isClosed: boolean = true;

    public toggle() {
        this.isClosed = !this.isClosed;
    }

    public get shouldLimitChildren() {
        return this.hasLimit && this.buttons.length > this.maxVisible;
    }

    public get canDisplayAllChildren() {
        return !this.shouldLimitChildren || this.isOpen;
    }   
}

Где ButtonActionComponent:

@Component({
  selector: "button-action",
  template: `
    ...
  `
})
export class ButtonActionComponent {
    ...
  constructor(public element: ElementRef, public template: TemplateRef<any>) {}
}

1 Ответ

1 голос
/ 04 мая 2020

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

Невозможность использовать TemplateRef без учета структурных директив я подумал о механизме, который похож на React.cloneElement API.


Итак, давайте определим базовый c ButtonComponent, который будет использоваться как дочерний элемент ButtonGroupComponent.

// button.component.ts

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

@Component({
  selector: "app-button",
  template: `
    <button>{{ text }}</button>
  `
})
export class ButtonComponent {
  @Input()
  public text: string;
}


GroupComponent должен выполнять клонирование и добавлять к его просмотру только те дочерние числа, которые указаны в свойстве ввода maxVisible, которое я также дал по умолчанию POSITIVE_INFINITY значение для случаев, когда оно вообще не предоставлено, что позволяет показывать всех детей:

// group.component.ts

...

@Input()
public maxVisible: number = Number.POSITIVE_INFINITY;

...

Давайте попросим Angular предоставить детей, указанных в нашем контенте (я бы сказал, что это лучшее объяснение разницы: { ссылка }):

// group.component.ts

...

@ContentChildren(ButtonComponent)
private children: QueryList<ButtonComponent>;

...

Теперь нам нужно позволить Angular добавить несколько вещей:

  1. наш текущий контейнер где вручную создать экземпляр chil dren to;
  2. фабричный распознаватель, который поможет нам создавать компоненты на лету;
// group.component.ts

...

constructor(
  private container: ViewContainerRef,
  private factoryResolver: ComponentFactoryResolver
) {}

private factory = this.factoryResolver.resolveComponentFactory(ButtonComponent);

...

Теперь, когда нам дали все, что нам нужно от Angular, мы можем перехватить инициализацию контента, реализуя интерфейс AfterContentInit и добавив жизненный цикл ngAfterContentInit.

Нам нужно циклически перемещаться по дочерним элементам, создавать новые компоненты на лету и устанавливать все свойства publi c нового компоненты для данных детей:

// group.component.ts

...

ngAfterContentInit() {
  Promise.resolve().then(this.initChildren);
}

private initChildren = () => {
  // here we are converting the QueryList to an array
  this.children.toArray()

    // here we are taking only the elements we need to show
    .slice(0, this.maxVisible)

    // and for each child
    .forEach(child => {

      // we create the new component in the container injected
      // in the constructor the using the factory we created from
      // the resolver, also given by Angular in our constructor
      const component = this.container.createComponent(this.factory);

      // we clone all the properties from the user-given child
      // to the brand new component instance
      this.clonePropertiesFrom(child, component.instance);
    });
};

// nothing too fancy here, just cycling all the properties from
// one object and setting with the same values on another object
private clonePropertiesFrom(from: ButtonComponent, to: ButtonComponent) {
  Object.getOwnPropertyNames(from).forEach(property => {
    to[property] = from[property];
  });
}

...

Полный GroupComponent должен выглядеть следующим образом:

// group.component.ts

import {
  Component,
  ContentChildren,
  QueryList,
  AfterContentInit,
  ViewContainerRef,
  ComponentFactoryResolver,
  Input
} from "@angular/core";
import { ButtonComponent } from "./button.component";

@Component({
  selector: "app-group",
  template: ``
})
export class GroupComponent implements AfterContentInit {
  @Input()
  public maxVisible: number = Number.POSITIVE_INFINITY;

  @ContentChildren(ButtonComponent)
  public children: QueryList<ButtonComponent>;

  constructor(
    private container: ViewContainerRef,
    private factoryResolver: ComponentFactoryResolver
  ) {}

  private factory = this.factoryResolver.resolveComponentFactory(
    ButtonComponent
  );

  ngAfterContentInit() {
    Promise.resolve().then(this.initChildren);
  }

  private initChildren = () => {
    this.children
      .toArray()
      .slice(0, this.maxVisible)
      .forEach(child => {
        const component = this.container.createComponent(this.factory);
        this.clonePropertiesFrom(child, component.instance);
      });
  };

  private clonePropertiesFrom(from: ButtonComponent, to: ButtonComponent) {
    Object.getOwnPropertyNames(from).forEach(property => {
      to[property] = from[property];
    });
  }
}

Остерегайтесь того, что мы создаем ButtonComponent в время выполнения, поэтому нам нужно добавить его в массив entryComponents AppModule (вот ссылка: https://angular.io/guide/entry-components).

// app.module.ts

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppComponent } from "./app.component";
import { ButtonComponent } from "./button.component";
import { GroupComponent } from "./group.component";

@NgModule({
  declarations: [AppComponent, ButtonComponent, GroupComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents: [ButtonComponent]
})
export class AppModule {}

С этими два простых компонента, вы должны быть в состоянии отобразить только подмножество данных дочерних элементов, поддерживая очень четкое использование:

<!-- app.component.html -->

<app-group [maxVisible]="3">
  <app-button [text]="'Button 1'"></app-button>
  <app-button [text]="'Button 2'"></app-button>
  <app-button [text]="'Button 3'"></app-button>
  <app-button [text]="'Button 4'"></app-button>
  <app-button [text]="'Button 5'"></app-button>
</app-group>

В этом случае должны отображаться только первый, второй и третий дочерние элементы.


Коды и коробка, которые я тестировал накануне Вот что: https://codesandbox.io/s/nervous-darkness-6zorf?file= / src / app / app.component. html

Надеюсь, это поможет.

...