Comment détecter quand une valeur @Input () change en angulaire?

471

J'ai un composant parent ( CategoryComponent ), un composant enfant ( videoListComponent ) et un ApiService.

J'ai la plupart de cela fonctionne bien, c'est-à-dire que chaque composant peut accéder à l'api json et obtenir ses données pertinentes via des observables.

Actuellement, le composant de liste de vidéos n'obtient que toutes les vidéos, je voudrais filtrer cela uniquement pour les vidéos d'une catégorie particulière, j'ai réussi cela en transmettant le categoryId à l'enfant via @Input().

CategoryComponent.html

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

Cela fonctionne et lorsque la catégorie parent CategoryComponent change, la valeur categoryId est transmise via @Input()mais j'ai besoin de détecter cela dans VideoListComponent et de demander à nouveau le tableau de vidéos via APIService (avec le nouveau categoryId).

Dans AngularJS, j'aurais fait un $watchsur la variable. Quelle est la meilleure façon de gérer cela?

Jon Catmull
la source
regarder les changements d'entrée: - angulartutorial.net/2017/12/watch-input-changes-angular-4.html
Prashobh
pour les changements de baies: stackoverflow.com/questions/42962394/…
dasfdsa

Réponses:

720

En fait, il existe deux façons de détecter et de réagir lorsqu'une entrée change dans le composant enfant dans angular2 +:

  1. Vous pouvez utiliser la méthode de cycle de vie ngOnChanges () comme également mentionné dans les réponses plus anciennes:
    @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

    }

Liens de documentation: ngOnChanges, SimpleChanges, Exemple de démonstration de SimpleChange
: Regardez ce plunker

  1. Alternativement, vous pouvez également utiliser un configurateur de propriétés d'entrée comme suit:
    private _categoryId: string;

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

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

    }

    get categoryId(): string {

        return this._categoryId;

    }

Lien de documentation: Regardez ici .

Exemple de démonstration: regardez ce plongeur .

QUELLE APPROCHE DEVRIEZ-VOUS UTILISER?

Si votre composant a plusieurs entrées, alors, si vous utilisez ngOnChanges (), vous obtiendrez toutes les modifications pour toutes les entrées à la fois dans ngOnChanges (). En utilisant cette approche, vous pouvez également comparer les valeurs actuelles et précédentes de l'entrée qui a changé et prendre des mesures en conséquence.

Cependant, si vous voulez faire quelque chose lorsque seule une entrée particulière change (et que vous ne vous souciez pas des autres entrées), il peut être plus simple d'utiliser un configurateur de propriétés d'entrée. Cependant, cette approche ne fournit pas de méthode intégrée pour comparer les valeurs précédentes et actuelles de l'entrée modifiée (ce que vous pouvez faire facilement avec la méthode de cycle de vie ngOnChanges).

EDIT 2017-07-25: LA DÉTECTION DE CHANGEMENT ANGULAIRE PEUT TOUJOURS NE PAS SE DÉCLENCHER DANS CERTAINES CIRCONSTANCES

