17

This is my component:

import { Component, OnInit, ContentChildren, QueryList } from '@angular/core';
import { IconBoxComponent } from '../icon-box/icon-box.component';

@Component({
  selector: 'app-three-icon-box',
  templateUrl: './three-icon-box.component.html',
  styleUrls: ['./three-icon-box.component.scss']
})
export class ThreeIconBoxComponent implements OnInit {
  @ContentChildren(IconBoxComponent) boxes: QueryList<IconBoxComponent>;

  constructor() { }

  ngOnInit() {
  }

  ngAfterContentInit() {
    console.log(this.boxes);
  }

}

Its template looks like this:

<div class="row justify-content-center">
  <div class="col-12 col-md-10">
    <div class="row justify-content-center text-center">
      <div *ngFor="let box of boxes" class="col-12 col-sm-4 col-lg-3 offset-lg-1 lz-mb-2 lz-mb-sm-0">
        {{ box }}
      </div>
    </div>
  </div>
</div>

This is how I'm rendering it:

  <app-three-icon-box>
    <app-icon-box icon="#icon-one">content 1</app-icon-box>
    <app-icon-box icon="#icon-two">content 2</app-icon-box>
    <app-icon-box icon="#icon-three">content 3</app-icon-box>
  </app-three-icon-box>

In that second block of code, I'm trying to render the <app-icon-box>, but I can't figure out how. {{ box }} was just an idea of what I'm trying to do, but I just get [object Object].

Freestyle09
  • 4,894
  • 8
  • 52
  • 83
user1807782
  • 451
  • 1
  • 4
  • 17

3 Answers3

20

You should use this pattern

  1. Create TemplateMarker Directive to mark which templates you want to pass as parameters (to prevent grabbing other templates).
  2. Inject markers using @ContentChildren.
  3. Render them where you need using NgTemplateOutlet.

Hint: You can render each template multiple times and send them parameters.

Example:

import { Component, Input, Directive, TemplateRef, ContentChildren, QueryList } from '@angular/core';
@Directive({
  selector: '[templateMarker]'
})
export class TemplateMarker {
  constructor(public template: TemplateRef<any>) {}
}
@Component({
  selector: 'template-owner',
  template: `
    <div *ngFor="let marker of markers">
      <i><ng-template [ngTemplateOutlet]="marker.template"></ng-template></i>
    </div>
  `,
})
export class TemplateOwner {
  @ContentChildren(TemplateMarker) markers: QueryList<TemplateMarker>;
}
@Component({
  selector: 'hello',
  template: `<template-owner>
    <div *templateMarker>first template</div>
    <div *templateMarker>second template</div>
  </template-owner>`,
})
export class HelloComponent  {}
Ilia Volk
  • 526
  • 3
  • 9
14

It's a bit unclear what you need. But I guess it would be enough to use ng-content. Remove the ContentChildren and also the ngFor and just use

<ng-content></ng-content>

In your template.

Then you have to add the classes directly where your box components are declared.

<app-icon-box class="col-12 col-sm-4 col-lg-3 offset-lg-1 lz-mb-2 lz-mb-sm-0" icon="#here-to-help-you">

To enrich projected components within the ThreeIconBoxComponent you will need a totally different approach using templates and a template outlet.

David Ibl
  • 901
  • 5
  • 13
  • `` has three elements in it, and I want to wrap each one in some HTML. I don't just want to spit out the HTML directly. – user1807782 Mar 14 '18 at 21:41
  • That's not possible this way. You could style the boxes by components scss with :host::ng-deep. – David Ibl Mar 14 '18 at 21:43
  • I want to keep the original `app-icon-box` components they way they are because they're being used elsewhere in different ways. I'm shocked that Angular doesn't provide a way to just render its child components. – user1807782 Mar 14 '18 at 21:49
  • I meant to style them fro the scss of ThreeIconBoxComponent. This can be done by using :host::ng-deep { app-icon-box { // some styling } } – David Ibl Mar 14 '18 at 21:51
  • @user1807782 What you want is called "Dynamic Content Projection" and is currently not possible, as David Ibl stated. There's an open issue about this: https://github.com/angular/angular/issues/8563 – Kim Kern Mar 14 '18 at 22:26
0

This is an old question but I was just asking this same question in an application I'm developing.

I'm unsure how "correct" my solution is but this is how I tackled this problem.

In my case my parent component is only supposed to accept content of a specific type (in your case this would be the IconBoxComponent).

Within that child component (IconBoxComponent here) I exposed a TemplateRef which can then be used by the parent component for rendering.

Although using a directive also worked I didn't like the idea of having to use both my custom child component as well as a custom directive.

Note: In my example below I removed all of the CSS classes and some HTML elements to make it more readable. They weren't removed for some technical reason.

IconBoxComponent

@Component({
  selector: 'app-icon-box',
  template: `
    <ng-template #template>
       <span>My Icon Box</span>
    </ng-template>
  `,
   styleUrls: ['./icon-box.component.scss']
})
export class IconBoxComponent {
  @ViewChild('template', { static: true }) template!: TemplateRef<any>;
}

ThreeIconBoxComponent


@Component({
  selector: 'app-three-icon-box',
  template: `
     <div>
       <div *ngFor="let box of boxes">
         <ng-container
           [ngTemplateOutlet]="box.template"
         ></ng-container>
       </div>
      </div>
  `,
  styleUrls: ['./three-icon-box.component.scss']
})
export class ThreeIconBoxComponent {
  @ContentChildren(IconBoxComponent) boxes = new QueryList<IconBoxComponent>();
}

Usage

<app-three-icon-box>
  <app-icon-box>content 1</app-icon-box>
  <app-icon-box>content 2</app-icon-box>
  <app-icon-box>content 3</app-icon-box>
</app-three-icon-box>
WBuck
  • 5,162
  • 2
  • 25
  • 36