NgFor tidak memperbarui data dengan Pipe di Angular2

114

Dalam skenario ini, saya menampilkan daftar siswa (larik) ke tampilan dengan ngFor:

<li *ngFor="#student of students">{{student.name}}</li>

Sangat menyenangkan bahwa itu diperbarui setiap kali saya menambahkan siswa lain ke daftar.

Namun, ketika saya memberikan pipekepada filterdengan nama mahasiswa,

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

Itu tidak memperbarui daftar sampai saya mengetik sesuatu di bidang pemfilteran nama siswa.

Ini tautan ke plnkr .

Hello_world.html

<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
    <li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>

sortir_by_name_pipe.ts

import {Pipe} from 'angular2/core';

@Pipe({
    name: 'sortByName'
})
export class SortByNamePipe {

    transform(value, [queryString]) {
        // console.log(value, queryString);
        return value.filter((student) => new RegExp(queryString).test(student.name))
        // return value;
    }
}

Chu Son
sumber
14
Tambahkan pure:falsePipa Anda, dan changeDetection: ChangeDetectionStrategy.OnPushKomponen Anda.
Eric Martinez
2
Terima kasih @Eartinez. Berhasil. Tapi bisakah Anda menjelaskan sedikit?
Chu Son
2
Juga, saya akan menyarankan untuk TIDAK menggunakan .test()fungsi filter Anda. Itu karena, jika pengguna memasukkan string yang menyertakan karakter arti khusus seperti: *atau +dll, kode Anda akan rusak. Saya pikir Anda harus menggunakan .includes()atau keluar dari string kueri dengan fungsi khusus.
Eggy
6
Menambahkan pure:falsedan membuat pipa Anda stateful akan memperbaiki masalah tersebut. Mengubah ChangeDetectionStrategy tidak diperlukan.
pixelbits
2
Bagi siapa pun yang membaca ini, dokumentasi untuk Angular Pipes menjadi jauh lebih baik dan membahas banyak hal yang sama yang dibahas di sini. Saksikan berikut ini.
0xcaff

Jawaban:

154

Untuk memahami sepenuhnya masalah dan solusi yang mungkin, kita perlu membahas deteksi perubahan sudut - untuk pipa dan komponen.

Deteksi Perubahan Pipa

Pipa tanpa negara / murni

Secara default, pipa tidak memiliki kewarganegaraan / murni. Pipa tanpa status / murni hanya mengubah data masukan menjadi data keluaran. Mereka tidak mengingat apa pun, jadi mereka tidak memiliki properti apa pun - hanya sebuah transform()metode. Oleh karena itu Angular dapat mengoptimalkan penanganan pipa stateless / murni: jika inputnya tidak berubah, pipa tidak perlu dijalankan selama siklus deteksi perubahan. Untuk pipa seperti {{power | exponentialStrength: factor}}, powerdan factoradalah masukan.

Untuk pertanyaan ini "#student of students | sortByName:queryElem.value",, studentsdan queryElem.valueadalah masukan, dan pipa sortByNameadalah tanpa kewarganegaraan / murni. studentsadalah larik (referensi).

  • Ketika seorang siswa ditambahkan, referensi array tidak berubah - studentstidak berubah - maka pipa stateless / pure tidak dijalankan.
  • Ketika sesuatu diketik ke dalam input filter, queryElem.valueberubah, maka pipa stateless / pure dijalankan.

Salah satu cara untuk memperbaiki masalah larik adalah dengan mengubah referensi larik setiap kali seorang siswa ditambahkan - yaitu, membuat larik baru setiap kali seorang siswa ditambahkan. Kami dapat melakukan ini dengan concat():

this.students = this.students.concat([{name: studentName}]);

Meskipun ini berhasil, addNewStudent()metode kami tidak harus diimplementasikan dengan cara tertentu hanya karena kami menggunakan pipa. Kami ingin menggunakan push()untuk menambah array kami.

Pipa Stateful

Pipa berstatus memiliki status - pipa tersebut biasanya memiliki properti, bukan hanya transform()metode. Mereka mungkin perlu dievaluasi bahkan jika masukan mereka tidak berubah. Ketika kita menentukan bahwa pipa adalah stateful / non-pure - pure: false- maka setiap kali sistem deteksi perubahan Angular memeriksa komponen untuk perubahan dan komponen itu menggunakan pipa stateful, itu akan memeriksa output pipa, apakah inputnya berubah atau tidak.

