Bagaimana cara mendeteksi ketika nilai @Input () berubah di Angular?

471

Saya memiliki komponen induk ( CategoryComponent ), komponen anak ( videoListComponent ) dan ApiService.

Saya memiliki sebagian besar ini berfungsi dengan baik yaitu setiap komponen dapat mengakses json api dan mendapatkan data yang relevan melalui yang bisa diamati.

Saat ini komponen daftar video baru saja mendapatkan semua video, saya ingin memfilter ini untuk hanya video dalam kategori tertentu, saya mencapai ini dengan meneruskan kategoriId ke anak melalui @Input().

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

Ini berfungsi dan ketika kategori induk CategoryComponent berubah maka nilai CategoryId akan dilewati, @Input()tetapi saya kemudian perlu mendeteksi ini di VideoListComponent dan meminta kembali array video melalui APIService (dengan CategoryId baru).

Di AngularJS saya akan melakukan $watchpada variabel. Apa cara terbaik untuk menangani ini?

Jon Catmull
sumber

Jawaban:

720

Sebenarnya, ada dua cara untuk mendeteksi dan bertindak ketika input berubah dalam komponen anak di angular2 +:

  1. Anda dapat menggunakan metode siklus hidup ngOnChanges () seperti juga disebutkan dalam jawaban yang lebih lama:
    @Input() categoryId: string;

    ngOnChanges(changes: SimpleChanges) {

        this.doSomething(changes.categoryId.currentValue);
        // You can also use categoryId.previousValue and 
        // categoryId.firstChange for comparing old and new values

    }

Tautan Dokumentasi: ngOnChanges, SimpleChanges, SimpleChange
Demo Contoh: Lihat plunker ini

  1. Sebagai alternatif, Anda juga dapat menggunakan setter properti input sebagai berikut:
    private _categoryId: string;

    @Input() set categoryId(value: string) {

       this._categoryId = value;
       this.doSomething(this._categoryId);

    }

    get categoryId(): string {

        return this._categoryId;

    }

Tautan Dokumentasi: Lihat di sini .

Contoh Demo: Lihat plunker ini .

PENDEKATAN APA YANG HARUS ANDA GUNAKAN?

Jika komponen Anda memiliki beberapa input, maka, jika Anda menggunakan ngOnChanges (), Anda akan mendapatkan semua perubahan untuk semua input sekaligus dalam ngOnChanges (). Dengan menggunakan pendekatan ini, Anda juga dapat membandingkan nilai input saat ini dan sebelumnya yang telah berubah dan mengambil tindakan yang sesuai.

Namun, jika Anda ingin melakukan sesuatu ketika hanya satu input tertentu yang berubah (dan Anda tidak peduli dengan input lainnya), maka mungkin lebih mudah untuk menggunakan setter properti input. Namun, pendekatan ini tidak menyediakan cara bawaan untuk membandingkan nilai sebelumnya dan saat ini dari input yang diubah (yang dapat Anda lakukan dengan mudah dengan metode siklus hidup ngOnChanges).

EDIT 2017-07-25: DETEKSI PERUBAHAN ANGULAR MUNGKIN MASIH TIDAK KEBAKARAN DI BAWAH BEBERAPA SIRKUMSTAN

Biasanya, deteksi perubahan untuk penyetel dan ngOnChanges akan diaktifkan setiap kali komponen induk mengubah data yang dilewatinya kepada anak, asalkan data tersebut adalah tipe data primitif JS (string, number, boolean) . Namun, dalam skenario berikut, itu tidak akan menyala dan Anda harus mengambil tindakan tambahan untuk membuatnya berfungsi.

  1. Jika Anda menggunakan objek atau array bersarang (alih-alih tipe data primitif JS) untuk meneruskan data dari Induk ke Anak, ubah deteksi (menggunakan penyetel atau ngchanges) mungkin tidak diaktifkan, seperti juga disebutkan dalam jawaban oleh pengguna: muetzerich. Untuk solusinya lihat di sini .

  2. Jika Anda memutasikan data di luar konteks sudut (yaitu, secara eksternal), maka sudut tidak akan mengetahui perubahan. Anda mungkin harus menggunakan ChangeDetectorRef atau NgZone di komponen Anda untuk membuat sudut menyadari perubahan eksternal dan karenanya memicu deteksi perubahan. Lihat ini .

