Detecção de alteração Angular2: ngOnChanges não dispara para objeto aninhado

129

Sei que não sou o primeiro a perguntar sobre isso, mas não consigo encontrar uma resposta nas perguntas anteriores. Eu tenho isso em um componente

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

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

No controlador rawLapsdataé alterado de tempos em tempos.

Em laps, os dados são enviados como HTML em um formato tabular. Isso muda sempre que rawLapsdatamuda.

Meu mapcomponente precisa usar ngOnChangescomo gatilho para redesenhar marcadores em um mapa do Google. O problema é que o ngOnChanges não é acionado quando as rawLapsDataalterações no pai. O que eu posso fazer?

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

Atualização: ngOnChanges não está funcionando, mas parece que lapsData está sendo atualizado. No ngInit, há um ouvinte de eventos para alterações de zoom que também chama this.drawmarkers. Quando mudo o zoom, vejo de fato uma mudança nos marcadores. Portanto, o único problema é que não recebo a notificação no momento em que os dados de entrada são alterados.

No pai, eu tenho essa linha. (Lembre-se de que a mudança é refletida em voltas, mas não no mapa).

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

E observe que this.rawLapsDataele próprio é um ponteiro para o meio de um objeto json grande

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Simon H
fonte
Seu código não mostra como os dados são atualizados ou que tipo são. Uma nova instância foi atribuída ou apenas uma propriedade do valor foi modificada?
Günter Zöchbauer
@ GünterZöchbauer Adicionei a linha do componente pai
Simon H
Eu acho que envolver esta linha zone.run(...)deve fazê-lo então.
Günter Zöchbauer
1
Sua matriz (referência) não está mudando, portanto ngOnChanges()não será chamada. Você pode usar ngDoCheck()e implementar sua própria lógica para determinar se o conteúdo da matriz foi alterado. lapsDataé atualizado porque possui / é uma referência à mesma matriz que rawLapsData.
Mark Rajcok
1
1) No componente voltas, seu código / modelo passa por cima de cada entrada na matriz lapsData e exibe o conteúdo, para que haja ligações angulares em cada parte de dados exibida. 2) Mesmo que o Angular não detecte nenhuma alteração (verificação de referência) nas propriedades de entrada de um componente, ele ainda (por padrão) verifica todas as ligações do modelo. É assim que as voltas pegam as mudanças. 3) O componente de mapas provavelmente não possui ligações em seu modelo para sua propriedade de entrada lapsData, certo? Isso explicaria a diferença.
Mark Rajcok

Respostas:

153

rawLapsData continua a apontar para a mesma matriz, mesmo se você modificar o conteúdo da matriz (por exemplo, adicionar itens, remover itens, alterar um item).

Durante a detecção de alterações, quando o Angular verifica as propriedades de entrada dos componentes em busca de alterações, ele usa (essencialmente) ===uma verificação suja. Para matrizes, isso significa que as referências da matriz (apenas) estão marcadas suja. Como a rawLapsDatareferência da matriz não está mudando, ngOnChanges()não será chamada.

Posso pensar em duas soluções possíveis:

  1. Implemente ngDoCheck()e execute sua própria lógica de detecção de alterações para determinar se o conteúdo da matriz foi alterado. (O documento Ganchos do ciclo de vida tem um exemplo .)

  2. Atribua uma nova matriz rawLapsDatasempre que fizer alterações no conteúdo da matriz. Em seguida ngOnChanges(), será chamado porque o array (referência) aparecerá como uma alteração.

Na sua resposta, você encontrou outra solução.

Repetindo alguns comentários aqui no OP:

Eu ainda não vejo como lapspode pegar a mudança (certamente ela deve estar usando algo equivalente a ngOnChanges()si mesma?) Enquanto mapnão pode.

  • No lapscomponente, seu código / modelo faz um loop sobre cada entrada na lapsDatamatriz e exibe o conteúdo, para que haja ligações Angulares em cada parte dos dados que são exibidos.
  • Mesmo quando o Angular não detecta nenhuma alteração nas propriedades de entrada de um componente (usando a ===verificação), ele continua (por padrão) sujo verifica todas as ligações do modelo. Quando alguma dessas alterações for alterada, o Angular atualizará o DOM. É isso que você está vendo.
  • O mapscomponente provavelmente não possui nenhuma ligação em seu modelo para sua lapsDatapropriedade de entrada, certo? Isso explicaria a diferença.

