Мне потребовалось некоторое время, чтобы придумать гипотетическое решение, но я думаю, что у меня может быть что-то полезное, что не зависит от явной директивы, добавленной к вашим дочерним компонентам.
Невозможность использовать 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 добавить несколько вещей:
- наш текущий контейнер где вручную создать экземпляр chil dren to;
- фабричный распознаватель, который поможет нам создавать компоненты на лету;
// 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
Надеюсь, это поможет.