Come rilevare quando un valore @Input () cambia in Angolare?


471

Ho un componente padre ( CategoryComponent ), un componente figlio ( videoListComponent ) e un ApiService.

Ho la maggior parte di questo lavoro bene, cioè ogni componente può accedere a API JSON e ottenere i suoi dati rilevanti tramite osservabili.

Attualmente il componente dell'elenco dei video ottiene solo tutti i video, vorrei filtrare solo i video in una particolare categoria, ho raggiunto questo obiettivo passando la categoriaId al bambino tramite @Input().

CategoryComponent.html

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

Funziona e quando la categoria @Input()ParentComponent cambia, quindi il valore di CategoryId viene passato attraverso ma devo quindi rilevarlo in VideoListComponent e richiedere nuovamente l'array video tramite APIService (con il nuovoId category).

In AngularJS avrei fatto uno $watchsulla variabile. Qual è il modo migliore per gestirlo?




per le modifiche di matrice: stackoverflow.com/questions/42962394/...
dasfdsa

Risposte:


720

In realtà, ci sono due modi per rilevare e agire quando un input cambia nella componente figlio in angular2 +:

  1. È possibile utilizzare il metodo del ciclo di vita ngOnChanges () come indicato anche nelle risposte precedenti:
    @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

    }

Link alla documentazione: ngOnChanges, SimpleChanges, SimpleChange
Demo Esempio: guarda questo plunker

  1. In alternativa, è anche possibile utilizzare un setter proprietà input come segue:
    private _categoryId: string;

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

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

    }

    get categoryId(): string {

        return this._categoryId;

    }

Link alla documentazione: guarda qui .

Esempio dimostrativo: guarda questo plunker .

QUALE APPROCCIO DOVETE USARE?

Se il componente ha diversi input, quindi, se si utilizza ngOnChanges (), si otterranno tutte le modifiche per tutti gli input contemporaneamente in ngOnChanges (). Usando questo approccio, puoi anche confrontare i valori attuali e precedenti dell'input che è cambiato e intraprendere le azioni di conseguenza.

Tuttavia, se vuoi fare qualcosa quando cambia solo un singolo input (e non ti preoccupi degli altri input), potrebbe essere più semplice usare un setter proprietà input. Tuttavia, questo approccio non fornisce un modo integrato per confrontare i valori precedenti e attuali dell'input modificato (cosa che puoi fare facilmente con il metodo del ciclo di vita di ngOnChanges).

MODIFICA 25/07/2017: LA RILEVAZIONE DEL CAMBIAMENTO ANGOLARE PU ST ANCORA NON INCENDI IN ALCUNE CIRCOSTANZE

Normalmente, il rilevamento delle modifiche sia per setter che per ngOnChanges viene attivato ogni volta che il componente padre modifica i dati che passa al figlio, a condizione che i dati siano un tipo di dati primitivo JS (stringa, numero, valore booleano) . Tuttavia, nei seguenti scenari, non si attiverà e devi fare ulteriori azioni per farlo funzionare.

  1. Se si utilizza un oggetto nidificato o un array (anziché un tipo di dati primitivo JS) per passare i dati da padre a figlio, il rilevamento delle modifiche (utilizzando setter o ngchanges) potrebbe non attivarsi, come indicato anche nella risposta dell'utente: muetzerich. Per soluzioni guarda qui .

  2. Se stai mutando i dati al di fuori del contesto angolare (cioè esternamente), allora l'angolare non conoscerà i cambiamenti. Potrebbe essere necessario utilizzare ChangeDetectorRef o NgZone nel componente per rendere gli angolari consapevoli delle modifiche esterne e quindi attivare il rilevamento delle modifiche. Fare riferimento a questo .


8
Ottimo punto sull'uso dei setter con input, lo aggiornerò per essere la risposta accettata in quanto più completa. Grazie.
Jon Catmull,

2
@trichetriche, il setter (metodo set) verrà chiamato ogni volta che il genitore modifica l'input. Lo stesso vale anche per ngOnChanges ().
Alan CS,

Beh, apparentemente no ... proverò a capirlo, probabilmente ho fatto qualcosa di sbagliato.

1
Mi sono appena imbattuto in questa domanda, ho notato che hai detto che l'uso del setter non permetterà di confrontare i valori, tuttavia non è vero, puoi chiamare il tuo doSomethingmetodo e prendere 2 argomenti i valori vecchi e nuovi prima di impostare effettivamente il nuovo valore, in un altro modo sarebbe memorizzare il vecchio valore prima di impostare e chiamare il metodo dopo quello.
T.Aoukar,

1
@ T.Aoukar Quello che sto dicendo è che il setter di input non supporta nativamente il confronto di valori vecchi / nuovi come ngOnChanges (). Puoi sempre usare gli hack per archiviare il vecchio valore (come dici tu) per confrontarlo con il nuovo valore.
Alan CS,

105

Utilizzare il ngOnChanges()metodo del ciclo di vita nel componente.

ngOnChanges viene chiamato subito dopo il controllo delle proprietà associate ai dati e prima che i figli della vista e del contenuto vengano controllati se almeno uno di essi è stato modificato.

Ecco i documenti .


