Detección de cambio Angular2: ngOnChanges no dispara para objeto anidado

129

Sé que no soy el primero en preguntar sobre esto, pero no puedo encontrar una respuesta en las preguntas anteriores. Tengo esto en un componente

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

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

En el controlador rawLapsdatase muta de vez en cuando.

En laps, los datos se generan como HTML en formato tabular. Esto cambia cada vez que rawLapsdatacambia.

Mi mapcomponente debe usarse ngOnChangescomo desencadenante para volver a dibujar marcadores en un mapa de Google. El problema es que ngOnChanges no se dispara cuando hay rawLapsDatacambios en el padre. ¿Que puedo hacer?

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

Actualización: ngOnChanges no funciona, pero parece que lapsData se está actualizando. En el ngInit es un detector de eventos para cambios de zoom que también llama this.drawmarkers. Cuando cambio el zoom, sí veo un cambio en los marcadores. Entonces, el único problema es que no recibo la notificación en el momento en que cambian los datos de entrada.

En el padre, tengo esta línea. (Recuerde que el cambio se refleja en vueltas, pero no en el mapa).

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

Y tenga en cuenta que this.rawLapsDataes en sí mismo un puntero al centro de un gran objeto json

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Simon H
fuente
Su código no muestra cómo se actualizan los datos o qué tipo de datos son. ¿Se asigna una nueva instancia o solo se modifica una propiedad del valor?
Günter Zöchbauer
@ GünterZöchbauer Agregué la línea del componente principal
Simon H
Supongo que envolver esta línea zone.run(...)debería hacerlo entonces.
Günter Zöchbauer
1
Su matriz (referencia) no está cambiando, por ngOnChanges()lo que no se llamará. Puede usar ngDoCheck()e implementar su propia lógica para determinar si el contenido de la matriz ha cambiado. lapsDatase actualiza porque tiene / es una referencia a la misma matriz que rawLapsData.
Mark Rajcok
1
1) En el componente laps, su código / plantilla recorre cada entrada de la matriz lapsData y muestra el contenido, por lo que hay enlaces angulares en cada pieza de datos que se muestra. 2) Incluso si Angular no detecta ningún cambio (verificación de referencia) en las propiedades de entrada de un componente, todavía (de manera predeterminada) verifica todos los enlaces de plantilla. Así es como vueltas vueltas sobre los cambios. 3) El componente de mapas probablemente no tiene enlaces en su plantilla a su propiedad de entrada lapsData, ¿verdad? Eso explicaría la diferencia.
Mark Rajcok

Respuestas:

153

rawLapsData continúa apuntando a la misma matriz, incluso si modifica el contenido de la matriz (por ejemplo, agregar elementos, eliminar elementos, cambiar un elemento).

Durante la detección de cambios, cuando Angular verifica las propiedades de entrada de los componentes para detectar cambios, utiliza (esencialmente) ===para la verificación sucia. Para las matrices, esto significa que las referencias de la matriz (solo) están sucias. Como la rawLapsDatareferencia de matriz no está cambiando, ngOnChanges()no se llamará.

Se me ocurren dos posibles soluciones:

  1. Implemente ngDoCheck()y realice su propia lógica de detección de cambios para determinar si el contenido de la matriz ha cambiado. (El documento Lifecycle Hooks tiene un ejemplo ).

  2. Asigne una nueva matriz rawLapsDatacada vez que realice cambios en el contenido de la matriz. Luego ngOnChanges()se llamará porque la matriz (referencia) aparecerá como un cambio.

En su respuesta, se le ocurrió otra solución.

Repitiendo algunos comentarios aquí sobre el OP:

Todavía no veo cómo lapspuedo captar el cambio (¿seguramente debe estar usando algo equivalente a ngOnChanges()sí mismo?) Mientras mapque no puedo.

  • En el lapscomponente, su código / plantilla recorre cada entrada de la lapsDatamatriz y muestra el contenido, por lo que hay enlaces angulares en cada pieza de datos que se muestra.
  • Incluso cuando Angular no detecta ningún cambio en las propiedades de entrada de un componente (usando la ===comprobación), todavía (por defecto) comprueba suciamente todos los enlaces de plantilla. Cuando alguno de esos cambios, Angular actualizará el DOM. Eso es lo que estás viendo.
  • El mapscomponente probablemente no tiene enlaces en su plantilla a su lapsDatapropiedad de entrada, ¿verdad? Eso explicaría la diferencia.