Observe que, lapsDatanos dois componentes e rawLapsDatano componente pai, todos apontam para a mesma / uma matriz. Portanto, mesmo que o Angular não observe nenhuma alteração (de referência) nas lapsDatapropriedades de entrada, os componentes "obtêm" / veem qualquer alteração no conteúdo da matriz porque todos compartilham / referenciam essa matriz. Não precisamos do Angular para propagar essas alterações, como faria com um tipo primitivo (string, número, booleano). Mas com um tipo primitivo, qualquer alteração no valor sempre acionariangOnChanges() - o que é algo que você explora em sua resposta / solução.

Como você provavelmente já descobriu, as propriedades de entrada do objeto têm o mesmo comportamento das propriedades de entrada da matriz.

Mark Rajcok
fonte
Sim, eu estava mudando um objeto profundamente aninhado, e acho que isso dificultou para a Angular detectar alterações na estrutura. Não a minha preferência de sofrer mutações, mas isso é traduzido XML e eu não posso dar ao luxo de perder qualquer um dos dados circundante como eu quero recriar o xml novamente no final
Simon H
7
@SimonH, "difícil para o Angular detectar alterações na estrutura" - Só para deixar claro, o Angular nem mesmo olha dentro das propriedades de entrada que são matrizes ou objetos para alterações. Ele só procura ver se o valor foi alterado - para objetos e matrizes, o valor é a referência. Para tipos primitivos, o valor é ... o valor. (Não estou certo que eu tenho todo o jargão reta, mas você começa a idéia.)
Mark Rajcok
11
Ótima resposta. A equipe do Angular2 precisa desesperadamente publicar um documento detalhado e oficial sobre os internos da detecção de alterações.
Se eu fizer a funcionalidade no doCheck, no meu caso, a verificação do do chama tantas vezes. Você pode me dizer de outra maneira?
precisa saber é o seguinte
@MarkRajcok você pode me ajudar a resolver este problema stackoverflow.com/questions/50166996/...
Nikson
27

Não é a abordagem mais limpa, mas você pode clonar o objeto sempre que alterar o valor?

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

Acho que prefiro essa abordagem ao ngDoCheck()invés de implementar a sua própria, mas talvez alguém como a @ GünterZöchbauer possa entrar na conversa.

David
fonte
Se você não tem certeza se um navegador alvejado apoiaria <Object.assign ()>, você também pode stringify em uma string JSON e volta parse para JSON, que também irá criar um novo objeto ...
Guntram
1
@ Guntram Ou um polyfill?
David
12

No caso de matrizes, você pode fazer assim:

No .tsarquivo (componente pai) em que você está atualizando, rawLapsDatafaça o seguinte:

rawLapsData = somevalue; // change detection will not happen

Solução:

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

e ngOnChangesserá chamado no componente filho

Dullu dinamarquês
fonte
10

Como uma extensão da segunda solução de Mark Rajcok

Designe uma nova matriz para rawLapsData sempre que fizer alterações no conteúdo da matriz. Então ngOnChanges () será chamado porque a matriz (referência) aparecerá como uma alteração

você pode clonar o conteúdo da matriz assim:

rawLapsData = rawLapsData.slice(0);

Eu estou mencionando isso porque

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

não funcionou para mim. Eu espero que isso ajude.

decebal
fonte
8

Se os dados vierem de uma biblioteca externa, pode ser necessário executar a instrução upate de dados dentro zone.run(...). Injete zone: NgZonepara isso. Se você já pode executar a instanciação da biblioteca externa zone.run(), talvez não seja necessário zone.run()mais tarde.

Günter Zöchbauer
fonte
Como observado nos comentários ao OP, as mudanças não foram externo, mas profundamente dentro de um objeto JSON
Simon H
1
Como sua resposta diz, ainda precisamos executar algo para manter as coisas sincronizadas em Angular2, como angular1 era $scope.$apply?
Pankaj Parkar
1
Se algo for iniciado de fora do Angular, a API corrigida por zona não será usada e o Angular não será notificado sobre possíveis alterações. Sim, zone.runé semelhante a $scope.apply.
Günter Zöchbauer
6