6
Funziona quando una variabile è impostata su un nuovo valore MyComponent.myArray = [], ad esempio , ma se un valore di input viene modificato MyComponent.myArray.push(newValue), ad esempio , ngOnChanges()non viene attivato. Qualche idea su come catturare questo cambiamento?
Levi Fuller,

4
ngOnChanges()non viene chiamato quando un oggetto nidificato è cambiato. Forse trovi una soluzione qui
muetzerich l'

33

Stavo ricevendo errori nella console, nel compilatore e nell'IDE durante l'utilizzo del SimpleChangestipo nella firma della funzione. Per evitare gli errori, utilizzare la anyparola chiave nella firma, invece.

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

MODIFICARE:

Come Jon ha sottolineato di seguito, puoi usare la SimpleChangesfirma quando usi la notazione tra parentesi anziché la notazione con punti.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}

1
Hai importato l' SimpleChangesinterfaccia nella parte superiore del tuo file?
Jon Catmull,

@Jon - Sì. Il problema non era la firma stessa, ma l'accesso ai parametri assegnati in fase di esecuzione all'oggetto SimpleChanges. Ad esempio, nel mio componente ho associato la mia classe User all'input (es<my-user-details [user]="selectedUser"></my-user-details> ). È possibile accedere a questa proprietà dalla classe SimpleChanges chiamando changes.user.currentValue. L'avviso userè vincolato in fase di esecuzione e non fa parte dell'oggetto SimpleChanges
Darcy

1
Hai provato questo però? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Jon Catmull

@Jon - Il passaggio alla notazione parentesi fa il trucco. Ho aggiornato la mia risposta per includerla come alternativa.
Darcy,

1
importare {SimpleChanges} da '@ angular / core'; Se è nei documenti angolari e non in un tipo dattiloscritto nativo, probabilmente proviene da un modulo angolare, molto probabilmente "angolare / core"
BentOnCoding

13

La scommessa più sicura è quella di andare con un servizio condiviso invece di un @Inputparametro. Inoltre, il @Inputparametro non rileva le modifiche nel tipo di oggetto nidificato complesso.

Un semplice servizio di esempio è il seguente:

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

È possibile utilizzare un'implementazione simile in altri componenti e tutti i componenti condivideranno gli stessi valori condivisi.


1
Grazie Sanket. Questo è un ottimo punto da fare e, se del caso, un modo migliore per farlo. Lascerò la risposta accettata così com'è in quanto risponde alle mie domande specifiche relative al rilevamento delle modifiche di @Input.
Jon Catmull,

1
Un parametro @Input non rileva le modifiche all'interno di un tipo di oggetto nidificato complesso, ma se l'oggetto stesso cambia, si attiverà, giusto? ad es. ho il parametro di input "parent", quindi se cambio parent nel suo insieme, il rilevamento delle modifiche si attiverà, ma se cambio parent.name, non lo farà?
Yogibimbi,

Dovresti fornire un motivo per cui la tua soluzione è più sicura
Joshua Kemmerer,

1
Il @Inputparametro @JoshuaKemmerer non rileva le modifiche nel tipo di oggetto nidificato complesso ma lo fa nei semplici oggetti nativi.
Sanket Berde,

6

Voglio solo aggiungere che esiste un altro hook Lifecycle chiamato DoCheckutile se il @Inputvalore non è un valore primitivo.

Ho un array come Inputtale, quindi questo non attiva l' OnChangesevento quando il contenuto cambia (perché il controllo che Angular fa è 'semplice' e non profondo, quindi l'array è ancora un array, anche se il contenuto sull'array è cambiato).

Quindi implemento un codice di controllo personalizzato per decidere se voglio aggiornare la mia vista con l'array modificato.


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

prova a utilizzare questo metodo. Spero che sia di aiuto


4

Puoi anche avere un osservabile che si innesca sulle modifiche nel componente padre (CategoryComponent) e fare ciò che vuoi fare nella sottoscrizione nel componente figlio. (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
});
}

2

Qui ngOnChanges si attiverà sempre quando la proprietà di input cambia:

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

1

Questa soluzione utilizza una classe proxy e offre i seguenti vantaggi:

  • Consente al consumatore di sfruttare la potenza di RXJS
  • Più compatto di altre soluzioni proposte finora
  • Più typesafe rispetto all'usongOnChanges()

Esempio di utilizzo:

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

Funzione utile:

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

0

Se non desideri utilizzare il metodo ngOnChange implement og onChange (), puoi anche iscriverti alle modifiche di un elemento specifico per evento valueChanges , ETC.

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

});

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

il markForCheck () scritto a causa dell'utilizzo in questa dichiarazione:

  changeDetection: ChangeDetectionStrategy.OnPush

3
Questo è completamente diverso dalla domanda e sebbene le persone utili dovrebbero essere consapevoli che il tuo codice riguarda un diverso tipo di modifica. La domanda posta sulla modifica dei dati è ESTERNA da un componente e quindi la conoscenza e la risposta del componente al fatto che i dati sono cambiati. La tua risposta parla del rilevamento di modifiche all'interno del componente stesso su elementi come input in un modulo che sono LOCALI al componente.
rmcsharry,

0

Potresti anche passare un EventEmitter come input. Non sono sicuro che questa sia la migliore pratica ...

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>

E in VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}

Fornisci link utili, frammenti di codice per supportare la tua soluzione.
mishsx,

Ho aggiunto un esempio.
Cédric Schweizer,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.