2

I'm attempting to make a reusable tab component and am confused about how to iterate over multiple ContentChildren (corrected) within the component to wrap them in html.

I've got a component in my view

<demo-tabs>
  <a routerLink="/some/link">Tab 1</a>
  <a routerLink="/some/other/link">Tab 2</a>
  <a routerLink="/some/third/link">Tab 3</a>
</demo-tabs>

which I'd like to render like so:

<ul>
  <li><a routerLink="/some/link">Tab 1</a></li>
  <li><a routerLink="/some/other/link">Tab 2</a></li>
  <li><a routerLink="/some/third/link">Tab 3</a></li>
<ul>

It doesn't seem like I can embed content in ng-content which is what I tried first, and the following template blows up with ExpressionChangedAfterItHasBeenCheckedError

@Component({
  selector: 'demo-tabs',
  template: `<ul class="tabs">
    <li *ngFor="let a of links">
      {{a}}
    </li>
  </ul>`})
export class TabsComponent implements OnInit {
  @ContentChildren('a') links: TemplateRef<any>; // corrected. I originally had this as @ViewChildren

  constructor() { }

  ngOnInit() {
  }

}
BLSully
  • 5,929
  • 1
  • 27
  • 43

1 Answers1

4

First of all, those links are not @ViewChildren() for Tabs component - they are @ContentChildren(), because @ViewChildren() must be declared in the component's template, while @ContentChildren() are coming from an outside declaration - just like you did.

To be able to separate content in such a way you'll need to "mark" all elements with very simple custom structural directive (as follows) so that you could get them as list of separate items in your TabsComponent.

link.directive.ts

import {Directive, TemplateRef} from '@angular/core';

@Directive({
    selector: '[appLink]'
})
export class AppLinkDirective {
    constructor(public template: TemplateRef<any>) { }
}

This is a structural directive that can receive HTML template as a DI injection token. This item's template is what we actually need to be rendered into TabsComponent's template.

Then, let's mark our items:

app.component.html

<app-demo-tabs>
    <a *appLink routerLink="/some/link">Tab 1</a>
    <a *appLink routerLink="/some/other/link">Tab 2</a>
    <a *appLink routerLink="/some/third/link">Tab 3</a>
</app-demo-tabs>

And, finally, render them in the component's template:

tabs.component.ts

import {Component, ContentChildren, OnInit, QueryList} from '@angular/core';
import {AppLinkDirective} from './link.directive';

@Component({
    selector: 'app-demo-tabs',
    template: `
        <ul class="tabs">
            <li *ngFor="let link of links">
                <ng-template [ngTemplateOutlet]="link?.template"></ng-template>
            </li>
        </ul>`
})
export class TabsComponent implements OnInit {

    @ContentChildren(AppLinkDirective)
    links: QueryList<AppLinkDirective>;

    constructor() {
    }

    ngOnInit() {
    }

}

And of course you'll need to import this directive in some module so that it could be used in the template.

Alexander Leonov
  • 4,694
  • 1
  • 17
  • 25
  • Some alternative solution https://stackblitz.com/edit/angular-wqjlbw?file=app%2Fapp.module.ts but i like your answer – yurzui Oct 10 '17 at 18:44
  • @yurzui, yes, that's of course possible, and not a bad option at all, but I actually like to put as little stuff in the html as possible - this way it reads more clearly and important things are less hidden under piles of secondary HTML stuff. – Alexander Leonov Oct 10 '17 at 18:51
  • That worked perfectly. I looked at @yurzui's solution too and I see how that works as well, but I agree, this one is clearer (to a noob like myself anyway). Thanks! – BLSully Oct 10 '17 at 20:42