Romain Tete
Romain Tete

Reputation: 143

How to filter @ContentChildren in Angular components

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 !

Upvotes: 7

Views: 1873

Answers (2)

Simon_Weaver
Simon_Weaver

Reputation: 145950

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 :-)

Upvotes: 2

Romain Tete
Romain Tete

Reputation: 143

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

Upvotes: 5

Related Questions