Búsqueda de sitios web

¿Cómo hago referencia a un componente de Angular desde TypeScript cuando puede ser eliminado por ngIf?


Tengo una plantilla que utiliza MatInput de Angular Material. Estoy intentando acceder a este componente desde el código usando @ViewChild para poder alterar programáticamente el estado focused, pero este MatInput no está presente cuando se inicializa la vista, su presencia está determinada por una directiva *ngIf. Básicamente, cuando la vista se carga, hay un elemento p con algo de texto. Si un usuario hace clic en ese elemento, será reemplazado por un input donde el valor inicial es el texto del elemento original. Cuando pierde el foco, se guarda y vuelve a convertirse en un elemento p. El problema es que cuando hacen clic por primera vez en el texto para cambiarlo, el input que se crea no tiene el foco, tienen que hacer clic nuevamente para empezar a editar. Quiero que puedan hacer clic una vez y comenzar a escribir.

Aquí está mi código relevante.

Plantilla:

<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
  }
}

De los documentos:

Puedes usar ViewChild para obtener el primer elemento o la directiva que coincida con el selector desde el DOM de la vista. Si el DOM de la vista cambia, y un nuevo hijo coincide con el selector, la propiedad se actualizará.

¿Está funcionando *ngIf demasiado lento, y estoy intentando acceder a this.input demasiado pronto antes de que se actualice la propiedad? Si es así, ¿cómo puedo esperar hasta que *ngIf termine de reemplazar el DOM y luego acceder al MatInput? ¿O hay alguna otra manera de resolver mi problema de enfoque que simplemente no estoy viendo?

Respuesta #1

Reproduje tu caso en este stackblitz. Después de establecer this.selected=true, Angular tiene que realizar la detección de cambios para mostrar el elemento mat-form-field, y eso normalmente ocurriría después del ciclo de ejecución actual. Una forma de obtener acceso inmediato al elemento de entrada es activar la detección de cambios en tu código, por ejemplo con ChangeDetector.detectChanges (consulta esta respuesta para otras técnicas):

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();
  }  
}

Otra solución alternativa, sugerida por kiranghule27, es retrasar la llamada a this.input.focus() haciéndola asincrónica:

  select(): void {
    this.selected = true;
    setTimeout(() => {
      this.input.focus();
    }, 0);
  }  

Respuesta #2

Puedes usar el decorador @ViewChildren para obtener un QueryList y suscribirte al observable changes para recibir actualizaciones cada vez que el DOM se actualice realmente.

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
  }
}

Artículos relacionados