Tenga lapsDataen cuenta que en ambos componentes y rawLapsDataen el componente principal todos apuntan a la misma / una matriz. Entonces, aunque Angular no nota ningún cambio (de referencia) en las lapsDatapropiedades de entrada, los componentes "obtienen" / ven cualquier cambio en el contenido de la matriz porque todos comparten / hacen referencia a esa matriz. No necesitamos Angular para propagar estos cambios, como lo haríamos con un tipo primitivo (cadena, número, booleano). Pero con un tipo primitivo, cualquier cambio en el valor siempre se activaría ngOnChanges(), lo cual es algo que explota en su respuesta / solución.

Como probablemente haya descubierto ahora, las propiedades de entrada de objeto tienen el mismo comportamiento que las propiedades de entrada de matriz.

Mark Rajcok
fuente
Sí, estaba mutando un objeto profundamente anidado, y supongo que eso dificultó a Angular detectar cambios en la estructura. No es mi preferencia mutar, pero este es XML traducido y no puedo permitirme perder ninguno de los datos circundantes, ya que quiero volver a crear el XML al final
Simon H
77
@SimonH, "difícil para Angular para detectar cambios en la estructura" - Solo para ser claros, Angular ni siquiera mira dentro de las propiedades de entrada que son matrices u objetos para los cambios. Solo se ve para ver si el valor cambió: para los objetos y las matrices, el valor es la referencia. Para los tipos primitivos, el valor es ... el valor. (No estoy seguro de tener toda la jerga correcta, pero entiendes la idea.)
Mark Rajcok
11
Gran respuesta. El equipo de Angular2 necesita desesperadamente publicar un documento detallado y autorizado sobre los aspectos internos de la detección de cambios.
Si hago la funcionalidad en doCheck, en mi caso el do check me llama tantas veces. ¿Puedes decirme de alguna otra manera?
Mr_Perfect
@ MarkRajcok, ¿pueden ayudarme a resolver este problema? Stackoverflow.com/questions/50166996/…
Nikson
27

No es el enfoque más limpio, pero ¿puede clonar el objeto cada vez que cambia el valor?

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

Creo que preferiría este enfoque en lugar de implementar el suyo, ngDoCheck()pero tal vez alguien como @ GünterZöchbauer podría intervenir.

David
fuente
Si no está seguro de si un navegador de destino admitiría <Object.assign ()>, también puede encadenar en una cadena json y analizar de nuevo a json, que también creará un nuevo objeto ...
Guntram
1
@Guntram ¿O un polyfill?
David
12

En Case of Arrays puedes hacerlo así:

En el .tsarchivo (componente principal) donde está actualizando, rawLapsDatahágalo así:

rawLapsData = somevalue; // change detection will not happen

Solución:

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

y ngOnChangesllamará en el componente hijo

Dulce danés
fuente
10

Como una extensión a la segunda solución de Mark Rajcok

Asigne una nueva matriz a rawLapsData siempre que realice cambios en el contenido de la matriz. Entonces se llamará a ngOnChanges () porque la matriz (referencia) aparecerá como un cambio

puede clonar el contenido de la matriz de esta manera:

rawLapsData = rawLapsData.slice(0);

Estoy mencionando esto porque

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

no funciono para mi Espero que esto ayude.

decebal
fuente
8

Si los datos provienen de una biblioteca externa, es posible que deba ejecutar la declaración de actualización de datos dentro zone.run(...). Inyectar zone: NgZonepara esto. Si ya puede ejecutar la creación de instancias de la biblioteca externa zone.run(), es posible que no necesite zone.run()más tarde.

Günter Zöchbauer
fuente
Como se señaló en los comentarios al OP, los cambios no fueron externos sino profundos dentro de un objeto json
Simon H
1
Como dice su respuesta, ¿aún necesitamos ejecutar algo para mantener la cosa sincronizada en Angular2, como angular1 $scope.$apply?
Pankaj Parkar
1
Si algo se inicia desde fuera de Angular, la API, parcheada por zona, no se usa y Angular no recibe notificaciones sobre posibles cambios. Sí, zone.runes similar al $scope.apply.
Günter Zöchbauer
6