Ini terdengar seperti yang kami inginkan, meskipun kurang efisien, karena kami ingin pipa dieksekusi meskipun studentsreferensinya tidak berubah. Jika kita membuat pipa menjadi stateful, kita mendapatkan error:

EXCEPTION: Expression 'students | sortByName:queryElem.value  in HelloWorld@7:6' 
has changed after it was checked. Previous value: '[object Object],[object Object]'. 
Current value: '[object Object],[object Object]' in [students | sortByName:queryElem.value

Menurut jawaban @drewmoore , "kesalahan ini hanya terjadi dalam mode pengembang (yang diaktifkan secara default pada beta-0). Jika Anda memanggil enableProdMode()saat melakukan bootstrap aplikasi, kesalahan tidak akan muncul." The docs untukApplicationRef.tick() negara:

Dalam mode pengembangan, tick () juga menjalankan siklus deteksi perubahan kedua untuk memastikan bahwa tidak ada perubahan lebih lanjut yang terdeteksi. Jika perubahan tambahan terjadi selama siklus kedua ini, binding di aplikasi memiliki efek samping yang tidak dapat diselesaikan dalam satu kali proses deteksi perubahan. Dalam hal ini, Angular melontarkan kesalahan, karena aplikasi Angular hanya dapat memiliki satu deteksi perubahan yang selama itu semua deteksi perubahan harus diselesaikan.

Dalam skenario kami, saya yakin kesalahan itu palsu / menyesatkan. Kami memiliki pipa stateful, dan hasilnya dapat berubah setiap kali dipanggil - ini dapat memiliki efek samping dan tidak apa-apa. NgFor dievaluasi setelah pipa, jadi seharusnya berfungsi dengan baik.

Namun, kami tidak dapat benar-benar mengembangkan dengan kesalahan ini dilemparkan, jadi salah satu solusinya adalah menambahkan properti array (yaitu, status) ke implementasi pipa dan selalu mengembalikan array itu. Lihat jawaban @ pixelbits untuk solusi ini.

Namun, kita bisa menjadi lebih efisien, dan seperti yang akan kita lihat, kita tidak memerlukan properti array dalam implementasi pipa, dan kita tidak memerlukan solusi untuk deteksi perubahan ganda.

Deteksi Perubahan Komponen

Secara default, pada setiap acara browser, deteksi perubahan Angular melewati setiap komponen untuk melihat apakah itu berubah - input dan template (dan mungkin hal-hal lain?) Dicentang.

Jika kita mengetahui bahwa sebuah komponen hanya bergantung pada properti masukannya (dan kejadian template), dan bahwa properti masukan tidak dapat diubah, kita dapat menggunakan onPushstrategi deteksi perubahan yang jauh lebih efisien . Dengan strategi ini, alih-alih memeriksa setiap peristiwa browser, sebuah komponen hanya diperiksa saat input berubah dan saat peristiwa template dipicu. Dan, tampaknya, kami tidak mendapatkan Expression ... has changed after it was checkedkesalahan itu dengan pengaturan ini. Ini karena sebuah onPushkomponen tidak diperiksa lagi sampai "ditandai" ( ChangeDetectorRef.markForCheck()) lagi. Jadi binding Template dan keluaran pipa stateful dijalankan / dievaluasi hanya sekali. Pipa stateless / murni masih tidak dieksekusi kecuali inputnya berubah. Jadi kami masih membutuhkan pipa stateful di sini.

Ini adalah solusi yang disarankan @EricMartinez: pipa stateful dengan onPushdeteksi perubahan. Lihat jawaban @ caffinatedmonkey untuk solusi ini.

Perhatikan bahwa dengan solusi ini, transform()metode tidak perlu mengembalikan larik yang sama setiap kali. Saya merasa agak aneh: pipa stateful tanpa state. Berpikir tentang itu lagi ... pipa stateful mungkin harus selalu mengembalikan array yang sama. Jika tidak, ini hanya dapat digunakan dengan onPushkomponen dalam mode pengembang.


Jadi setelah semua itu, saya rasa saya suka kombinasi jawaban @ Eric dan @ pixelbits: pipa stateful yang mengembalikan referensi array yang sama, dengan onPushdeteksi perubahan jika komponen memungkinkan. Karena pipa stateful mengembalikan referensi larik yang sama, pipa tersebut masih dapat digunakan dengan komponen yang tidak dikonfigurasi dengan onPush.

Plunker

Ini mungkin akan menjadi idiom Angular 2: jika array memberi makan pipa, dan array mungkin berubah (item dalam array itu, bukan referensi array), kita perlu menggunakan pipa stateful.

Mark Rajcok
sumber
Terima kasih atas penjelasan rinci Anda tentang masalah tersebut. Anehnya, deteksi perubahan array diimplementasikan sebagai identitas alih-alih perbandingan kesetaraan. Apakah mungkin menyelesaikan ini dengan Observable?
Martin Nowak
@MartinNowak, jika Anda bertanya apakah array adalah Observable, dan pipanya tidak memiliki kewarganegaraan ... Saya tidak tahu, saya belum mencobanya.
Mark Rajcok
Solusi lain adalah pipa murni di mana larik keluaran melacak perubahan dalam larik masukan dengan pendengar peristiwa.
Tuupertunut
Saya baru saja membuat fork Plunker Anda, dan ini berfungsi untuk saya tanpa onPushdeteksi perubahan plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan
28

Seperti yang ditunjukkan Eric Martinez di komentar, menambahkan pure: falseke Pipedekorator Anda dan dekorator changeDetection: ChangeDetectionStrategy.OnPushAnda Componentakan memperbaiki masalah Anda. Berikut adalah plunkr yang berfungsi. Mengganti ke ChangeDetectionStrategy.Always, juga berfungsi. Inilah alasannya.

Menurut panduan angular2 pada pipa :

Pipa tidak memiliki kewarganegaraan secara default. Kita harus mendeklarasikan pipa menjadi stateful dengan menyetel pureproperti @Pipedekorator ke false. Pengaturan ini memberi tahu sistem deteksi perubahan Angular untuk memeriksa keluaran pipa ini setiap siklus, apakah masukannya berubah atau tidak.

Adapun ChangeDetectionStrategy, secara default, semua binding diperiksa setiap siklus. Ketika pure: falsepipa ditambahkan, saya yakin metode deteksi perubahan berubah dari CheckAlwaysmenjadi CheckOncekarena alasan kinerja. Dengan OnPush, binding untuk Komponen hanya diperiksa saat properti input berubah atau saat peristiwa dipicu. Untuk informasi lebih lanjut tentang detektor perubahan, bagian penting dari angular2, lihat tautan berikut:

0xcaff
sumber
"Saat pure: falsepipa ditambahkan, saya yakin metode deteksi perubahan berubah dari CheckAlways ke CheckOnce" - yang tidak sesuai dengan apa yang Anda kutip dari dokumen. Pemahaman saya adalah sebagai berikut: input pipa stateless diperiksa setiap siklus secara default. Jika ada perubahan, metode pipa transform()dipanggil untuk (kembali) menghasilkan output. Metode pipa stateful transform()dipanggil setiap siklus, dan keluarannya diperiksa untuk perubahan. Lihat juga stackoverflow.com/a/34477111/215945 .
Mark Rajcok
@MarkRajcok, Ups, Anda benar, tetapi jika demikian, mengapa mengubah strategi deteksi perubahan berfungsi?
0xcaff
1
Pertanyaan bagus. Tampaknya ketika strategi deteksi perubahan diubah menjadi OnPush(yaitu, komponen ditandai sebagai "tidak dapat diubah"), keluaran pipa yang stateful tidak harus "distabilkan" - yaitu, tampaknya transform()metode dijalankan hanya sekali (mungkin semua deteksi perubahan untuk komponen dijalankan hanya sekali). Bandingkan itu dengan jawaban @ pixelbits, di mana transform()metode ini dipanggil beberapa kali, dan itu harus stabil (menurut jawaban lain dari pixelbits ), maka perlu menggunakan referensi array yang sama.
Mark Rajcok
@MarkRajcok Jika yang Anda katakan benar, dalam hal efisiensi, ini mungkin cara yang lebih baik, dalam kasus penggunaan khusus ini karena transformhanya dipanggil saat data baru didorong alih-alih beberapa kali hingga keluaran stabil, bukan?
0xcaff
1
Mungkin, lihat jawaban bertele-tele saya. Saya berharap ada lebih banyak dokumentasi tentang deteksi perubahan di Angular 2.
Mark Rajcok
22

Demo Plunkr

Anda tidak perlu mengubah ChangeDetectionStrategy. Menerapkan Pipe stateful sudah cukup untuk membuat semuanya berfungsi.

Ini adalah pipa stateful (tidak ada perubahan lain yang dilakukan):

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  tmp = [];
  transform (value, [queryString]) {
    this.tmp.length = 0;
    // console.log(value, queryString);
    var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
    for (var i =0; i < arr.length; ++i) {
        this.tmp.push(arr[i]);
     }

    return this.tmp;
  }
}
pixelbits
sumber
Saya mendapat kesalahan ini tanpa mengubah changeDectection ke onPush: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview](Plnkr) Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]
Chu Son
1
Dapatkah Anda memberi contoh mengapa Anda perlu menyimpan nilai ke variabel lokal di SortByNamePipe dan kemudian mengembalikannya dalam fungsi transformasi @pixelbits? Saya juga memperhatikan bahwa siklus Angular melalui fungsi transformasi dua kali dengan changeDetion.OnPush dan 4 kali tanpa itu
Chu Son
@ChuSon, Anda mungkin melihat log dari versi sebelumnya. Alih-alih mengembalikan larik nilai, kami mengembalikan referensi ke larik nilai, yang saya yakin bisa diubah. Pixelbits, jawaban Anda lebih masuk akal.
0xcaff
Untuk kepentingan pembaca lain, mengenai kesalahan ChuSon yang disebutkan di atas dalam komentar, dan kebutuhan untuk menggunakan variabel lokal, pertanyaan lain telah dibuat , dan dijawab oleh pixelbits.
Mark Rajcok
19