Normalement, la détection de changement pour setter et ngOnChanges se déclenche chaque fois que le composant parent modifie les données qu'il transmet à l'enfant, à condition que les données soient un type de données primitif JS (chaîne, nombre, booléen) . Cependant, dans les scénarios suivants, il ne se déclenchera pas et vous devrez prendre des mesures supplémentaires pour le faire fonctionner.

  1. Si vous utilisez un objet ou un tableau imbriqué (au lieu d'un type de données primitif JS) pour transmettre des données du parent à l'enfant, la détection des modifications (à l'aide de setter ou de ngchanges) peut ne pas se déclencher, comme également mentionné dans la réponse de l'utilisateur: muetzerich. Pour des solutions, regardez ici .

  2. Si vous mutez des données en dehors du contexte angulaire (c'est-à-dire en externe), alors angular ne sera pas au courant des changements. Vous devrez peut-être utiliser ChangeDetectorRef ou NgZone dans votre composant pour rendre angulaire conscient des changements externes et ainsi déclencher la détection des changements. Référez-vous à ceci .

Alan CS
la source
8
Très bon point concernant l'utilisation de setters avec des entrées, je mettrai à jour cela pour être la réponse acceptée car elle est plus complète. Merci.
Jon Catmull
2
@trichetriche, le setter (méthode set) sera appelé chaque fois que le parent modifie l'entrée. De même, il en va de même pour ngOnChanges ().
Alan CS
Eh bien, apparemment non ... Je vais essayer de comprendre cela, c'est probablement quelque chose de mal que j'ai fait.
1
Je suis juste tombé sur cette question, j'ai noté que vous avez dit que l'utilisation du setter ne permettrait pas de comparer les valeurs, mais ce n'est pas vrai, vous pouvez appeler votre doSomethingméthode et prendre 2 arguments les nouvelles et les anciennes valeurs avant de réellement définir la nouvelle valeur, d'une autre manière serait de stocker l'ancienne valeur avant de définir et d'appeler la méthode après cela.
T.Aoukar
1
@ T.Aoukar Ce que je dis, c'est que le setter d'entrée ne supporte pas nativement la comparaison de valeurs anciennes / nouvelles comme le fait ngOnChanges (). Vous pouvez toujours utiliser des hacks pour stocker l'ancienne valeur (comme vous le mentionnez) pour la comparer avec la nouvelle valeur.
Alan CS
105

Utilisez la ngOnChanges()méthode du cycle de vie dans votre composant.

ngOnChanges est appelé juste après que les propriétés liées aux données ont été vérifiées et avant que les enfants de vue et de contenu ne soient vérifiés si au moins l'un d'entre eux a changé.

Voici les documents .

muetzerich
la source
6
Cela fonctionne lorsqu'une variable est définie sur une nouvelle valeur, par exemple MyComponent.myArray = [], mais si une valeur d'entrée est modifiée, par exemple MyComponent.myArray.push(newValue), elle ngOnChanges()n'est pas déclenchée. Des idées sur la façon de saisir ce changement?
Levi Fuller
4
ngOnChanges()n'est pas appelé lorsqu'un objet imbriqué a changé. Peut-être que vous trouvez une solution ici
muetzerich
33

J'obtenais des erreurs dans la console ainsi que le compilateur et l'IDE lors de l'utilisation du SimpleChangestype dans la signature de fonction. Pour éviter les erreurs, utilisez anyplutôt le mot - clé dans la signature.

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

ÉDITER:

Comme Jon l'a souligné ci-dessous, vous pouvez utiliser la SimpleChangessignature lorsque vous utilisez la notation entre crochets plutôt que la notation par points.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Darcy
la source
1
Avez-vous importé l' SimpleChangesinterface en haut de votre fichier?
Jon Catmull
@Jon - Oui. Le problème n'était pas la signature elle-même, mais l'accès aux paramètres attribués au moment de l'exécution à l'objet SimpleChanges. Par exemple, dans mon composant, j'ai lié ma classe User à l'entrée (ie <my-user-details [user]="selectedUser"></my-user-details>). Cette propriété est accessible à partir de la classe SimpleChanges en appelant changes.user.currentValue. L'avis userest lié au moment de l'exécution et ne fait pas partie de l'objet SimpleChanges
Darcy
1
Avez-vous essayé cela? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Jon Catmull
@Jon - Le passage à la notation entre crochets fait l'affaire. J'ai mis à jour ma réponse pour l'inclure comme alternative.
Darcy
1
importer {SimpleChanges} depuis '@ angular / core'; Si c'est dans les documents angulaires, et non dans un type natif de typographie, alors c'est probablement à partir d'un module angulaire, très probablement 'angular / core'
BentOnCoding
13

Le pari le plus sûr est d'aller avec un service partagé au lieu d'un @Inputparamètre. En outre, le @Inputparamètre ne détecte pas les modifications du type d'objet imbriqué complexe.

Un exemple de service simple est le suivant:

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

Vous pouvez utiliser une implémentation similaire dans d'autres composants et tous vos composants partageront les mêmes valeurs partagées.

Sanket Berde
la source
1
Merci Sanket. C'est un très bon point à faire valoir et, le cas échéant, une meilleure façon de le faire. Je laisserai la réponse acceptée telle quelle, car elle répond à mes questions spécifiques concernant la détection des modifications @Input.
Jon Catmull
1
Un paramètre @Input ne détecte pas les modifications à l' intérieur d' un type d'objet imbriqué complexe, mais si l'objet lui-même change, il se déclenchera, non? Par exemple, j'ai le paramètre d'entrée "parent", donc si je change de parent dans son ensemble, la détection de changement se déclenchera, mais si je change parent.name, ce ne sera pas le cas?
yogibimbi
Vous devez fournir une raison pour laquelle votre solution est plus sûre
Joshua Kemmerer
1
Le @Inputparamètre @JoshuaKemmerer ne détecte pas les changements dans le type d'objet imbriqué complexe mais il le fait dans les objets natifs simples.
Sanket Berde
6

Je veux juste ajouter qu'il existe un autre crochet Lifecycle appelé DoCheckqui est utile si la @Inputvaleur n'est pas une valeur primitive.

J'ai un tableau en tant que Inputtel, cela ne déclenche donc pas l' OnChangesévénement lorsque le contenu change (car la vérification que fait Angular est `` simple '' et peu profonde, donc le tableau est toujours un tableau, même si le contenu du tableau a changé).

J'implémente ensuite un code de vérification personnalisé pour décider si je veux mettre à jour ma vue avec le tableau modifié.

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

veuillez essayer d'utiliser cette méthode. J'espère que cela t'aides

Akshay Rajput
la source
4

Vous pouvez également avoir un observable qui se déclenche lors des changements dans le composant parent (CategoryComponent) et faire ce que vous voulez faire dans l'abonnement dans le composant enfant. (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
la source
2

Ici, ngOnChanges se déclenchera toujours lorsque votre propriété d'entrée change:

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

Cette solution utilise une classe proxy et offre les avantages suivants:

  • Permet au consommateur de tirer parti de la puissance de RXJS
  • Plus compact que les autres solutions proposées jusqu'à présent
  • Plus sûr que d'utiliserngOnChanges()

Exemple d'utilisation:

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

Fonction d'utilité:

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
la source
0

Si vous ne souhaitez pas utiliser la méthode ngOnChange implement og onChange (), vous pouvez également vous abonner aux modifications d'un élément spécifique par l' événement valueChanges , ETC.

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

});

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

le markForCheck () écrit en raison de l'utilisation dans cette déclaration:

  changeDetection: ChangeDetectionStrategy.OnPush
user1012506
la source
3
Ceci est complètement différent de la question et bien que les personnes utiles devraient être conscientes que votre code concerne un type de changement différent. La question portait sur la modification des données provenant de l'extérieur d'un composant, puis sur la connaissance et la réponse de votre composant au fait que les données ont changé. Votre réponse parle de la détection des changements dans le composant lui-même sur des choses comme les entrées sur un formulaire qui sont LOCALES au composant.
rmcsharry
0

Vous pouvez également simplement passer un EventEmitter en entrée. Je ne sais pas si c'est la meilleure pratique, mais ...

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>

Et dans VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Cédric Schweizer
la source
Veuillez fournir des liens utiles et des extraits de code pour prendre en charge votre solution.
mishsx
J'ai ajouté un exemple.
Cédric Schweizer