Détection de changement angulaire2: ngOnChanges ne se déclenche pas pour l'objet imbriqué

129

Je sais que je ne suis pas le premier à poser des questions à ce sujet, mais je ne trouve pas de réponse dans les questions précédentes. J'ai ceci dans un composant

<div class="col-sm-5">
    <laps
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"
        (lapsHandler)="lapsHandler($event)">
    </laps>
</div>

<map
    [lapsData]="rawLapsData"
    class="col-sm-7">
</map>

Dans le contrôleur rawLapsdataest muté de temps en temps.

Dans laps, les données sont sorties au format HTML dans un format tabulaire. Cela change chaque fois que cela rawLapsdatachange.

Mon mapcomposant doit être utilisé ngOnChangescomme déclencheur pour redessiner des marqueurs sur une carte Google. Le problème est que ngOnChanges ne se déclenche pas lors des rawLapsDatamodifications du parent. Que puis-je faire?

import {Component, Input, OnInit, OnChanges, SimpleChange} from 'angular2/core';

@Component({
    selector: 'map',
    templateUrl: './components/edMap/edMap.html',
    styleUrls: ['./components/edMap/edMap.css']
})
export class MapCmp implements OnInit, OnChanges {
    @Input() lapsData: any;
    map: google.maps.Map;

    ngOnInit() {
        ...
    }

    ngOnChanges(changes: { [propName: string]: SimpleChange }) {
        console.log('ngOnChanges = ', changes['lapsData']);
        if (this.map) this.drawMarkers();
    }

Mise à jour: ngOnChanges ne fonctionne pas, mais il semble que lapsData soit mis à jour. Dans le ngInit se trouve un écouteur d'événements pour les modifications de zoom qui appelle également this.drawmarkers. Lorsque je change de zoom, je constate en effet un changement de marqueurs. Le seul problème est que je ne reçois pas la notification au moment où les données d'entrée changent.

Dans le parent, j'ai cette ligne. (Rappelez-vous que le changement se reflète dans les tours, mais pas sur la carte).

this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);

Et notez que this.rawLapsDatac'est lui-même un pointeur vers le milieu d'un grand objet json

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Simon H
la source
Votre code n'indique pas comment les données sont mises à jour ou de quel type elles sont. Une nouvelle instance est-elle attribuée ou est-ce simplement une propriété de la valeur modifiée?
Günter Zöchbauer
@ GünterZöchbauer J'ai ajouté la ligne du composant parent
Simon H
Je suppose que envelopper cette ligne zone.run(...)devrait le faire alors.
Günter Zöchbauer
1
Votre tableau (référence) ne change pas, donc ngOnChanges()ne sera pas appelé. Vous pouvez utiliser ngDoCheck()et implémenter votre propre logique pour déterminer si le contenu du tableau a changé. lapsDataest mis à jour car il a / est une référence au même tableau que rawLapsData.
Mark Rajcok le
1
1) Dans le composant laps, votre code / modèle boucle sur chaque entrée du tableau lapsData et affiche le contenu, il existe donc des liaisons angulaires sur chaque élément de données affiché. 2) Même si Angular ne détecte aucune modification (vérification des références) des propriétés d'entrée d'un composant, il vérifie toujours (par défaut) toutes les liaisons de modèle. C'est ainsi que les tours reprennent les changements. 3) Le composant maps n'a probablement aucune liaison dans son modèle avec sa propriété d'entrée lapsData, n'est-ce pas? Cela expliquerait la différence.
Mark Rajcok le

Réponses:

153

rawLapsData continue de pointer vers le même tableau, même si vous modifiez le contenu du tableau (par exemple, ajouter des éléments, supprimer des éléments, changer un élément).

Pendant la détection de changement, lorsque Angular vérifie les propriétés d'entrée des composants pour le changement, il utilise (essentiellement) ===pour la vérification sale. Pour les tableaux, cela signifie que les références aux tableaux (uniquement) sont vérifiées. Comme la rawLapsDataréférence du tableau ne change pas, ngOnChanges()ne sera pas appelée.

