How do I reference an Angular component from typescript when it may be removed by ngIf?
Solution 1
I reproduced your case in this stackblitz. After setting this.selected = true
, Angular has to perform change detection to display the mat-form-field
element, and that would normally happen after the current execution cycle. One way to get immediate access to the input element is to trigger change detection in your code, for example with ChangeDetector.detectChanges
(see this answer for other techniques):
import { Component, ChangeDetectorRef, ViewChild } from '@angular/core';
import { MatInput } from '@angular/material';
@Component({
...
})
export class FormFieldPrefixSuffixExample {
@ViewChild(MatInput) input: MatInput;
text = "Hello world!"
selected = false;
constructor(private changeDetector: ChangeDetectorRef) {
}
select(): void {
this.selected = true;
this.changeDetector.detectChanges();
this.input.focus();
}
}
Another workaround, suggested by kiranghule27, is to delay the call to this.input.focus()
by making it asynchronous:
select(): void {
this.selected = true;
setTimeout(() => {
this.input.focus();
}, 0);
}
Solution 2
You can use the @ViewChildren
decorator to get a QueryList
and subscribe to the changes
observable to get updates whenever the DOM is actually updated.
import { Component, Input, Output, EventEmitter, ViewChildren } from '@angular/core';
import { MatInput } from '@angular/material';
import { AfterViewInit } from '@angular/core/src/metadata/lifecycle_hooks';
@Component({
selector: 'app-click-to-input',
templateUrl: './click-to-input.component.html',
styleUrls: ['./click-to-input.component.scss']
})
export class ClickToInputComponent implements AfterViewInit {
@Input() text: string;
@Output() saved = new EventEmitter<string>();
@ViewChildren(MatInput) input: QueryList<MatInput>;
selected = false;
ngAfterViewInit(): void {
// Will get called everytime the input gets added/removed.
this.input.changes.subscribe((list: any) => {
if (!this.selected || list.first == null)
return;
console.log(list.first);
list.first.focus();
});
}
save(): void {
this.saved.emit(this.text);
this.selected = false;
}
select(): void {
this.selected = true; // ngIf should now add the input to the template
}
}
Solution 3
Are you sure the selector is matching the same input?
Another way you can do is declare input as a template variable like this
<input matInput #myInput type="text" [(ngModel)]="text" (blur)="save()">
and access it in your component as
@ViewChild('#myInput') input: MatInput
Or may be use setTimeout()
<input matInput id="myInput" type="text" [(ngModel)]="text" (blur)="save()">
and
setTimeout(() => {
document.getElementById('myInput').focus();
});
Seth
I code for a living. I enjoy reading and learning new things all the time. I am currently working on a personal project, creating a video game. Many of my questions will probably relate back to my game world, either the story of it or the technical aspects of its creation. I'll post the game here when I'm done...but it may take awhile!
Updated on June 08, 2022Comments
-
Seth almost 2 years
I have a template which uses
MatInput
from Angular Material. I am trying to access this component from code using@ViewChild
so I can programmatically alter thefocused
state, but thisMatInput
is not there when the view initializes - its presence is determined by an*ngIf
directive. Basically, when the view loads, there is ap
element with some text. If a user clicks that element, it will be replaced with aninput
where the starting value is the text of the original element. When it loses focus, it is saved and reverts back to ap
element. The problem is that when they first click on the text to change it, theinput
that's created does not have focus - they have to click it again to start editing. I want them to be able to click once and start typing.Here's my relevant code.
Template:
<mat-form-field *ngIf="selected; else staticText" class="full-width"> <input matInput type="text" [(ngModel)]="text" (blur)="save()"> </mat-form-field> <ng-template #staticText> <p class="selectable" (click)="select()">{{ text }}</p> </ng-template>
Typescript:
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; import { MatInput } from '@angular/material'; import { AfterViewInit } from '@angular/core/src/metadata/lifecycle_hooks'; @Component({ selector: 'app-click-to-input', templateUrl: './click-to-input.component.html', styleUrls: ['./click-to-input.component.scss'] }) export class ClickToInputComponent implements AfterViewInit { @Input() text: string; @Output() saved = new EventEmitter<string>(); @ViewChild(MatInput) input: MatInput; selected = false; ngAfterViewInit(): void { console.log(this.input); // shows undefined - no elements match the ViewChild selector at this point } save(): void { this.saved.emit(this.text); this.selected = false; } select(): void { this.selected = true; // ngIf should now add the input to the template this.input.focus(); // but input is still undefined } }
From the docs:
You can use ViewChild to get the first element or the directive matching the selector from the view DOM. If the view DOM changes, and a new child matches the selector, the property will be updated.
Is
*ngIf
working too slow, and I'm trying to accessthis.input
too soon before the property is updated? If so, how can I wait until*ngIf
is done replacing the DOM and then access theMatInput
? Or is there some other way to solve my focusing problem altogether that I'm just not seeing?