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 the focused
state, but this MatInput
is not there when the view initializes - its presence is determined by an *ngIf
directive. Basically, when the view loads, there is a p
element with some text. If a user clicks that element, it will be replaced with an input
where the starting value is the text of the original element. When it loses focus, it is saved and reverts back to a p
element. The problem is that when they first click on the text to change it, the input
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 access this.input
too soon before the property is updated? If so, how can I wait until *ngIf
is done replacing the DOM and then access the MatInput
? Or is there some other way to solve my focusing problem altogether that I'm just not seeing?
Angular's ngIf directive does not simply hide and show. It creates and destroys an HTML element based on the result of a JavaScript expression.
The ngIf directive removes or recreates a portion of the DOM tree based on an {expression}. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.
Solution for Angular Can't bind to 'ngIf' since it isn't a known property of 'div' There are multiple ways to fix this. Incorrect ngIf syntax One way, This is due to an error caused due to misspelling capital 'I' for div. To solve this, Import CommonModule or BroswerModule or both into app.
Component is used to break up the application into smaller components. But Directive is used to design re-usable components, which is more behavior-oriented. That is why components are widely used in later versions of Angular to make things easy and build a total component-based model.
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);
}
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();
});
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With