I have a ButtonGroup component that will render a certain number of ButtonAction components.
I tried to assign a template property (TemplateRef) to each ButtonAction, so that I can loop on and pass them to a ng-template (via *ngTemplateOutlet).
I directly inject TemplateRef into the constructor of ButtonAction, but I get the error “No provider for TemplateRef”.
Since my aim is to render only a limited number of a component’s children, another solution I have found is to access the template via directive. But I don’t want to force our user to use the directive on each child.
So, how can I do?
@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;
}
}
Where ButtonActionComponent is:
@Component({
selector: "button-action",
template: `
...
`
})
export class ButtonActionComponent {
...
constructor(public element: ElementRef, public template: TemplateRef<any>) {}
}
It took me some time to come up with an ipothetical solution but I think I might have something useful that doesn't rely on explicit directive added to your component children.
Being unable to use TemplateRef
without structural directives involved, I thought about a mechanism which is similar to the React.cloneElement
API.
So, let's define a basic ButtonComponent
that will be used as a children of the 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;
}
The GroupComponent
should do clone and append to its View only the number children specified via the maxVisible
input property, which I also given a POSITIVE_INFINITY
default value for cases when it isn't provided at all, allowing all children to be shown:
// group.component.ts
...
@Input()
public maxVisible: number = Number.POSITIVE_INFINITY;
...
Let's ask Angular to provide children given in our content (I would say that this is the best explanation of the difference: https://stackoverflow.com/a/34327754/3359473):
// group.component.ts
...
@ContentChildren(ButtonComponent)
private children: QueryList<ButtonComponent>;
...
We now need to let Angular inject a couple of things:
// group.component.ts
...
constructor(
private container: ViewContainerRef,
private factoryResolver: ComponentFactoryResolver
) {}
private factory = this.factoryResolver.resolveComponentFactory(ButtonComponent);
...
Now that we have been given anything we need from Angular, we can intercept the content initialization implementing the AfterContentInit
interface and adding the ngAfterContentInit
lifecycle.
We need to cycle over our children, create new components on the fly and set all the public properties of the new components to the ones of the children given:
// 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];
});
}
...
The complete GroupComponent
should look like this:
// 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];
});
}
}
Beware that we are creating the ButtonComponent
at runtime, so we need to add it in the entryComponents
array of the AppModule
(here is the reference: 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 {}
With these two simple components, you should be able to render only a subset of the given children maintaining a very clear usage:
<!-- 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>
In this case, only the first, second and third children should be rendered.
The codesandbox I tested everything is this one: https://codesandbox.io/s/nervous-darkness-6zorf?file=/src/app/app.component.html
Hope this helps.