Deteksi perubahan angular2: ngOnChanges tidak menembak untuk objek bersarang

129

Saya tahu saya bukan yang pertama bertanya tentang hal ini, tetapi saya tidak dapat menemukan jawaban dalam pertanyaan sebelumnya. Saya memiliki ini dalam satu komponen

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

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

Dalam controller rawLapsdataakan dimutasi dari waktu ke waktu.

Dalam laps, data adalah output sebagai HTML dalam format tabel. Ini berubah setiap kali rawLapsdataberubah.

mapKomponen saya perlu digunakan ngOnChangessebagai pemicu untuk menggambar ulang marker di Google Map. Masalahnya adalah ngOnChanges tidak menyala ketika ada rawLapsDataperubahan pada induknya. Apa yang dapat saya?

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

Pembaruan: ngOnChanges tidak berfungsi, tetapi sepertinya lapsData sedang diperbarui. Di ngInit adalah pendengar acara untuk perubahan zoom yang juga memanggil this.drawmarkers. Ketika saya mengubah zoom, saya memang melihat perubahan pada marker. Jadi satu-satunya masalah adalah saya tidak mendapatkan notifikasi pada saat input data berubah.

Pada orang tua, saya memiliki baris ini. (Ingat bahwa perubahan tercermin dalam putaran, tetapi tidak di peta).

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

Dan perhatikan bahwa this.rawLapsDataitu sendiri adalah pointer ke tengah objek json besar

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
Simon H
sumber
Kode Anda tidak menunjukkan bagaimana data diperbarui atau jenis data apa. Apakah instance baru ditetapkan atau hanya properti dari nilai yang dimodifikasi?
Günter Zöchbauer
@ GünterZöchbauer Saya menambahkan baris dari komponen induk
Simon H
Saya kira membungkus baris ini zone.run(...)harus dilakukan kemudian.
Günter Zöchbauer
1
Array Anda (referensi) tidak berubah, jadi ngOnChanges()tidak akan dipanggil. Anda dapat menggunakan ngDoCheck()dan mengimplementasikan logika Anda sendiri untuk menentukan apakah isi array telah berubah. lapsDatadiperbarui karena memiliki / merupakan referensi ke array yang sama dengan rawLapsData.
Mark Rajcok
1
1) Dalam komponen lap kode Anda / template loop atas setiap entri dalam array lapsData, dan menampilkan konten, sehingga ada binding sudut pada setiap bagian data yang ditampilkan. 2) Bahkan jika Angular tidak mendeteksi perubahan apa pun (pengecekan referensi) ke properti input komponen, tetap (secara default) memeriksa semua binding templat. Begitulah cara lap mengambil perubahan. 3) Komponen peta kemungkinan tidak memiliki binding di templatnya ke properti input lapsData, kan? Itu akan menjelaskan perbedaannya.
Mark Rajcok

Jawaban:

153

rawLapsData terus menunjuk ke array yang sama, bahkan jika Anda memodifikasi konten array (misalnya, menambah item, menghapus item, mengubah item).

Selama deteksi perubahan, ketika Angular memeriksa properti input komponen untuk perubahan, ia menggunakan (pada dasarnya) ===untuk pemeriksaan kotor. Untuk array, ini berarti referensi array (hanya) kotor diperiksa. Karena rawLapsDatareferensi array tidak berubah, ngOnChanges()tidak akan dipanggil.

Saya dapat memikirkan dua solusi yang mungkin:

  1. Menerapkan ngDoCheck()dan melakukan logika deteksi perubahan Anda sendiri untuk menentukan apakah isi array telah berubah. (Doc Lifecycle Hooks memiliki contoh .)

  2. Tetapkan array baru rawLapsDatasetiap kali Anda membuat perubahan pada konten array. Maka ngOnChanges()akan dipanggil karena array (referensi) akan muncul sebagai perubahan.

Dalam jawaban Anda, Anda menemukan solusi lain.

Mengulangi beberapa komentar di sini di OP:

Saya masih tidak melihat bagaimana lapsbisa mengambil perubahan (pasti itu harus menggunakan sesuatu yang setara dengan ngOnChanges()dirinya sendiri?) Sementara maptidak bisa.

  • Dalam lapskomponen, kode / templat Anda berulang-ulang melewati setiap entri dalam lapsDatalarik, dan menampilkan konten, sehingga ada ikatan sudut pada setiap bagian data yang ditampilkan.
  • Bahkan ketika Angular tidak mendeteksi perubahan apa pun pada properti input komponen (menggunakan ===pengecekan), itu tetap (secara default) kotor memeriksa semua binding templat. Ketika ada yang berubah, Angular akan memperbarui DOM. Itu yang kamu lihat.
  • The mapskomponen mungkin tidak memiliki binding dalam template untuk nya lapsDataproperti masukan, kan? Itu akan menjelaskan perbedaannya.

