Wykrywanie zmian Angular2: ngOnChanges nie jest uruchamiany dla zagnieżdżonego obiektu


134

Wiem, że nie jestem pierwszą, która o to pyta, ale nie mogę znaleźć odpowiedzi w poprzednich pytaniach. Mam to w jednym komponencie

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

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

W kontrolerze rawLapsdataulega od czasu do czasu mutacjom.

W lapsprogramie dane są wyprowadzane jako HTML w formacie tabelarycznym. To zmienia się za każdym razem rawLapsdata.

Mój mapkomponent musi być użyty ngOnChangesjako wyzwalacz do przerysowywania znaczników na Mapie Google. Problem polega na tym, że ngOnChanges nie odpala przy rawLapsDatazmianach w rodzicu. Co mogę zrobić?

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

Aktualizacja: ngOnChanges nie działa, ale wygląda na to, że lapsData jest aktualizowany. W ngInit jest odbiornikiem zdarzeń dla zmian zoomu, który również wywołuje this.drawmarkers. Kiedy zmieniam zoom, rzeczywiście widzę zmianę w znacznikach. Jedynym problemem jest to, że nie otrzymuję powiadomienia w momencie zmiany danych wejściowych.

W rodzicu mam tę linię. (Przypomnij sobie, że zmiana jest odzwierciedlona w okrążeniach, ale nie na mapie).

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

Zauważ, że this.rawLapsDatasam jest wskaźnikiem do środka dużego obiektu json

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;

Twój kod nie pokazuje, w jaki sposób dane są aktualizowane ani jakiego typu są dane. Czy przypisano nową instancję, czy jest to tylko właściwość zmodyfikowanej wartości?
Günter Zöchbauer

@ GünterZöchbauer Dodałem linię z komponentu nadrzędnego
Simon H

Myślę, że zawinięcie tej linii zone.run(...)powinno to zrobić.
Günter Zöchbauer

1
Twoja tablica (odniesienie) się nie zmienia, więc ngOnChanges()nie zostanie wywołana. Możesz użyć ngDoCheck()i zaimplementować własną logikę, aby określić, czy zawartość tablicy uległa zmianie. lapsDatajest aktualizowany, ponieważ ma / jest odniesieniem do tej samej tablicy co rawLapsData.
Mark Rajcok

1
1) W komponencie laps Twój kod / szablon zapętla każdy wpis w tablicy lapsData i wyświetla zawartość, więc na każdym wyświetlanym fragmencie danych istnieją powiązania kątowe. 2) Nawet jeśli Angular nie wykryje żadnych zmian (sprawdzania referencji) we właściwościach wejściowych komponentu, nadal (domyślnie) sprawdza wszystkie powiązania szablonu. Tak właśnie zmieniają się okrążenia. 3) Komponent mapy prawdopodobnie nie ma żadnych powiązań w swoim szablonie z właściwością wejściową lapsData, prawda? To wyjaśniałoby różnicę.
Mark Rajcok,

Odpowiedzi:


157

rawLapsData nadal wskazuje tę samą tablicę, nawet jeśli zmodyfikujesz zawartość tablicy (np. dodasz elementy, usuniesz elementy, zmienisz pozycję).

Podczas wykrywania zmian, gdy Angular sprawdza właściwości wejściowe komponentów pod kątem zmiany, używa (zasadniczo) ===do sprawdzania brudnego. W przypadku tablic oznacza to, że odwołania do tablic (tylko) są sprawdzane na brudno. Ponieważ rawLapsDataodwołanie do tablicy się nie zmienia, ngOnChanges()nie zostanie wywołane.

Przychodzą mi do głowy dwa możliwe rozwiązania:

  1. Zaimplementuj ngDoCheck()i przeprowadź własną logikę wykrywania zmian, aby określić, czy zawartość tablicy uległa zmianie. (Dokument Lifecycle Hooks zawiera przykład ).

  2. Przypisz nową tablicę do rawLapsDatakażdej zmiany zawartości tablicy. Następnie ngOnChanges()zostanie wywołana, ponieważ tablica (odniesienie) pojawi się jako zmiana.

W swojej odpowiedzi podałeś inne rozwiązanie.

Powtarzam tutaj kilka komentarzy na temat PO:

Nadal nie rozumiem, jak lapsmogę odebrać zmianę (na pewno musi używać czegoś równoważnego ngOnChanges()sobie?), Podczas gdy mapnie mogę.

  • W lapskomponencie twój kod / szablon zapętla każdy wpis w lapsDatatablicy i wyświetla zawartość, więc na każdym wyświetlanym fragmencie danych istnieją powiązania kątowe.
  • Nawet jeśli Angular nie wykrywa żadnych zmian we właściwościach wejściowych składnika (przy użyciu ===sprawdzania), nadal (domyślnie) po brudnym sprawdzaniu wszystkich powiązań szablonu. Kiedy któraś z tych zmian się zmieni, Angular zaktualizuje DOM. To właśnie widzisz.
  • mapsKomponent prawdopodobnie nie ma żadnych powiązań w swoim szablonie do jego lapsDatawłasności wejściowego, prawda? To wyjaśniałoby różnicę.

