Zze
Zze

Reputation: 18805

@ViewChild is undefined

I am trying to toggle a @ViewChild element with an *ngIf and then after this, call a native event.

This is my html element, adorned with #audioPlayer so I can extract the element via @ViewChild.

<audio 
    #audioPlayer     
    *ngIf="conversationIsRunning"    
    [src]='activeConversation?.clips[activeConversation.activeClipId].audio.blob'
    (ended)="audioComplete($event)" 
    autoplay controls preload="auto" >
</audio>

In my typescript I have the following:

@ViewChild('audioPlayer') audioPlayer;
private conversationIsRunning: boolean;

ngOnInit() {
    this.conversationIsRunning = false;
}

ngOnChanges() {
    console.log("ngOnChanges");
    this.conversationIsRunning = true;       
    console.log(this.audioPlayer); // undefined
    this.audioPlayer.nativeElement.play(); // error
}

The error disappears if I remove the *ngIf from the audio element. However I really want this functionality in place where the element is destroyed when I don't need it.

I saw in this answer that you can use a setter on @ViewChild so I implemented that, however to no success...

private privAudioPlayer: ViewContainerRef;
@ViewChild('audioPlayer') set audioPlayer(audioPlayer: ViewContainerRef) {
    this.privAudioPlayer = audioPlayer;
    console.log('audioPlayer set called >> ', audioPlayer, this.privAudioPlayer);
};

...however this always outputs audioPlayer set called >> undefined undefined

I have also tried splitting the this.conversationIsRunning = true; from its current location and putting into a variety of different ng lifecycle hooks, and then also changing ngOnChanges to other lifecycle hooks as well to no avail.

Do I have to wait until next frame or something? And why does that set audioPlayer mutator recieve undefined?

Upvotes: 0

Views: 2800

Answers (2)

Matt Langley
Matt Langley

Reputation: 23

The problem is that as a dynamic element, the element doesn't exist when the component is initialised, so the view child is created with an undefined ElementRef.

There are a few solutions.

If you running Angular 8, you can simply tell it that the element is not static and when it exists the view child will receive the ElementRef:

@ViewChild('audioPlayer', { static: false }) audioPlayer: ElementRef;

Prior to Angular 8, the best way is to place the conditional element in a child component, and pass any required data in as a input parameters:

<app-audio *ngIf="conversationIsRunning"> </app-audio>

with the the child component having a template like:

<audio #audioPlayer [src]='activeConversation?.clips[activeConversation.activeClipId].audio.blob'
(ended)="audioComplete($event)" 
autoplay controls preload="auto" >
</audio>

Your event handler would be in the child's code behind, and you could expose a method to be called to play in response to ngChanges in the parent component. You could also publish events if you need to communicate back up to the parent. The element always exists in the child component so the ElementRef will always be valid.

And finally, you could also just toggle the style display property instead. The component always exists in the DOM and the view child will always have a valid ElementRef. Obviously it always uses resources and contributes to load overhead, where you ever display it or not:

<audio 
#audioPlayer     
[style.display]="conversationIsRunningDisplay()"   [src]='activeConversation?.clips[activeConversation.activeClipId].audio.blob'
(ended)="audioComplete($event)" 
autoplay controls preload="auto" >
</audio>

With your code behind being something like:

@ViewChild('audioPlayer') audioPlayer;
private conversationIsRunning: boolean;

ngOnInit() {
    this.conversationIsRunning = false;
}

ngOnChanges() {
    console.log("ngOnChanges");
    this.conversationIsRunning = true;       
    console.log(this.audioPlayer); // undefined
    this.audioPlayer.nativeElement.play(); // error
}

public conversationIsRunningDisplay() {
    if (this.conversationIsRunning) {
       return 'block';
    }
    return 'none';
}

Upvotes: 1

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657108

In the first code example, you should use ngAfterViewInit() instead of ngOnChanges().

Upvotes: 1

Related Questions