Alan CS
sumber
8
Poin yang sangat bagus tentang menggunakan setter dengan input, saya akan memperbarui ini menjadi jawaban yang diterima karena lebih komprehensif. Terima kasih.
Jon Catmull
2
@trichetriche, setter (set method) akan dipanggil kapan pun orang tua mengubah input. Juga, hal yang sama juga berlaku untuk ngOnChanges ().
Alan CS
Yah sepertinya tidak ... Saya akan mencoba mencari tahu ini, mungkin ada sesuatu yang salah yang saya lakukan.
1
Saya baru saja menemukan pertanyaan ini, mencatat bahwa Anda mengatakan menggunakan setter tidak akan memungkinkan untuk membandingkan nilai, namun itu tidak benar, Anda dapat memanggil doSomethingmetode Anda dan mengambil 2 argumen nilai baru dan lama sebelum benar-benar menetapkan nilai baru, dengan cara lain akan menyimpan nilai lama sebelum menetapkan dan memanggil metode setelah itu.
T.oukar
1
@ T.Aoukar Apa yang saya katakan adalah bahwa setter input tidak secara native mendukung membandingkan nilai lama / baru seperti ngOnChanges () tidak. Anda selalu dapat menggunakan peretasan untuk menyimpan nilai lama (seperti yang Anda sebutkan) untuk membandingkannya dengan nilai baru.
Alan CS
105

Gunakan ngOnChanges()metode siklus hidup di komponen Anda.

ngOnChanges dipanggil tepat setelah properti yang terikat data diperiksa dan sebelum tampilan dan konten anak-anak diperiksa jika setidaknya satu dari mereka telah berubah.

Berikut adalah Documents .

muetzerich
sumber
6
Ini berfungsi ketika variabel diatur ke nilai baru mis. MyComponent.myArray = [], Tetapi jika nilai input diubah misalnya MyComponent.myArray.push(newValue), ngOnChanges()tidak dipicu. Ada ide tentang cara menangkap perubahan ini?
Levi Fuller
4
ngOnChanges()tidak dipanggil ketika objek bersarang telah berubah. Mungkin Anda menemukan solusinya di sini
muetzerich
33

Saya mendapatkan kesalahan di konsol serta kompiler dan IDE saat menggunakan SimpleChangestipe pada fungsi signature. Untuk mencegah kesalahan, gunakan anykata kunci dalam tanda tangan sebagai gantinya.

ngOnChanges(changes: any) {
    console.log(changes.myInput.currentValue);
}

EDIT:

Seperti yang ditunjukkan Jon di bawah ini, Anda dapat menggunakan SimpleChangestanda tangan saat menggunakan notasi braket daripada notasi titik.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Darcy
sumber
1
Apakah Anda mengimpor SimpleChangesantarmuka di bagian atas file Anda?
Jon Catmull
@ Jon - Ya. Masalahnya bukan tanda tangan itu sendiri, tetapi mengakses parameter yang ditugaskan saat runtime ke objek SimpleChanges. Sebagai contoh, dalam komponen saya, saya mengikat kelas Pengguna saya ke input (yaitu <my-user-details [user]="selectedUser"></my-user-details>). Properti ini diakses dari kelas SimpleChanges dengan menelepon changes.user.currentValue. Pemberitahuan userterikat pada saat runtime dan bukan bagian dari objek SimpleChanges
Darcy
1
Apakah Anda sudah mencoba ini? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
Jon Catmull
@ Jon - Beralih ke notasi braket berhasil. Saya memperbarui jawaban saya untuk memasukkan itu sebagai alternatif.
Darcy
1
impor {SimpleChanges} dari '@ angular / core'; Jika ada di dokumen sudut, dan bukan jenis naskah asli, maka mungkin dari modul sudut, kemungkinan besar 'sudut / inti'
BentOnCoding
13

Taruhan paling aman adalah menggunakan layanan bersama alih - alih@Input parameter. Juga, @Inputparameter tidak mendeteksi perubahan dalam tipe objek bersarang yang kompleks.

Contoh layanan sederhana adalah sebagai berikut:

Service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SyncService {

    private thread_id = new Subject<number>();
    thread_id$ = this.thread_id.asObservable();

    set_thread_id(thread_id: number) {
        this.thread_id.next(thread_id);
    }

}

Component.ts

export class ConsumerComponent implements OnInit {