Perhatikan bahwa lapsDatadi kedua komponen dan rawLapsDatadi komponen induk semua mengarah ke array yang sama / satu. Jadi meskipun Angular tidak melihat (referensi) perubahan pada lapsDataproperti input, komponen "dapatkan" / lihat perubahan isi array karena semuanya berbagi / referensi bahwa satu array. Kami tidak membutuhkan Angular untuk menyebarkan perubahan ini, seperti halnya dengan tipe primitif (string, angka, boolean). Tetapi dengan tipe primitif, setiap perubahan nilainya akan selalu memicu ngOnChanges()- yang merupakan sesuatu yang Anda manfaatkan dalam jawaban / solusi Anda.

Seperti yang Anda ketahui sekarang properti input objek memiliki perilaku yang sama dengan properti input array.

Mark Rajcok
sumber
Ya, saya bermutasi objek yang sangat bersarang, dan saya kira itu menyulitkan Angular untuk menemukan perubahan dalam struktur. Bukan preferensi saya untuk bermutasi tetapi ini diterjemahkan XML dan saya tidak dapat kehilangan data di sekitarnya karena saya ingin membuat ulang xml lagi di akhir
Simon H
7
@SimonH, "sulit bagi Angular untuk melihat perubahan dalam struktur" - Hanya untuk memperjelas, Angular bahkan tidak melihat ke dalam properti input yang merupakan array atau objek untuk perubahan. Itu hanya terlihat untuk melihat apakah nilainya berubah - untuk objek dan array, nilainya adalah referensi. Untuk tipe primitif, nilainya adalah ... nilainya. (Saya tidak yakin saya memiliki semua jargon langsung, tetapi Anda mendapatkan ide.)
Mark Rajcok
11
Jawaban yang bagus Tim Angular2 sangat perlu menerbitkan dokumen yang terperinci dan otoritatif tentang internal deteksi perubahan.
Jika saya melakukan fungsionalitas di doCheck, dalam kasus saya cek lakukan menelepon berkali-kali. Bisakah Anda memberi tahu saya cara lain?
Mr_Perfect
@MarkRajcok dapat tolong bantu saya untuk mengatasi masalah ini stackoverflow.com/questions/50166996/…
Nikson
27

Bukan pendekatan terbersih, tetapi Anda hanya bisa mengkloning objek setiap kali Anda mengubah nilainya?

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

Saya pikir saya lebih suka pendekatan ini daripada menerapkan Anda sendiri ngDoCheck()tetapi mungkin seseorang seperti @ GünterZöchbauer bisa berpadu.

David
sumber
Jika Anda tidak yakin apakah browser yang ditargetkan akan mendukung <Object.assign ()>, Anda juga dapat merangkai menjadi string json dan mengurai kembali ke json, yang juga akan membuat objek baru ...
Guntram
1
@Guntram Atau polyfill?
David
12

Dalam Case of Arrays Anda dapat melakukannya seperti ini:

Dalam .tsfile (komponen Induk) tempat Anda memperbarui, rawLapsDatalakukan seperti ini:

rawLapsData = somevalue; // change detection will not happen

Larutan:

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

dan ngOnChangesakan memanggil komponen anak

Dullu Denmark
sumber
10

Sebagai perpanjangan dari solusi kedua Mark Rajcok

Tetapkan larik baru ke rawLapsData setiap kali Anda membuat perubahan pada isi larik. Kemudian ngOnChanges () akan dipanggil karena array (referensi) akan muncul sebagai perubahan

Anda dapat mengkloning isi array seperti ini:

rawLapsData = rawLapsData.slice(0);

Saya menyebutkan ini karena

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

tidak bekerja untuk saya. Saya harap ini membantu.

decebal
sumber
8

Jika data berasal dari perpustakaan eksternal Anda mungkin perlu menjalankan pernyataan data upate dalam zone.run(...). Suntikkan zone: NgZoneuntuk ini. Jika Anda dapat menjalankan instantiation dari perpustakaan eksternal di dalam zone.run()sudah, maka Anda mungkin tidak perlu zone.run()nanti.

Günter Zöchbauer
sumber
Sebagaimana dicatat dalam komentar untuk OP, perubahan itu tidak eksternal tetapi jauh di dalam objek json
Simon H
1
Seperti jawaban Anda mengatakan, masih kita perlu menjalankan sesuatu untuk menjaga sinkronisasi di Angular2, seperti angular1 itu $scope.$apply?
Pankaj Parkar
1
Jika sesuatu dimulai dari luar Angular, API, yang ditambal oleh zona tidak digunakan dan Angular tidak diberitahu tentang kemungkinan perubahan. Ya, zone.runmirip dengan $scope.apply.
Günter Zöchbauer
6