Use ChangeDetectorRef.detectChanges()para informar ao Angular para executar uma detecção de alterações ao editar um objeto aninhado (que está ausente na verificação suja).

Tim Phillips
fonte
Mas como ? Estou tentando usar isso, quero acionar changeDetection em um filho depois que o pai empurrou um novo item de uma maneira bidirecional [(Coleção)].
Deunz 04/10
O problema não é que a detecção de alterações não ocorra, mas essa detecção não detecta as alterações! então isso não parece ser uma solução! por que essa resposta recebeu cinco votos positivos?
Shahryar Saljoughi 26/02
5

Tenho 2 soluções para resolver seu problema

  1. Use ngDoCheckpara detectarobject dados alterados ou não
  2. Atribua objecta um novo endereço de memória object = Object.create(object)do componente pai.
giapnh
fonte
2
Haveria alguma diferença notável entre Object.create (objeto) versus Object.assign ({}, objeto)?
Daniel Galarza
3

Minha solução 'hack' é

   <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 muda ao mesmo tempo que rawLapsData e isso dá ao mapa outra chance de detectar a alteração por meio de um tipo primitivo de objeto mais simples . NÃO é elegante, mas funciona.

Simon H
fonte
Acho difícil acompanhar todas as alterações em vários componentes na sintaxe do modelo, especialmente em aplicativos de médio / grande porte. Normalmente eu uso compartilhado emissor evento e inscrição para passar os dados (solução rápida), ou implementar padrão Redux (via Rx.Subject) para esta (quando não há tempo para plano) ...
Sasxa
3

A detecção de alterações não é acionada quando você altera uma propriedade de um objeto (incluindo objeto aninhado). Uma solução seria reatribuir uma nova referência de objeto usando a função 'lodash' clone ().

import * as _ from 'lodash';

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

Aqui está um truque que me tirou de problemas com este.

Portanto, um cenário semelhante ao OP - eu tenho um componente Angular aninhado que precisa de dados passados ​​para ele, mas a entrada aponta para uma matriz e, como mencionado acima, o Angular não vê uma alteração, pois não examina o conteúdo da matriz.

Portanto, para corrigi-lo, converto a matriz em uma sequência para que o Angular detecte uma alteração e, no componente aninhado, divido (',') a sequência novamente em uma matriz e nos seus dias felizes novamente.

Graham Phillips
fonte
1
Que nojo! A abordagem array.slice (0) é muito mais limpa.
Chris Haines
2

Eu me deparei com a mesma necessidade. E eu li muito sobre isso, aqui está o meu cobre sobre o assunto.

Se você deseja a detecção de alterações por push, você a teria quando alterar o valor de um objeto dentro, certo? E você também o teria se, de alguma forma, remover objetos.

Como já foi dito, o uso de changeDetectionStrategy.onPush

Digamos que você tenha esse componente criado com changeDetectionStrategy.onPush:

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

Em seguida, você pressiona um item e aciona a detecção de alterações:

myCollection.push(anItem);
refresh();

ou você removeria um item e acionaria a detecção de alterações:

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

ou você alteraria um valor de atributo para um item e acionaria a detecção de alterações:

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

Conteúdo da atualização:

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

O método de fatia retorna exatamente a mesma matriz e o sinal [=] faz uma nova referência a ela, acionando a detecção de alterações sempre que você precisar. Fácil e legível :)

Saudações,

Deunz
fonte
2

Eu tentei todas as soluções mencionadas aqui, mas por algum motivo ngOnChanges()ainda não disparou para mim. Então eu liguei this.ngOnChanges()depois de chamar o serviço que repovoa minhas matrizes e funcionou .... correto? provavelmente não. Arrumado? de jeito nenhum. Trabalho? sim!

Yazan Khalaileh
fonte
1

ok, então minha solução para isso foi:

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

E isso me desencadeou ngOnChanges

DanielWaw
fonte
0

Eu tive que criar um hack para isso -

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

No meu caso, foram as alterações no valor do objeto que o ngOnChangenão estava capturando. Alguns valores de objeto são modificados em resposta à chamada da API. A reinicialização do objeto corrigiu o problema e causou ongOnChange acionamento no componente filho.

Algo como

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