Use ChangeDetectorRef.detectChanges()para decirle a Angular que ejecute una detección de cambios cuando edita un objeto anidado (que se pierde con su verificación sucia).

Tim Phillips
fuente
Pero cómo ? Estoy tratando de usar esto, quiero desencadenar changeDetection en un niño después de que el padre empuje un nuevo elemento en un límite de 2 formas [(Colección)].
Deunz
¡El problema no es que la detección de cambios no ocurra, sino que la detección de cambios no detecta los cambios! ¡Así que esto no parece ser una solución! ¿Por qué esta respuesta obtuvo cinco votos a favor?
Shahryar Saljoughi
5

Tengo 2 soluciones para resolver tu problema.

  1. Se usa ngDoCheckpara detectar objectdatos modificados o no
  2. Asignar objecta una nueva dirección de memoria por parte object = Object.create(object)del componente principal.
giapnh
fuente
2
¿Habría alguna diferencia notable entre Object.create (objeto) versus Object.assign ({}, objeto)?
Daniel Galarza
3

Mi solución de 'pirateo' es

   <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 cambia al mismo tiempo que rawLapsData y eso le da al mapa otra oportunidad de detectar el cambio a través de un tipo primitivo de objeto más simple . NO es elegante, pero funciona.

Simon H
fuente
Me resulta difícil rastrear todos los cambios en varios componentes en la sintaxis de la plantilla, especialmente en aplicaciones de mediana / gran escala. Por lo general, uso el emisor de eventos compartidos y la suscripción para pasar los datos (solución rápida), o implementar el patrón Redux (a través de Rx.Subject) para esto (cuando hay tiempo para planificar) ...
Sasxa
3

La detección de cambios no se activa cuando cambia una propiedad de un objeto (incluido el objeto anidado). Una solución sería reasignar una nueva referencia de objeto usando la función 'lodash' clone ().

import * as _ from 'lodash';

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

Aquí hay un truco que me sacó de problemas con este.

Entonces, un escenario similar al OP: tengo un componente angular anidado que necesita datos transmitidos, pero la entrada apunta a una matriz y, como se mencionó anteriormente, Angular no ve un cambio ya que no examina el contenido de la matriz.

Entonces, para solucionarlo, convierto la matriz en una cadena para que Angular detecte un cambio, y luego, en el componente anidado, dividí (',') la cadena en una matriz y sus días felices nuevamente.

Graham Phillips
fuente
1
¡Qué asco! El enfoque array.slice (0) es mucho más limpio.
Chris Haines
2

Me topé con la misma necesidad. Y leí mucho sobre esto, así que aquí está mi cobre sobre el tema.

Si desea su detección de cambio en el momento, entonces la tendría cuando cambie el valor de un objeto dentro de la derecha? Y también lo tendría si de alguna manera, elimina objetos.

Como ya se dijo, el uso de changeDetectionStrategy.onPush

Digamos que tiene este componente que creó, con changeDetectionStrategy.onPush:

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

Luego empujaría un elemento y activaría la detección de cambio:

myCollection.push(anItem);
refresh();

o eliminaría un elemento y activaría la detección de cambio:

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

o cambiaría un valor de atributo para un artículo y activaría la detección de cambio:

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

Contenido de actualización:

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

El método de división devuelve exactamente la misma matriz, y el signo [=] le hace una nueva referencia, activando la detección de cambios cada vez que lo necesite. Fácil y legible :)

Saludos,

Deunz
fuente
2

He intentado todas las soluciones mencionadas aquí, pero por alguna razón ngOnChanges()todavía no me ha funcionado. Entonces lo llamé this.ngOnChanges()después de llamar al servicio que repobla mis matrices y funcionó ... ¿correcto? Probablemente no. ¿Ordenado? diablos no. ¿Trabajos? ¡si!

Yazan Khalaileh
fuente
1

ok, entonces mi solución para esto fue:

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

Y esto me dispara ngOnChanges

DanielWaw
fuente
0

Tuve que crear un truco para ello.

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
fuente
0

En mi caso, fueron los cambios en el valor del objeto lo que ngOnChangeno estaba capturando. Algunos valores de objeto se modifican en respuesta a una llamada api. Reinicializar el objeto solucionó el problema y provocó ngOnChangeque se activara en el componente secundario.

Algo como

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