Gunakan ChangeDetectorRef.detectChanges()untuk memberi tahu Angular untuk menjalankan deteksi perubahan saat Anda mengedit objek yang bersarang (yang terlewatkan dengan pemeriksaan kotornya).

Tim Phillips
sumber
Tapi bagaimana caranya ? Saya mencoba menggunakan ini, saya ingin memicu changeDetection pada anak setelah orang tua mendorong item baru dengan 2 cara terikat [(Koleksi)].
Deunz
Masalahnya bukan bahwa deteksi perubahan tidak terjadi, tetapi deteksi perubahan itu tidak mendeteksi perubahan! jadi ini sepertinya bukan solusi! mengapa jawaban ini mendapat lima upvotes?
Shahryar Saljoughi
5

Saya punya 2 solusi untuk menyelesaikan masalah Anda

  1. Gunakan ngDoCheckuntuk mendeteksi objectdata yang diubah atau tidak
  2. Tetapkan objectke alamat memori baru dengan object = Object.create(object)dari komponen induk.
giapnh
sumber
2
Apakah akan ada perbedaan penting antara Object.create (objek) versus Object.assign ({}, objek)?
Daniel Galarza
3

Solusi 'hack' saya adalah

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

perubahan yang dipilih pada saat yang sama dengan rawLapsData dan yang memberikan peta kesempatan lain untuk mendeteksi perubahan melalui tipe primitif objek yang lebih sederhana . BUKAN elegan, tapi berhasil.

Simon H
sumber
Saya merasa sulit untuk melacak semua perubahan pada berbagai komponen dalam sintaks template, khususnya pada aplikasi skala menengah / besar. Biasanya saya menggunakan emitor acara bersama dan berlangganan untuk meneruskan data (solusi cepat), atau menerapkan pola Redux (melalui Rx.Subject) untuk ini (ketika ada waktu untuk merencanakan) ...
Sasxa
3

Ubah deteksi tidak dipicu saat Anda mengubah properti objek (termasuk objek bersarang). Salah satu solusinya adalah dengan menetapkan kembali referensi objek baru menggunakan fungsi 'lodash' clone ().

import * as _ from 'lodash';

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

Ini hack yang baru saja membuat saya keluar dari masalah dengan yang ini.

Jadi skenario yang mirip dengan OP - Saya punya komponen Angular bersarang yang membutuhkan data yang diturunkan ke sana, tetapi input menunjuk ke sebuah array, dan seperti yang disebutkan di atas, Angular tidak melihat perubahan karena tidak memeriksa isi dari array.

Jadi untuk memperbaikinya saya mengubah array ke string untuk Angular untuk mendeteksi perubahan, dan kemudian di komponen bersarang saya membagi (',') string kembali ke array dan hari-hari bahagia lagi.

Graham Phillips
sumber
1
Huek! Pendekatan array.slice (0) jauh lebih bersih.
Chris Haines
2

Saya menemukan kebutuhan yang sama. Dan saya banyak membaca tentang ini, inilah tembaga saya tentang masalah ini.

Jika Anda ingin deteksi perubahan Anda saat ditekan, maka Anda akan memilikinya ketika Anda mengubah nilai objek di dalam kan? Dan Anda juga akan memilikinya jika entah bagaimana, Anda menghapus objek.

Seperti yang sudah dikatakan, gunakan changeDetectionStrategy.onPush

Katakanlah Anda memiliki komponen yang Anda buat ini, dengan changeDetectionStrategy.onPush:

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

Maka Anda akan mendorong item dan memicu deteksi perubahan:

myCollection.push(anItem);
refresh();

atau Anda akan menghapus item dan memicu deteksi perubahan:

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

atau Anda akan mengubah nilai attrbibute untuk suatu item dan memicu deteksi perubahan:

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

Konten penyegaran:

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

Metode slice mengembalikan Array yang sama persis, dan tanda [=] membuat referensi baru untuk itu, memicu deteksi perubahan setiap kali Anda membutuhkannya. Mudah dan mudah dibaca :)

Salam,

Deunz
sumber
2

Saya telah mencoba semua solusi yang disebutkan di sini, tetapi untuk beberapa alasan ngOnChanges()masih tidak memecat saya. Jadi saya menyebutnya this.ngOnChanges()setelah memanggil layanan yang mengisi ulang array saya dan berfungsi .... benar? mungkin tidak. Rapi? tidak. Bekerja? Iya!

Yazan Khalaileh
sumber
1

ok jadi solusi saya untuk ini adalah:

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

Dan ini memicu saya ngOnChanges

DanielWaw
sumber
0

Saya harus membuat retasan untuk itu -

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

Dalam kasus saya itu adalah perubahan nilai objek yang ngOnChangetidak ditangkap. Beberapa nilai objek dimodifikasi sebagai respons panggilan api. Inisialisasi ulang objek memperbaiki masalah dan menyebabkan ngOnChangepemicu di komponen anak.

Sesuatu seperti

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