  constructor(
    public sync: SyncService
  ) {
     this.sync.thread_id$.subscribe(thread_id => {
          **Process Value Updates Here**
      }
    }

  selectChat(thread_id: number) {  <--- How to update values
    this.sync.set_thread_id(thread_id);
  }
}

Anda dapat menggunakan implementasi yang serupa di komponen lain dan semua komponen Anda akan berbagi nilai bersama yang sama.

Sanket Berde
sumber
1
Sanket terima kasih. Ini adalah poin yang sangat baik untuk dibuat dan jika perlu cara yang lebih baik untuk melakukannya. Saya akan meninggalkan jawaban yang diterima karena tetap menjawab pertanyaan spesifik saya tentang mendeteksi perubahan @ Input.
Jon Catmull
1
Parameter @Input tidak mendeteksi perubahan di dalam tipe objek bersarang yang kompleks, tetapi jika objek itu sendiri berubah, itu akan menyala, kan? misalnya saya memiliki parameter input "parent", jadi jika saya mengubah parent secara keseluruhan, deteksi perubahan akan diaktifkan, tetapi jika saya mengubah parent.name, itu tidak akan terjadi?
yogibimbi
Anda harus memberikan alasan mengapa solusi Anda lebih aman
Joshua Kemmerer
1
@InputParameter @JoshuaKemmerer tidak mendeteksi perubahan dalam tipe objek bersarang yang kompleks tetapi dalam objek asli yang sederhana.
Sanket Berde
6

Saya hanya ingin menambahkan bahwa ada kait Lifecycle lain yang disebut DoCheckyang berguna jika @Inputnilainya bukan nilai primitif.

Saya memiliki Array sebagai Inputsehingga ini tidak memecat OnChangesacara ketika konten berubah (karena pengecekan yang dilakukan Angular adalah 'sederhana' dan tidak dalam sehingga Array masih merupakan Array, meskipun konten pada Array telah berubah).

Saya kemudian menerapkan beberapa kode pemeriksaan khusus untuk memutuskan apakah saya ingin memperbarui tampilan saya dengan Array yang diubah.

Stevo
sumber
6
  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

silakan coba menggunakan metode ini. Semoga ini membantu

Akshay Rajput
sumber
4

Anda juga dapat, memiliki yang dapat diamati yang memicu perubahan pada komponen induk (CategoryComponent) dan melakukan apa yang ingin Anda lakukan dalam berlangganan komponen anak. (videoListComponent)

service.ts 
public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

-----------------
CategoryComponent.ts
public onCategoryChange(): void {
  service.categoryChange$.next();
}

-----------------
videoListComponent.ts
public ngOnInit(): void {
  service.categoryChange$.subscribe(() => {
   // do your logic
});
}
Josf
sumber
2

Di sini ngOnChanges akan selalu memicu ketika properti input Anda berubah:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}
Chirag
sumber
1

Solusi ini menggunakan kelas proxy dan menawarkan keuntungan berikut:

  • Memungkinkan konsumen untuk meningkatkan kekuatan RXJS
  • Lebih kompak dari solusi lain yang diusulkan sejauh ini
  • Jenis huruf lebih aman daripada menggunakanngOnChanges()

Contoh penggunaan:

@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');

Fungsi utilitas:

export function observeProperty<T, K extends keyof T>(target: T, key: K) {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get(): T[K] { return subject.getValue(); },
    set(newValue: T[K]): void {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
}
Stephen Paul
sumber
0

Jika Anda tidak ingin menggunakan metode og onChange () implement ngOnChange, Anda juga dapat berlangganan perubahan item tertentu dengan event valueChanges , ETC.

 myForm= new FormGroup({
first: new FormControl()   

});

this.myForm.valueChanges.subscribe(formValue => {
  this.changeDetector.markForCheck();
});

the markForCheck () writen karena menggunakan dalam menyatakan ini:

  changeDetection: ChangeDetectionStrategy.OnPush
pengguna1012506
sumber
3
Ini sama sekali berbeda dari pertanyaan dan meskipun orang yang membantu harus menyadari bahwa kode Anda adalah tentang jenis perubahan yang berbeda. Pertanyaan yang diajukan tentang perubahan data yang berasal dari luar komponen dan kemudian memiliki komponen Anda tahu dan menanggapi fakta data telah berubah. Jawaban Anda berbicara tentang mendeteksi perubahan di dalam komponen itu sendiri pada hal-hal seperti input pada formulir yang LOKAL ke komponen.
rmcsharry
0

Anda juga bisa hanya melewatkan EventEmitter sebagai Input. Tidak yakin apakah ini praktik terbaik ...

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

Dan di VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Cédric Schweizer
sumber
Harap berikan tautan yang bermanfaat, cuplikan kode untuk mendukung solusi Anda.
mishsx
Saya menambahkan contoh.
Cédric Schweizer