¿Cómo detectar cuándo un valor @Input () cambia en Angular?

471

Tengo un componente principal ( CategoryComponent ), un componente secundario ( videoListComponent ) y un ApiService.

Tengo la mayor parte de esto funcionando bien, es decir, cada componente puede acceder a la API json y obtener sus datos relevantes a través de observables.

Actualmente, el componente de la lista de videos solo obtiene todos los videos, me gustaría filtrar esto solo a los videos en una categoría en particular, lo logré pasando el Id de categoría al niño a través de @Input().

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

Esto funciona y cuando la categoría principal de CategoryComponent cambia, el valor de categoryId se transfiere a través de, @Input()pero luego necesito detectar esto en VideoListComponent y volver a solicitar la matriz de videos a través de APIService (con el nuevo categoryId).

En AngularJS habría hecho un $watchen la variable. ¿Cuál es la mejor manera de manejar esto?

Jon Catmull
fuente

Respuestas:

720

En realidad, hay dos formas de detectar y actuar cuando una entrada cambia en el componente secundario en angular2 +:

  1. Puede usar el método de ciclo de vida ngOnChanges () como también se menciona en respuestas anteriores:
    @Input() categoryId: string;

    ngOnChanges(changes: SimpleChanges) {

        this.doSomething(changes.categoryId.currentValue);
        // You can also use categoryId.previousValue and 
        // categoryId.firstChange for comparing old and new values

    }

Enlaces de documentación: ngOnChanges, SimpleChanges, SimpleChange
Demo Ejemplo: Mira este plunker

  1. Alternativamente, también puede usar un configurador de propiedades de entrada de la siguiente manera:
    private _categoryId: string;

    @Input() set categoryId(value: string) {

       this._categoryId = value;
       this.doSomething(this._categoryId);

    }

    get categoryId(): string {

        return this._categoryId;

    }

Enlace de documentación: mira aquí .

Ejemplo de demostración: Mira este plunker .

¿QUÉ ENFOQUE DEBE USAR?

Si su componente tiene varias entradas, entonces, si usa ngOnChanges (), obtendrá todos los cambios para todas las entradas a la vez dentro de ngOnChanges (). Con este enfoque, también puede comparar los valores actuales y anteriores de la entrada que ha cambiado y tomar las medidas correspondientes.

Sin embargo, si desea hacer algo cuando solo cambia una entrada en particular (y no le importan las otras entradas), entonces podría ser más simple usar un configurador de propiedades de entrada. Sin embargo, este enfoque no proporciona una forma integrada de comparar los valores anteriores y actuales de la entrada modificada (que puede hacer fácilmente con el método del ciclo de vida ngOnChanges).

EDITAR 2017-07-25: LA DETECCIÓN DE CAMBIO ANGULAR PUEDE TODAVÍA NO FUERA DE FUEGO BAJO ALGUNAS CIRCUNSTANCIAS

Normalmente, la detección de cambios para setter y ngOnChanges se activará siempre que el componente principal cambie los datos que pasa al hijo, siempre que los datos sean un tipo de datos primitivo JS (cadena, número, booleano) . Sin embargo, en los siguientes escenarios, no se disparará y tendrá que tomar medidas adicionales para que funcione.

  1. Si está utilizando un objeto anidado o una matriz (en lugar de un tipo de datos primitivo JS) para pasar datos de padre a hijo, la detección de cambios (usando setter o ngchanges) podría no activarse, como también se menciona en la respuesta del usuario: muetzerich. Para soluciones mira aquí .

  2. Si está mutando datos fuera del contexto angular (es decir, externamente), entonces angular no sabrá los cambios. Puede que tenga que usar ChangeDetectorRef o NgZone en su componente para hacer que Angular tenga conocimiento de los cambios externos y, por lo tanto, desencadenar la detección de cambios. Consulte esto .