Je peux penser à deux solutions possibles:

  1. Implémentez ngDoCheck()et exécutez votre propre logique de détection des modifications pour déterminer si le contenu du tableau a changé. (Le document Lifecycle Hooks a un exemple .)

  2. Attribuez un nouveau tableau à rawLapsDatachaque fois que vous apportez des modifications au contenu du tableau. Puis ngOnChanges()sera appelé car le tableau (référence) apparaîtra comme un changement.

Dans votre réponse, vous avez proposé une autre solution.

Répétant ici quelques commentaires sur le PO:

Je ne vois toujours pas comment lapspeut prendre en compte le changement (il doit sûrement utiliser quelque chose d'équivalent à ngOnChanges()lui-même?) Alors que je ne mappeux pas.

  • Dans le lapscomposant, votre code / modèle boucle sur chaque entrée du lapsDatatableau et affiche le contenu, de sorte qu'il existe des liaisons angulaires sur chaque élément de données affiché.
  • Même lorsque Angular ne détecte aucune modification des propriétés d'entrée d'un composant (en utilisant la ===vérification), il vérifie toujours (par défaut) toutes les liaisons de modèle. Lorsque l'un de ces changements change, Angular mettra à jour le DOM. C'est ce que vous voyez.
  • Le mapscomposant n'a probablement aucune liaison dans son modèle avec sa lapsDatapropriété d'entrée, non? Cela expliquerait la différence.

Notez que lapsDatadans les deux composants et rawLapsDatadans le composant parent, tous pointent vers le même / un tableau. Ainsi, même si Angular ne remarque aucun changement (de référence) dans les lapsDatapropriétés d'entrée, les composants "obtiennent" / voient tout changement de contenu du tableau parce qu'ils partagent / référencent tous ce tableau. Nous n'avons pas besoin d'Angular pour propager ces changements, comme nous le ferions avec un type primitif (chaîne, nombre, booléen). Mais avec un type primitif, toute modification de la valeur se déclencherait toujours ngOnChanges()- ce que vous exploitez dans votre réponse / solution.

Comme vous l'avez probablement compris maintenant, les propriétés d'entrée d'objet ont le même comportement que les propriétés d'entrée de tableau.

Mark Rajcok
la source
Oui, je faisais muter un objet profondément imbriqué, et je suppose que cela a rendu difficile pour Angular de détecter les changements dans la structure. Ce n'est pas ma préférence pour muter mais c'est traduit en XML et je ne peux me permettre de perdre aucune des données environnantes car je veux recréer à nouveau le xml à la fin
Simon H
7
@SimonH, "difficile pour Angular de repérer les changements dans la structure" - Pour être clair, Angular ne regarde même pas les propriétés d'entrée qui sont des tableaux ou des objets pour les changements. Il regarde uniquement si la valeur a changé - pour les objets et les tableaux, la valeur est la référence. Pour les types primitifs, la valeur est ... la valeur. (Je ne suis pas sûr d'avoir tout le jargon, mais vous avez l'idée.)
Mark Rajcok
11
Très bonne réponse. L'équipe Angular2 a désespérément besoin de publier un document détaillé et faisant autorité sur les composants internes de la détection des changements.
Si je fais la fonctionnalité dans doCheck, dans mon cas, le do check appelle tellement de fois. Pouvez-vous me dire une autre manière?
Mr_Perfect
@MarkRajcok pouvez-vous s'il vous plaît m'aider à résoudre ce problème stackoverflow.com/questions/50166996/…
Nikson
27

Ce n'est pas l'approche la plus propre, mais vous pouvez simplement cloner l'objet à chaque fois que vous modifiez la valeur?

   rawLapsData = Object.assign({}, rawLapsData);

Je pense que je préférerais cette approche à la mise en œuvre de la vôtre, ngDoCheck()mais peut-être que quelqu'un comme @ GünterZöchbauer pourrait intervenir.

David
la source
Si vous n'êtes pas sûr qu'un navigateur ciblé prendrait en charge <Object.assign ()>, vous pouvez également stringify dans une chaîne json et analyser de nouveau à json, qui créera également un nouvel objet ...
Guntram
1
@Guntram Ou un polyfill?
David
12

Dans le cas de tableaux, vous pouvez le faire comme ceci:

Dans le .tsfichier (composant parent) où vous mettez à jour, rawLapsDataprocédez comme suit:

rawLapsData = somevalue; // change detection will not happen

Solution:

rawLapsData = {...somevalue}; //change detection will happen

et ngOnChangessera appelé dans le composant enfant

Danois Dullu
la source
10

En tant qu'extension de la deuxième solution de Mark Rajcok

Attribuez un nouveau tableau à rawLapsData chaque fois que vous apportez des modifications au contenu du tableau. Ensuite, ngOnChanges () sera appelé car le tableau (référence) apparaîtra comme un changement

vous pouvez cloner le contenu du tableau comme ceci:

rawLapsData = rawLapsData.slice(0);

Je mentionne cela parce que

rawLapsData = Object.assign ({}, rawLapsData);

n'a pas fonctionné pour moi. J'espère que ça aide.

decebal
la source
8

Si les données proviennent d'une bibliothèque externe, vous devrez peut-être exécuter l'instruction de mise à jour des données dans zone.run(...). Injectez zone: NgZonepour cela. Si vous pouvez déjà exécuter l'instanciation de la bibliothèque externe zone.run(), vous n'en aurez peut-être pas besoin zone.run()plus tard.

Günter Zöchbauer
la source
Comme indiqué dans les commentaires à l'OP, les changements n'étaient pas externes mais profondément dans un objet json
Simon H
1
Comme votre réponse l'indique, nous devons toujours exécuter quelque chose pour garder les choses synchronisées dans Angular2, comme angular1 c'était $scope.$apply?
Pankaj Parkar le
1
Si quelque chose est démarré depuis l'extérieur d'Angular, l'API, corrigée par zone, n'est pas utilisée et Angular n'est pas notifié des changements possibles. Oui, zone.runest similaire à $scope.apply.
Günter Zöchbauer
6

Utilisez ChangeDetectorRef.detectChanges()pour dire à Angular d'exécuter une détection de changement lorsque vous modifiez un objet imbriqué (qu'il manque avec sa vérification sale).

