7

So, let's say I have a component, ExampleComponent, that builds a QueryList of SomeOtherComponent components in its content view.

import {Component, ContentChildren, QueryList} from '@angular/core'
import {SomeOtherComponent}                    from '../some-other-component/some-other-component.component'

@Component({
    selector   : 'app-example',
    templateUrl: './example.component.html',
    styleUrls  : ['./example.component.css']
})
export class ExampleComponent {
    @ContentChildren(SomeOtherComponent)
    someOtherComponents: QueryList<SomeOtherComponent>
}

In its template, I have an NgFor that should insert a <hr /> element after each one.

<ng-container *ngFor="let component of someOtherComponents">
    <!-- put component here -->
    <hr />
</ng-container>

So that, for example, this (in some other component):

<app-example>
    <app-some-other-component>blablabla</app-some-other-component>
    <app-some-other-component>hello world</app-some-other-component>
    <app-some-other-component>testing</app-some-other-component>
</app-example>

Would result in this (in the HTML):

<app-example>
    <app-some-other-component>blablabla</app-some-other-component>
    <hr />
    <app-some-other-component>hello world</app-some-other-component>
    <hr />
    <app-some-other-component>testing</app-some-other-component>
    <hr />
</app-example>

However, this is where the problem shows up. How do I insert that SomeOtherComponent instance? ng-content's select attribute doesn't support component instances, CDK portals don't like me, I don't want to create some complicated solution using templates and require the user to wrap all of their children in templates... What do I do?

To clarify: I do NOT want to create SomeOtherComponent instances. I want to do something similar to <ng-content select="app-some-other-component">, but instead insert a specific INSTANCE (like one inside the QueryList returned by ContentChildren). I also don't want to use a directive/template (like putting *thisDirectiveIMadeForJustOneComponentWhichMakesItRequireBeingPlacedInItsOwnModule on all children).

Note: There are other ways to insert horizontal rules after components, and feel free to mention those, just make sure to answer the question too. This is just an example.

  • Please feel free to edit the title to make it more clear, this is my third version and I'm still not sure if this is good enough. –  Jun 13 '19 at 10:22
  • You can use a directive for this, you have to add the directive name to each component though, see [stackblitz example](https://stackblitz.com/edit/angular-ek8soc). – Munim Munna Jun 16 '19 at 19:52

2 Answers2

7

your question is "how do I insert specific component instances?" but what i understand from your explanation is that, you want to add lines just under the already inserted component instances via ng-content. Because you already have a QueryList of elements returned by ContentChildren.

From this point on we need to understand one important thing about ViewContainerRef;

  1. ViewContainerRef section of this article

What’s interesting is that Angular doesn’t insert views inside the element, but appends them after the element bound to ViewContainer.

So if we can access to ViewContainerRef's of elements in our QueryList we can easily append new elements to those elements. And we can access ViewContainerRef s of elements by using read metadata property of ContentChildren query;

@ContentChildren(SomeOtherComponent, { descendants: true, read: ViewContainerRef }) someOtherComponents: QueryList<ViewContainerRef>;

since we have ViewContainerRefs of our elements we can easily append new elements to these by using createEmbeddedView()

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.css']
})
export class ExampleComponent implements AfterContentInit {
  @ViewChild("templateToAppend", {static: true}) templateToAppend: TemplateRef<any>;
  @ContentChildren(SomeOtherComponent, { descendants: true, read: ViewContainerRef }) someOtherComponents: QueryList<ViewContainerRef>;

  ngAfterContentInit() {
    this.someOtherComponents.forEach(ap => ap.createEmbeddedView(this.templateToAppend));
  }
}

template

<ng-content></ng-content>

<ng-template #templateToAppend>
    <hr style="color: blue"/>
</ng-template>

demo here

by utilizing this approach to create a directive in order to achive your requirement of having something similar to <ng-content select="app-some-other-component">

we can create a directive that takes a TemplateRef as @Input() and appends it to the ViewContainerRef

export class CustomAppendable { }

@Directive({
  selector: '[appMyCustomAppender]'
})
export class MyCustomAppenderDirective {
  @ContentChildren(CustomAppendable, { descendants: true, read: ViewContainerRef }) appendables: QueryList<ViewContainerRef>;
  @Input() appMyCustomAppender: TemplateRef<any>;

  constructor() { }

  ngAfterContentInit() {
    setTimeout(() => {
      this.appendables.forEach(ap => ap.createEmbeddedView(this.appMyCustomAppender));
    });
  }
}

with this approach, in order not to create tight coupling between our SomeOtherComponent and our directive we make our components somehow generic by creating a common type CustomAppendable and use it as an alias for the components that we want to query in ContentChildren

NOTE: i couldn't find a way to make ContentChildren query work with template selectors. As explained here we can use ContentChildren with template reference variables or Component Types. that's why i created the alias.

@Component({
  selector: 'app-some-other-component',
  templateUrl: './some-other-component.component.html',
  styleUrls: ['./some-other-component.component.css'],
  providers: [{ provide: CustomAppendable, useExisting: SomeOtherComponent }]
})
export class SomeOtherComponent implements OnInit {

  constructor() { }

  ngOnInit() {}

}

also with this approach we don't need the container component and apply our directive any element.

<div [appMyCustomAppender]="templateToAppend">
  <app-some-other-component>underlined</app-some-other-component>
  <app-some-other-component>underlined</app-some-other-component>
  <app-some-other-component2>not underlined</app-some-other-component2>
  <br />
  <app-some-other-component2>not underlined</app-some-other-component2>
</div>
<br />
<app-some-other-component>but not underlined!</app-some-other-component>

<ng-template #templateToAppend>
  <hr  style="color: red"/>
  <br />
</ng-template>

demo here

i am hoping that i was able to understand your requirements correctly and all these are helpful somehow :)

ysf
  • 4,634
  • 3
  • 27
  • 29
-1

From my understanding of your question it sounds like you want to dynamically insert components. This can be done by pushing the amount of SomeOtherComponent's you want into an array then inside your ngFor you create the someOtherComponent like so:

import {Component, ContentChildren, QueryList} from '@angular/core'
import {SomeOtherComponent}                    from '../some-other-component/some-other-component.component'

@Component({
    selector   : 'app-example',
    templateUrl: './example.component.html',
    styleUrls  : ['./example.component.css']
})
export class ExampleComponent {
    someOtherComponents: any[];
    constructor(){
    this.someOtherComponents.push({data: "insert_stuff_here"});
    this.someOtherComponents.push({data: "insert_stuff_here"})
    }
}

Then in your ngFor

 <ng-container *ngFor="let component of someOtherComponents">
    <someOtherComponent [possibleInputHere]="component.data"></someOtherComponent>
    <hr />
</ng-container>

I hope this is what you're looking for!

Marcus Cantu
  • 463
  • 3
  • 14
  • I tihnk the idea was to be able to have different kinds of components inside the array oO – jonathan Heindl Jun 16 '19 at 18:41
  • I clarified in the question, here's the part I added: To clarify: I do NOT want to create `SomeOtherComponent` instances. I want to do something similar to ``, but instead insert a specific INSTANCE (like one inside the [`QueryList`](https://angular.io/api/core/QueryList) returned by [`ContentChildren`](https://angular.io/api/core/ContentChildren)). –  Jun 16 '19 at 22:22