Alan CS
fuente
8
Muy buen punto con respecto al uso de setters con entradas, actualizaré esta para que sea la respuesta aceptada, ya que es más completa. Gracias.
Jon Catmull
2
@trichetriche, se llamará al setter (método set) cada vez que el padre cambie la entrada. Además, lo mismo es cierto para ngOnChanges () también.
Alan CS
Bueno, aparentemente no ... Trataré de resolver esto, probablemente sea algo malo que hice.
1
Acabo de tropezar con esta pregunta, noté que dijiste que usar el setter no permitirá comparar valores, sin embargo, eso no es cierto, puedes llamar a tu doSomethingmétodo y tomar 2 argumentos los valores nuevos y viejos antes de establecer el nuevo valor, de otra manera estaría almacenando el valor anterior antes de configurar y llamar al método después de eso.
T.Aoukar
1
@ T.Aoukar Lo que digo es que el configurador de entrada no admite de forma nativa la comparación de valores antiguos / nuevos como ngOnChanges (). Siempre puede usar hacks para almacenar el valor anterior (como usted menciona) para compararlo con el nuevo valor.
Alan CS
105

Use el ngOnChanges()método del ciclo de vida en su componente.

ngOnChanges se llama justo después de que se hayan verificado las propiedades enlazadas a datos y antes de que se verifiquen los elementos secundarios de vista y contenido si al menos uno de ellos ha cambiado.

Aquí están los documentos .

Muetzerich
fuente
66
Esto funciona cuando una variable se establece en un nuevo valor MyComponent.myArray = [], por ejemplo , pero si se altera un valor de entrada MyComponent.myArray.push(newValue), por ejemplo , ngOnChanges()no se activa. ¿Alguna idea sobre cómo capturar este cambio?
Levi Fuller
44
ngOnChanges()no se llama cuando un objeto anidado ha cambiado. Tal vez encuentre una solución aquí
muetzerich
33

Recibía errores en la consola, así como en el compilador e IDE cuando usaba el SimpleChangestipo en la firma de la función. Para evitar los errores, utilice la anypalabra clave en la firma en su lugar.

ngOnChanges(changes: any) {
    console.log(changes.myInput.currentValue);
}

EDITAR:

Como Jon señaló a continuación, puede usar la SimpleChangesfirma al usar la notación de corchetes en lugar de la notación de puntos.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Darcy
fuente
1
¿Importó la SimpleChangesinterfaz en la parte superior de su archivo?
Jon Catmull
@ Jon - Sí. El problema no era la firma en sí, sino el acceso a los parámetros asignados en tiempo de ejecución al objeto SimpleChanges. Por ejemplo, en mi componente, uní mi clase de Usuario a la entrada (es decir <my-user-details [user]="selectedUser"></my-user-details>). Se accede a esta propiedad desde la clase SimpleChanges llamando changes.user.currentValue. El aviso userestá vinculado en tiempo de ejecución y no forma parte del objeto SimpleChanges
Darcy
1
¿Has probado esto sin embargo? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Jon Catmull
@ Jon - Cambiar a la notación de paréntesis es el truco. Actualicé mi respuesta para incluir eso como una alternativa.
Darcy
1
importar {SimpleChanges} desde '@ angular / core'; Si está en los documentos angulares, y no en un tipo de mecanografiado nativo, entonces probablemente sea de un módulo angular, muy probablemente 'angular / núcleo'
BentOnCoding
13

La apuesta más segura es ir con un servicio compartido en lugar de un @Inputparámetro. Además, el @Inputparámetro no detecta cambios en el tipo de objeto anidado complejo.

Un servicio de ejemplo simple es el siguiente:

Service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SyncService {

    private thread_id = new Subject<number>();
    thread_id$ = this.thread_id.asObservable();

    set_thread_id(thread_id: number) {
        this.thread_id.next(thread_id);
    }

}

Component.ts

export class ConsumerComponent implements OnInit {