Tim Phillips
la source
Mais comment ? J'essaye d'utiliser ceci, je veux déclencher changeDetection dans un enfant après que le parent ait poussé un nouvel élément dans une double limite [(Collection)].
Deunz
Le problème n'est pas que la détection de changement ne se produit pas, mais que la détection de changement ne détecte pas les changements! donc cela ne semble pas être une solution! pourquoi cette réponse a-t-elle obtenu cinq votes positifs?
Shahryar Saljoughi le
5

J'ai 2 solutions pour résoudre votre problème

  1. Utilisez ngDoCheckpour détecter les objectdonnées modifiées ou non
  2. Affectez objectà une nouvelle adresse mémoire à object = Object.create(object)partir du composant parent.
Giapnh
la source
2
Y aurait-il une différence notable entre Object.create (object) et Object.assign ({}, object)?
Daniel Galarza
3

Ma solution de 'hack' est

   <div class="col-sm-5">
        <laps
            [lapsData]="rawLapsData"
            [selectedTps]="selectedTps"
            (lapsHandler)="lapsHandler($event)">
        </laps>
    </div>
    <map
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"   // <--------
        class="col-sm-7">
    </map>

selectedTps change en même temps que rawLapsData et cela donne à la carte une autre chance de détecter le changement grâce à un type primitif d' objet plus simple . Ce n'est PAS élégant, mais cela fonctionne.