Dari dokumentasi sudut

Pipa murni dan tidak murni

Ada dua kategori pipa: murni dan tidak murni. Pipa murni secara default. Setiap pipa yang Anda lihat sejauh ini murni. Anda membuat pipa tidak murni dengan menyetel benderanya yang murni ke false. Anda bisa membuat FlyingHeroesPipe tidak murni seperti ini:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

Sebelum melakukan itu, pahami perbedaan antara murni dan tidak murni, dimulai dengan pipa murni.

Pipa murni Angular mengeksekusi pipa murni hanya jika mendeteksi perubahan murni ke nilai input. Perubahan murni adalah perubahan ke nilai input primitif (String, Angka, Boolean, Simbol) atau referensi objek yang diubah (Tanggal, Array, Fungsi, Objek).

Angular mengabaikan perubahan dalam objek (komposit). Ini tidak akan memanggil pipa murni jika Anda mengubah bulan masukan, menambah larik masukan, atau memperbarui properti objek masukan.

Ini mungkin tampak membatasi tetapi juga cepat. Pemeriksaan referensi objek cepat — jauh lebih cepat daripada pemeriksaan mendalam untuk perbedaan — jadi Angular dapat dengan cepat menentukan apakah ia dapat melewati eksekusi pipa dan pembaruan tampilan.