  constructor(
    public sync: SyncService
  ) {
     this.sync.thread_id$.subscribe(thread_id => {
          **Process Value Updates Here**
      }
    }

  selectChat(thread_id: number) {  <--- How to update values
    this.sync.set_thread_id(thread_id);
  }
}

Puede usar una implementación similar en otros componentes y todos sus componentes compartirán los mismos valores compartidos.

Sanket Berde
fuente
1
Gracias Sanket. Este es un muy buen punto para hacer y, cuando sea apropiado, una mejor manera de hacerlo. Dejaré la respuesta aceptada tal como está, ya que responde mis preguntas específicas sobre la detección de cambios en @Input.
Jon Catmull
1
Un parámetro @Input no detecta cambios dentro de un tipo de objeto anidado complejo, pero si el objeto cambia, se disparará, ¿verdad? por ejemplo, tengo el parámetro de entrada "parent", por lo que si cambio el parent en su conjunto, se activará la detección de cambio, pero si cambio parent.name, ¿no?
yogibimbi
Debe proporcionar una razón por la cual su solución es más segura
Joshua Kemmerer
1
El @Inputparámetro @JoshuaKemmerer no detecta cambios en el tipo de objeto anidado complejo, pero sí en objetos nativos simples.
Sanket Berde
6

Solo quiero agregar que hay otro enlace de Lifecycle llamado DoCheckque es útil si el @Inputvalor no es un valor primitivo.

Tengo una matriz como, Inputpor lo que esto no dispara el OnChangesevento cuando el contenido cambia (porque la comprobación de que Angular lo hace es 'simple' y no profunda, por lo que la matriz sigue siendo una matriz, a pesar de que el contenido de la matriz ha cambiado).

Luego implemento un código de verificación personalizado para decidir si quiero actualizar mi vista con la matriz modificada.

Stevo
fuente
6
  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

Intenta usar este método. Espero que esto ayude

Akshay Rajput
fuente
4

También puede tener un observable que desencadena los cambios en el componente principal (CategoryComponent) y hacer lo que desea hacer en la suscripción en el componente secundario. (videoListComponent)

service.ts 
public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

-----------------
CategoryComponent.ts
public onCategoryChange(): void {
  service.categoryChange$.next();
}

-----------------
videoListComponent.ts
public ngOnInit(): void {
  service.categoryChange$.subscribe(() => {
   // do your logic
});
}
Josf
fuente
2

Aquí ngOnChanges se activará siempre cuando cambie la propiedad de entrada:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}
Chirag
fuente
1

Esta solución utiliza una clase proxy y ofrece las siguientes ventajas:

  • Permite al consumidor aprovechar el poder de RXJS
  • Más compacto que otras soluciones propuestas hasta ahora.
  • Más seguro de escribir que usarngOnChanges()

Ejemplo de uso:

@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');

Función de utilidad:

export function observeProperty<T, K extends keyof T>(target: T, key: K) {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get(): T[K] { return subject.getValue(); },
    set(newValue: T[K]): void {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
}
Stephen Paul
fuente
0

Si no desea utilizar el método ngOnChange implement og onChange (), también puede suscribirse a los cambios de un elemento específico por el evento valueChanges , ETC.

 myForm= new FormGroup({
first: new FormControl()   

});

this.myForm.valueChanges.subscribe(formValue => {
  this.changeDetector.markForCheck();
});

el markForCheck () escrito debido al uso en esta declaración:

  changeDetection: ChangeDetectionStrategy.OnPush
usuario1012506
fuente
3
Esto es completamente diferente de la pregunta y, aunque las personas útiles deben ser conscientes de que su código se trata de un tipo diferente de cambio. La pregunta se hizo sobre el cambio de datos desde FUERA de un componente y luego hacer que su componente sepa y responda al hecho de que los datos han cambiado. Su respuesta está hablando de detectar cambios dentro del componente mismo en elementos como entradas en un formulario que son LOCALES para el componente.
rmcsharry
0

También podría pasar un EventEmitter como entrada. No estoy seguro si esta es la mejor práctica aunque ...

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

Y en VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Cédric Schweizer
fuente
Proporcione enlaces útiles, fragmentos de código para respaldar su solución.
mishsx
Agregué un ejemplo.
Cédric Schweizer