Simon H
la source
J'ai du mal à suivre tous les changements sur divers composants dans la syntaxe du modèle, en particulier sur les applications à moyenne / grande échelle. Habituellement, j'utilise un émetteur d'événement partagé et un abonnement pour transmettre les données (solution rapide), ou implémenter le modèle Redux (via Rx.Subject) pour cela (quand il est temps de planifier) ​​...
Sasxa
3

La détection des modifications n'est pas déclenchée lorsque vous modifiez une propriété d'un objet (y compris un objet imbriqué). Une solution serait de réaffecter une nouvelle référence d'objet en utilisant la fonction clone () 'lodash'.

import * as _ from 'lodash';

this.foo = _.clone(this.foo);
Anton Nikprelaj
la source
2

Voici un hack qui vient de me sortir des ennuis avec celui-ci.

Donc, un scénario similaire à l'OP - j'ai un composant angulaire imbriqué qui a besoin de données qui lui sont transmises, mais l'entrée pointe vers un tableau, et comme mentionné ci-dessus, Angular ne voit pas de changement car il n'examine pas le contenu du tableau.

Donc, pour le réparer, je convertis le tableau en une chaîne pour qu'Angular détecte un changement, puis dans le composant imbriqué, j'ai divisé (',') la chaîne en un tableau et ses jours heureux à nouveau.

Graham Phillips
la source
1
Beurk! L'approche array.slice (0) est beaucoup plus propre.
Chris Haines
2

Je suis tombé sur le même besoin. Et j'ai beaucoup lu à ce sujet, voici donc mon cuivre sur le sujet.

Si vous voulez votre détection de changement sur push, alors vous l'auriez lorsque vous modifiez la valeur d'un objet à l'intérieur, n'est-ce pas? Et vous l'auriez également si, d'une manière ou d'une autre, vous supprimiez des objets.

Comme déjà dit, l'utilisation de changeDetectionStrategy.onPush

Supposons que vous ayez ce composant que vous avez créé, avec changeDetectionStrategy.onPush:

<component [collection]="myCollection"></component>

Ensuite, vous poussez un élément et déclenchez la détection de changement:

myCollection.push(anItem);
refresh();

ou vous supprimez un élément et déclenchez la détection des modifications:

myCollection.splice(0,1);
refresh();

ou vous modifiez une valeur attrbibute pour un élément et déclenchez la détection de changement:

myCollection[5].attribute = 'new value';
refresh();

Contenu de rafraîchissement:

refresh() : void {
    this.myCollection = this.myCollection.slice();
}

La méthode slice renvoie exactement le même tableau et le signe [=] y fait une nouvelle référence, déclenchant la détection de changement chaque fois que vous en avez besoin. Facile et lisible :)

Cordialement,

Deunz
la source
2

J'ai essayé toutes les solutions mentionnées ici, mais pour une raison quelconque, ngOnChanges()je n'ai toujours pas tiré pour moi. Donc je l'ai appelé this.ngOnChanges()après avoir appelé le service qui repeuple mes tableaux et cela a fonctionné .... correct? probablement pas. Soigné? sûrement pas. Travaux? Oui!

Yazan Khalaileh
la source
1

ok donc ma solution était:

this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;

Et cela me déclenche ngOnChanges

DanielWaw
la source
0

J'ai dû créer un hack pour cela -

I created a Boolean Input variable and toggled it whenever array changed, which triggered change detection in the child component, hence achieving the purpose
Rishabh Wadhwa
la source
0

Dans mon cas, c'était des changements de valeur d'objet qui ngOnChangene capturaient pas. Quelques valeurs d'objet sont modifiées en réponse à l'appel de l'API. La réinitialisation de l'objet a résolu le problème et provoqué lengOnChange déclenchement du dans le composant enfant.

Quelque chose comme

 this.pagingObj = new Paging(); //This line did the magic
 this.pagingObj.pageNumber = response.PageNumber;
Kailas
la source