Untuk alasan ini, pipa murni lebih disukai bila Anda dapat menyesuaikan diri dengan strategi deteksi perubahan. Jika tidak bisa, Anda bisa menggunakan pipa yang tidak murni.

Sumit Jaiswal
sumber
Jawabannya benar, tetapi lebih banyak usaha saat mempostingnya akan menyenangkan
Stefan
2

Daripada melakukan pure: false. Anda dapat menyalin dalam dan mengganti nilai dalam komponen dengan this.students = Object.assign ([], NEW_ARRAY); di mana NEW_ARRAY adalah larik yang dimodifikasi.

Ini berfungsi untuk sudut 6 dan harus bekerja untuk versi sudut lainnya juga.

ideeps
sumber
0

Solusi: Impor Pipa secara manual dalam konstruktor dan panggil metode transformasi menggunakan pipa ini

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

Sebenarnya Anda bahkan tidak membutuhkan pipa

Richard Luo
sumber
0

Tambahkan parameter ekstra ke pipa, dan ubah tepat setelah perubahan array, dan bahkan dengan pipa murni, daftar akan diperbarui

biarkan item item | pipa: param

Nick Bistrov
sumber
0

Dalam kasus penggunaan ini saya menggunakan file Pipe in ts untuk pemfilteran data. Jauh lebih baik untuk kinerja, daripada menggunakan pipa murni. Gunakan di ts seperti ini:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}
Andris
sumber
0

untuk membuat pipa yang tidak murni mahal harganya. jadi jangan membuat pipa yang tidak murni, tetapi ubah referensi variabel data dengan membuat salinan data saat mengubah data dan menetapkan ulang referensi salinan dalam variabel data asli.

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
amit bhandari
sumber