THE PROBLEM
So I have two Angular components, a parent and a child. The parent passes a custom template to the child component, which then hydrates the template with its own data using ngTemplateOutlet.
This works well for the most part. Unfortunately, I run into issues when trying to access the DOM elements of this parent template from the child.
If I try to access <div #container></div>
from the default child template using @ViewChild('container',{static: false})
, it gets the element without issue. When I do the same using the custom template passed in by app.component, I get the error "cannot read property 'nativeElement' of undefined".
What else do I have to do to access the DOM of my template?
Here's a Stackblitz
App.Component (Parent)
import { Component } from "@angular/core";
@Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {}
<child [customTemplate]="parentTemplate"></child>
<ng-template #parentTemplate let-context="context">
<div #container>HELLO FROM CONTAINER</div>
<button (click)="context.toggleShow()">Toggle Display</button>
<div *ngIf="context.canShow">Container contains the text: {{context.getContainerText()}}</div>
</ng-template>
child.component (Child)
import {
Component,
ElementRef,
Input,
TemplateRef,
ViewChild
} from "@angular/core";
@Component({
selector: "child",
templateUrl: "./child.component.html",
styleUrls: ["./child.component.css"]
})
export class ChildComponent {
@Input() public customTemplate!: TemplateRef<HTMLElement>;
@ViewChild("container", { static: false })
public readonly containerRef!: ElementRef;
templateContext = { context: this };
canShow: boolean = false;
toggleShow() {
this.canShow = !this.canShow;
}
getContainerText() {
return this.containerRef.nativeElement.textContent;
}
}
<ng-container *ngTemplateOutlet="customTemplate || defaultTemplate; context: templateContext">
</ng-container>
<ng-template #defaultTemplate>
<div #container>GOODBYE FROM CONTAINER</div>
<button (click)="toggleShow()">Toggle Display</button>
<div *ngIf="canShow">Container contains the text: {{getContainerText()}}</div>
</ng-template>
MY QUESTION
How do I use @ViewChild to access this div from an outside template that updates with any changes in the DOM? (Note: Removing the *ngIf is NOT an option for this project)
What's causing this? Are there any lifecycle methods that I can use to remedy this issue?
MY HUNCH I'm guessing that ViewChild is being called BEFORE the DOM updates with its new template and I need to setup a listener for DOM changes. I tried this and failed so I'd really appreciate some wisdom on how best to proceed. Thanks in advance :)
EDIT:
This solution needs to properly display <div #container></div>
regardless of whether you're passing in a custom template or using the default one.
ViewChild
doesn't seem to pick up a rendered template - probably because it's not part of the components template initially. It's not a timing or lifecycle issue, it's just never available as aViewChild
An approach that does work is to pass in the template as content to the child component, and access it using
ContentChildren
. You subscribe to theContentChildren
QueryList
forchanges
, which will update when the DOM element becomes renderedYou can then access the
nativeElement
(thediv
). If you wanted you could add listeners here to the DOM element, and triggercd.detectChanges
afterwards, but that would be a bit unusual. It would probably be better to handle DOM changes in the parent element, and pass the required values down to the child using regular@Input
on the child