Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I reference an Angular component from typescript when it may be removed by ngIf?

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?

like image 613
Seth Avatar asked Jan 03 '18 18:01

Seth


People also ask

Does ngIf destroy component?

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.

Does ngIf remove element from DOM?

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.

Can't bind to ngIf since it isn't a known property of TR?

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.

What is the difference between @component and @directive in Angular?

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.


3 Answers

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);
  }  
like image 178
ConnorsFan Avatar answered Sep 29 '22 07:09

ConnorsFan


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();
});
like image 22
kiranghule27 Avatar answered Sep 29 '22 07:09

kiranghule27


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
  }
}
like image 39
Steveadoo Avatar answered Sep 29 '22 07:09

Steveadoo