Zauważ, że lapsDataw obu komponentach iw komponencie rawLapsDatamacierzystym wszystkie wskazują na tę samą / jedną tablicę. Więc nawet jeśli Angular nie zauważa żadnych (referencyjnych) zmian we lapsDatawłaściwościach wejściowych, komponenty „pobierają” / widzą zmiany zawartości tablicy, ponieważ wszystkie współużytkują / odwołują się do tej jednej tablicy. Nie potrzebujemy Angulara do propagowania tych zmian, tak jak w przypadku typu pierwotnego (string, number, boolean). Ale w przypadku typu prymitywnego każda zmiana wartości zawsze by się uruchomiła ngOnChanges()- co jest czymś, co wykorzystujesz w swojej odpowiedzi / rozwiązaniu.

Jak już zapewne wiesz, właściwości wejściowe obiektu zachowują się tak samo, jak właściwości wejściowe tablicy.


Tak, mutowałem głęboko zagnieżdżony obiekt i myślę, że to utrudniało Angularowi dostrzeżenie zmian w strukturze. Nie preferuję mutacji, ale to jest przetłumaczone na XML i nie mogę sobie pozwolić na utratę jakichkolwiek otaczających danych, ponieważ chcę ponownie odtworzyć xml na końcu
Simon H

8
@SimonH, „trudno Angularowi wykryć zmiany w strukturze” - żeby było jasne, Angular nie zagląda nawet do właściwości wejściowych, które są tablicami lub obiektami pod kątem zmian. Sprawdza tylko, czy wartość uległa zmianie - w przypadku obiektów i tablic wartość jest odniesieniem. W przypadku typów pierwotnych wartością jest ... wartość. (Nie jestem pewien, czy rozumiem cały żargon, ale masz pomysł.)
Mark Rajcok

14
Świetna odpowiedź. Zespół Angular2 desperacko potrzebuje opublikowania szczegółowego, autorytatywnego dokumentu na temat wewnętrznych elementów wykrywania zmian.

Jeśli wykonam funkcjonalność w doCheck, w moim przypadku funkcja do check wywołuje tyle razy. Czy możesz mi powiedzieć inny sposób?
Mr_Perfect

@MarkRajcok, czy możesz mi pomóc rozwiązać ten problem stackoverflow.com/questions/50166996/ ...
Nikson

30

Nie jest to najczystsze podejście, ale możesz po prostu sklonować obiekt za każdym razem, gdy zmieniasz wartość?

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

Myślę, że wolałbym to podejście niż wdrażanie własnego, ngDoCheck()ale może ktoś taki jak @ GünterZöchbauer mógłby się wtrącić.


Jeśli nie masz pewności, czy docelowa przeglądarka obsługuje <Object. assign ()>, możesz również wpisać ciąg json i przeanalizować go z powrotem do json, co spowoduje również utworzenie nowego obiektu ...
Guntram

1
@Guntram czy polyfill?
David,

13

W przypadku tablic można to zrobić w następujący sposób:

W .tspliku (komponent nadrzędny), w którym aktualizujesz, rawLapsDatazrób to w ten sposób:

rawLapsData = somevalue; // change detection will not happen

Rozwiązanie:

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

i ngOnChangeswywoła komponent potomny


12

Jako rozszerzenie drugiego rozwiązania Marka Rajcoka

Przypisz nową tablicę do rawLapsData za każdym razem, gdy wprowadzisz jakiekolwiek zmiany w zawartości tablicy. Następnie zostanie wywołana ngOnChanges (), ponieważ tablica (odniesienie) pojawi się jako zmiana

możesz sklonować zawartość tablicy w następujący sposób:

rawLapsData = rawLapsData.slice(0);

Wspominam o tym, ponieważ

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

nie działa dla mnie. Mam nadzieję, że to pomoże.


8

Jeśli dane pochodzą z biblioteki zewnętrznej, może być konieczne uruchomienie w niej instrukcji aktualizacji danych zone.run(...). Wstrzyknij zone: NgZoneza to. Jeśli możesz już uruchomić instancję biblioteki zewnętrznej zone.run(), możesz zone.run()później nie potrzebować .


Jak zauważono w komentarzach do PO, zmiany nie były zewnętrzne, ale głęboko w obiekcie json
Simon H

1
Jak mówi twoja odpowiedź, nadal musimy uruchomić coś, co zapewni synchronizację w Angular2, tak jak to było w angular1 $scope.$apply?
Pankaj Parkar

1
Jeśli coś jest uruchamiane spoza Angulara, API poprawiane przez strefę nie jest używane, a Angular nie jest powiadamiany o możliwych zmianach. Tak, zone.runjest podobny do $scope.apply.
Günter Zöchbauer

6

Służy ChangeDetectorRef.detectChanges()do mówienia Angularowi, aby uruchomił wykrywanie zmian podczas edycji zagnieżdżonego obiektu (którego pomija z brudnym sprawdzaniem).


