I want to create a component that can be used as follow :
<slides (close)="doSomething()">
<slide>
<header>Slide title</header>
<main>Slide content (any html)</main>
</slide>
<slide>
<header>Slide 2 title</header>
<main>...</main>
</slide>
...
</slides>
Then only one slide is displayed and you can navigate back and forth. To me, the SlidesComponent should orchestrate the individual SlideComponents. And SlideComponent should know very little and could very well be replaced by plain HTML.
My problem is that I can't find a proper way to filter the <slide>
to only display one at a time.
My first guess was to use the @ContentChildren DOM query and then somehow filter out the content to only output in the DOM one slide at a time.
I used @ContentChildren(SlideComponent) slides: QueryList<SlideComponent>;
It does provide me with the collection of the projected SlideComponents. But, I did not find a way to reuse a component's EmbeddedView to simply 'mount' it programatically.
My only success so far has been to set a property on each SlideComponent and rely on a *ngIf
directive to output nothing, from the SlideComponent's template, when the property is false. It doesn't sound very right.
I seem to either be wrong on the approach, or be missing key concepts about content projection, DOM Queries or EmbeddedViews.
Your comments and suggestions are very welcomed. Thanks !
ContentChild and ContentChildren in Angular. The ContentChild & ContentChildren are decorators, which we use to Query and get the reference to the Projected Content in the DOM. Projected content is the content that this component receives from a parent component. The ContentChild & ContentChildren is very similar to the ViewChild & ViewChildren.
There are times when a parent component needs access to his children. Let’s see how we can handle this with Angular. For example, let’s create a simple alert component. Now let’s use this component multiple times in our app component and use the @ViewChildren decorator. We can use the @ViewChildren decorator to grab elements from the host view.
As a selector for @ContentChild and @ContentChildren, we can pass directive, component or local template variable. By default @ContentChildren only selects direct children of content DOM and not all descendants. @ContentChildren has a metadata descendants and setting its value true, we can fetch all descendant elements.
This means the angular ContentChild element is placed between opening and closing tags of the host of a given component is called ContentChild. The @ViewChild can access only HTML elements/components that are part of the view but not inside the content projection of the “ng-content” directive.
I think I have found a satisfactory way to do what I want.
The short answer is to use structural directives. They come packed with the necessary features.
A longer answer (full code) :
The high level template :
<slides>
<div *slide>
<header>Titre</header>
<main>Content</main>
</div>
<span *slide>
Anything I want, it is kept
</span>
</slides>
The SlideDirective (the actual way around my issue) :
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[slide]'
})
export class SlideDirective {
constructor(
private template: TemplateRef<any>,
private container: ViewContainerRef
) {}
display() {
this.container.createEmbeddedView(this.template);
}
hide() {
this.container.detach();
}
}
The SlidesComponent :
export class SlidesComponent implements AfterContentInit {
@ContentChildren(SlideDirective) slides: QueryList<SlideDirective>;
@ViewChild('slideContainer') slideContainer: ViewContainerRef;
slideIndex = 0;
slide: SlideDirective;
constructor() {}
ngAfterContentInit() {
this.onSlideSelect(null, 0);
}
onSlideSelect(event: any, index: number) {
const filtered = this.slides.filter((s: SlideDirective, i: number) => {
return i === index;
});
const slide: SlideDirective = filtered[0];
if (this.slide) {
this.slide.hide();
}
slide.display();
this.slideIndex = index;
this.slide = slide;
}
}
The SlidesComponent template:
<main class="slide-viewport">
<ng-content></ng-content>
</main>
<nav class="actions">
<button (click)="onSkip()">Skip</button>
<div class="carousel-buttons">
<button *ngFor="let slide of slides; let i = index;" (click)="onSlideSelect($event, i)" class="carousel-button" [ngClass]="{ active: i == slideIndex }"></button>
</div>
<button (click)="onNext()" *ngIf="slideIndex < (slides.length - 1)">Next</button>
<button (click)="onComplete()" *ngIf="slideIndex === (slides.length - 1)">Complete</button>
</nav>
I am quite happy with that solution but I remain interested in comments and suggestions
This is quite similar to a tab group, such as the Angular Material tab control.
The source code for angular material tabs may be of interest
MatTab https://github.com/angular/material2/blob/569c2219e468077b563d2ccce2624851605c1df0/src/lib/tabs/tab-group.ts
MatTabGroup https://github.com/angular/material2/blob/bd3d0858f7f3e1ba4dbb1e8ac1226f2f9748ec69/src/lib/tabs/tab.ts
It's hard at first to see exactly where they add and show the tab contents - but the magic is here:
ngOnInit(): void {
this._contentPortal = new TemplatePortal(
this._explicitContent || this._implicitContent, this._viewContainerRef);
}
They use something called 'Portals' (part of the Angular Material CDK (lightweight components/utilities) in order to manage the contents.
If you want to get really sophisticated check it out :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With