8

I have a component that uses content projection.

<ng-content select="button">
</ng-content>

I want to do something like this in my component:

@ContentChild('button') button: ElementRef;

However, when I check this.button, it is undefined both in ngAfterViewInit and ngAfterContentInit.

How can I select my content child if it is a native element and not a custom component?

Adrien Brunelat
  • 4,492
  • 4
  • 29
  • 42
Yulian
  • 6,262
  • 10
  • 65
  • 92

2 Answers2

11

None of the query decorators can select native elements without a reference.

@ViewChild('button')
@ViewChildren('button')
@ContentChild('button')
@ContentChildren('button')

It is not a CSS selector.

The query decorators search the dependency graph for a matching type and select those. A matching type can be a template reference, component or directive.

The recommended approach for selecting specific elements in content is to tag it with a directive.

@Directive({...})
export class MyButtonDirective {}

Now you can query this in your parent component.

@ContentChild(MyButtonDirective) button: MyButtonDirective;

When you use a string value it refers to a template variable reference. Those are written as #someValue and they can be a template ref, component or directive.

<button #myButton></button>

You can now reference the above as

@ViewChild('myButton')

I do not know if this works for ContentChild because of the way views work. A #myButton template variable might not be visible in the parent's dependency graph.

Alternatively, you can inject the ElementRef of the parent component and then use native JavaScript to search for the children you're seeking. This is a bad practice from an Angular perspective, because the idea is to separate your application from the DOM.

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • 1
    I was about to post an answer with the template reference variable, and I can confirm that it works with `@ContentChild` if you access it in `ngAfterContentInit`. See [this stackblitz](https://stackblitz.com/edit/angular-fzddd4). – ConnorsFan Aug 03 '18 at 13:54
  • @ConnorsFan cool. It's not something I usually do. If I saw `#myButton` in a template and I didn't see it being used anywhere in that template, then I might remove it. So you should name them something like `#myButtonForParent`. – Reactgular Aug 03 '18 at 13:56
  • @ConnorsFan cool, it really works, but that means that AppComponent should know that it should use a button with a specific template ref of myButton. Which is something that it should not be aware of. Don't you think? – Yulian Aug 03 '18 at 13:59
  • @Yulian I've created generic button directives that have just `button` as their selector. You can then query those and other components aren't aware that it's happening. You just have to be careful no one else is using the same selector. – Reactgular Aug 03 '18 at 14:21
9

The important thing to understand is that the selector you pass to @ContentChild is not a CSS selector, and you cannot find a content child based on its ID, class or element type. You can only search on the identifier name you gave it using the # operator.

So given a component called my-comp and the following usage:

<my-comp><button #buttonName>Click me</button></my_comp>

You can find it using:

@ContentChild('buttonName') button: ElementRef;

Edit

As noted in the OP's comment on ckTag's answer (he beat me to it by a minute), that does mean the calling code will need to know you expect the content to be tagged with a specific name.

If your component does actually need to know detail of its content children, the better answer is actually to provide the directive type to @ContentChild:

@ContentChild(MyButtonComponent, { read: ElementRef })
button: ElementRef;

This is also better, because you can now realistically expect to know the type of content; the calling code can stick #button on a div if it feels like it, after all.

That does mean saying goodbye to using a regular HTML button element, but comes with other benefits. You could, for example, provide a message to guide your calling code, like this:

<ng-content></ng-content>
<div *ngIf="button">Error: Please provide a MyButtonComponent</div>
Bruno Medeiros
  • 2,251
  • 21
  • 34
Cobus Kruger
  • 8,338
  • 3
  • 61
  • 106
  • 2
    @CobusKruger actually, the calling code shouldn't allow anything else except for native button elements, if you have , should it? – Yulian Aug 03 '18 at 14:24
  • @Yulian I missed that you'd used `select`. I still normally use this with my own directives, which means that the calling code doesn't need to know things like "use this template reference name for the button". So requiring the template reference variable with a given name may technically be the answer you asked for, but as you suggested: it's unintuitive. – Cobus Kruger Aug 03 '18 at 14:40