Ale jak ? Próbuję tego użyć, chcę wyzwolić changeDetection u dziecka po tym, jak rodzic wypchnął nowy element w 2 sposób ograniczony [(Collection)].
Deunz

Problem nie polega na tym, że wykrywanie zmian nie występuje, ale to wykrywanie zmian nie wykrywa zmian! więc to nie wydaje się być rozwiązaniem! dlaczego ta odpowiedź otrzymała pięć głosów za?
Shahryar Saljoughi

5

Mam 2 rozwiązania, aby rozwiązać twój problem

  1. Służy ngDoCheckdo wykrywania objectzmian lub niezmienionych danych
  2. Przypisz objectdo nowego adresu pamięci object = Object.create(object)z komponentu nadrzędnego.

2
Czy byłaby jakaś zauważalna różnica między Object.create (object) a Object. assign ({}, object)?
Daniel Galarza

4

Wykrywanie zmian nie jest wyzwalane po zmianie właściwości obiektu (w tym obiektu zagnieżdżonego). Jednym z rozwiązań byłoby ponowne przypisanie nowego odniesienia do obiektu za pomocą funkcji clone () „lodash”.

import * as _ from 'lodash';

this.foo = _.clone(this.foo);

3

Moje rozwiązanie „hack” to

   <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 zmienia się w tym samym czasie co rawLapsData, co daje mapie kolejną szansę na wykrycie zmiany za pomocą prostszego typu obiektu pierwotnego. NIE jest elegancki, ale działa.


Trudno mi śledzić wszystkie zmiany w różnych składnikach składni szablonów, szczególnie w aplikacjach średniej / dużej skali. Zwykle używam współdzielonego emitera zdarzeń i subskrypcji do przekazywania danych (szybkie rozwiązanie) lub implementuję do tego wzorzec Redux (przez Rx.Subject) (kiedy jest czas na planowanie) ...
Sasxa

2

Oto hack, który właśnie wyciągnął mnie z kłopotów z tym.

A więc podobny scenariusz do OP - mam zagnieżdżony komponent Angular, który wymaga przesłania danych, ale dane wejściowe wskazują na tablicę i jak wspomniano powyżej, Angular nie widzi zmiany, ponieważ nie bada zawartość tablicy.

Aby to naprawić, konwertuję tablicę na ciąg znaków, aby Angular wykrył zmianę, a następnie w zagnieżdżonym komponencie dzielę (',') ciąg z powrotem na tablicę i znowu szczęśliwe dni.


2
Fuj! Podejście array.slice (0) jest znacznie czystsze.
Chris Haines

Dobry hack. Ale czy nie będzie to problem z wydajnością?
Kavinda Jayakody

2

Natknąłem się na tę samą potrzebę. Dużo czytam na ten temat, więc oto moja miedź na ten temat.

Jeśli chcesz, aby wykrywanie zmian było wykonywane przy wypychaniu, to miałbyś je mieć, gdy zmieniasz wartość obiektu w środku, prawda? I miałbyś to również, gdybyś w jakiś sposób usuwał przedmioty.

Jak już wspomniano, użycie changeDetectionStrategy.onPush

Powiedzmy, że masz ten komponent, który utworzyłeś za pomocą changeDetectionStrategy.onPush:

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

Następnie popchnąłbyś element i wywołał wykrywanie zmiany:

myCollection.push(anItem);
refresh();

lub możesz usunąć element i wywołać wykrywanie zmian:

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

albo zmienisz wartość atrybutu elementu i uruchomisz wykrywanie zmiany:

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

Treść odświeżenia:

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

Metoda slice zwraca dokładnie to samo Array, a znak [=] tworzy do niej nowe odniesienie, wyzwalając wykrywanie zmiany za każdym razem, gdy jest to potrzebne. Łatwe i czytelne :)

Pozdrowienia,


2

Wypróbowałem wszystkie wymienione tutaj rozwiązania, ale z jakiegoś powodu ngOnChanges()nadal nie odpaliłem. Więc zadzwoniłem z this.ngOnChanges()po wywołaniu usługi, która ponownie zapełnia moje tablice i zadziałało ... prawda? prawdopodobnie nie. Schludny? cholera nie. Pracuje? tak!


1

ok, więc moim rozwiązaniem było:

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

I to wyzwala mnie ngOnChanges


0

Musiałem stworzyć do tego hack -

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

0

W moim przypadku były to zmiany wartości obiektu, których ngOnChangenie przechwytywano. Kilka wartości obiektów jest modyfikowanych w odpowiedzi na wywołanie interfejsu API. Ponowne ngOnChangezainicjowanie obiektu rozwiązało problem i spowodowało wyzwolenie elementu podrzędnego.

Coś jak

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

0

Kiedy manipulujesz danymi, takimi jak:

this.data.profiles[i].icon.url = '';

Następnie powinieneś użyć, aby wykryć zmiany:

let array = this.data.profiles.map(x => Object.assign({}, x)); // It will detect changes

Ponieważ kątowe ngOnchanges nie są w stanie wykryć zmian w szyku, obiektach, musimy przypisać nowe odniesienie. Działa za każdym razem!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.