Sudut 2 - Tampilan tidak diperbarui setelah model berubah

129

Saya memiliki komponen sederhana yang memanggil REST api setiap beberapa detik dan menerima kembali beberapa data JSON. Saya dapat melihat dari pernyataan log saya dan lalu lintas jaringan bahwa data JSON yang dikembalikan berubah, dan model saya sedang diperbarui, namun tampilan tidak berubah.

Komponen saya terlihat seperti:

import {Component, OnInit} from 'angular2/core';
import {RecentDetectionService} from '../services/recentdetection.service';
import {RecentDetection} from '../model/recentdetection';
import {Observable} from 'rxjs/Rx';

@Component({
    selector: 'recent-detections',
    templateUrl: '/app/components/recentdetection.template.html',
    providers: [RecentDetectionService]
})



export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { this.recentDetections = recent;
             console.log(this.recentDetections[0].macAddress) });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Dan pandangan saya terlihat seperti:

<div class="panel panel-default">
    <!-- Default panel contents -->
    <div class="panel-heading"><h3>Recently detected</h3></div>
    <div class="panel-body">
        <p>Recently detected devices</p>
    </div>

    <!-- Table -->
    <table class="table" style="table-layout: fixed;  word-wrap: break-word;">
        <thead>
            <tr>
                <th>Id</th>
                <th>Vendor</th>
                <th>Time</th>
                <th>Mac</th>
            </tr>
        </thead>
        <tbody  >
            <tr *ngFor="#detected of recentDetections">
                <td>{{detected.broadcastId}}</td>
                <td>{{detected.vendor}}</td>
                <td>{{detected.timeStamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
                <td>{{detected.macAddress}}</td>
            </tr>
        </tbody>
    </table>
</div>

Saya bisa melihat dari hasil console.log(this.recentDetections[0].macAddress) bahwa objek recentDetections sedang diperbarui, tetapi tabel dalam tampilan tidak pernah berubah kecuali saya memuat ulang halaman.

Saya berjuang untuk melihat apa yang saya lakukan salah di sini. Adakah yang bisa membantu?

Matt Watson
sumber
Saya merekomendasikan untuk membuat kode lebih bersih dan tidak rumit: stackoverflow.com/a/48873980/571030
glukki

Jawaban:

216

Mungkin kode dalam layanan Anda entah bagaimana keluar dari zona Angular. Ini merusak deteksi perubahan. Ini harus bekerja:

import {Component, OnInit, NgZone} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private zone:NgZone, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                 this.zone.run(() => { // <== added
                     this.recentDetections = recent;
                     console.log(this.recentDetections[0].macAddress) 
                 });
        });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Untuk cara lain untuk menjalankan deteksi perubahan, lihat Memicu deteksi perubahan secara manual di Angular

Cara alternatif untuk meminta deteksi perubahan adalah

ChangeDetectorRef.detectChanges()

untuk segera menjalankan deteksi perubahan untuk komponen saat ini dan turunannya

ChangeDetectorRef.markForCheck()

untuk menyertakan komponen saat ini pada saat Angular menjalankan deteksi perubahan

ApplicationRef.tick()

untuk menjalankan deteksi perubahan untuk seluruh aplikasi

Günter Zöchbauer
sumber
9
Alternatif lain adalah dengan menyuntikkan ChangeDetectorRef dan memanggil cdRef.detectChanges()bukan zone.run(). Ini bisa lebih efisien, karena tidak akan menjalankan deteksi perubahan pada seluruh struktur pohon seperti yang zone.run()dilakukannya.
Mark Rajcok
1
Setuju. Gunakan hanya detectChanges()jika perubahan yang Anda buat bersifat lokal pada komponen dan turunannya. Pemahaman saya adalah bahwa detectChanges()akan memeriksa komponen dan keturunannya, tapi zone.run()dan ApplicationRef.tick()akan memeriksa seluruh pohon komponen.
Mark Rajcok
2
persis apa yang saya cari .. menggunakan @Input()tidak masuk akal atau berhasil.
yorch
15
Saya benar-benar tidak mengerti mengapa kita membutuhkan semua hal manual ini atau mengapa dewa angular2 dibiarkan begitu saja. Mengapa @Input tidak berarti bahwa komponen benar-benar bergantung pada apa pun yang dikatakan komponen induk yang bergantung pada datanya yang berubah? Tampaknya sangat tidak elegan bahwa itu tidak hanya berfungsi. Apa yang saya lewatkan?
13
Bekerja untuk saya. Tapi ini adalah contoh lain mengapa saya merasa pengembang kerangka bersudut tersesat dalam kompleksitas kerangka bersudut. Saat menggunakan angular sebagai pengembang aplikasi, Anda harus menghabiskan begitu banyak waktu untuk memahami bagaimana angular melakukan ini atau melakukan itu dan bagaimana mengatasi hal-hal seperti yang dipertanyakan di atas. Mengerikan !!
Karl
32

Ini awalnya adalah jawaban di komentar dari @Mark Rajcok, Tapi saya ingin menempatkannya di sini sebagai yang teruji dan berfungsi sebagai solusi menggunakan ChangeDetectorRef , saya melihat poin yang bagus di sini:

Alternatif lain adalah dengan menyuntikkan ChangeDetectorRefdan memanggil cdRef.detectChanges() alih-alih zone.run(). Ini bisa lebih efisien, karena tidak akan menjalankan deteksi perubahan pada seluruh struktur pohon seperti yang zone.run()dilakukannya. - Mark Rajcok

Jadi kodenya harus seperti:

import {Component, OnInit, ChangeDetectorRef} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Sunting : Menggunakan .detectChanges()dalam pelanggan dapat menyebabkan masalah Percobaan untuk menggunakan tampilan yang hancur: detectChanges

Untuk mengatasinya Anda perlu unsubscribesebelum Anda menghancurkan komponen tersebut, sehingga kode lengkapnya akan menjadi seperti:

import {Component, OnInit, ChangeDetectorRef, OnDestroy} from 'angular2/core';

export class RecentDetectionComponent implements OnInit, OnDestroy {

    recentDetections: Array<RecentDetection>;
    private timerObserver: Subscription;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        this.timerObserver = timer.subscribe(() => this.getRecentDetections());
    }

    ngOnDestroy() {
        this.timerObserver.unsubscribe();
    }

}
Al-Mothafar
sumber
this.cdRef.detectChanges (); memperbaiki masalah saya. Terima kasih!
mangesh
Terima kasih atas saran Anda, tetapi setelah beberapa kali mencoba menelepon, saya mendapatkan kesalahan: kegagalan: Kesalahan: ViewDestroyedError: Mencoba menggunakan tampilan yang hancur: detectChanges Bagi saya zone.run () membantu saya keluar dari kesalahan ini.
shivam srivastava
8

Coba gunakan @Input() recentDetections: Array<RecentDetection>;

EDIT: Alasan mengapa@Input()penting adalah karena Anda ingin mengikat nilai dalam file skrip / javascript ke tampilan (html). Tampilan akan diperbarui sendiri jika nilai yang dideklarasikan dengan@Input()dekorator diubah. Jika@Input()atau@Output()dekorator diubah,ngOnChanges-peristiwa akan terpicu, dan tampilan akan diperbarui sendiri dengan nilai baru. Anda mungkin mengatakan bahwa@Input()kehendak mengikat nilai dua arah.

cari Input di tautan ini dengan sudut: glosarium untuk informasi lebih lanjut

EDIT: setelah mempelajari lebih lanjut tentang pengembangan Angular 2, saya berpikir bahwa melakukan@Input()benar-benar bukanlah solusi, dan seperti yang disebutkan di komentar,

@Input() hanya berlaku ketika data diubah oleh data-binding dari luar komponen (data terikat di induk diubah) bukan ketika data diubah dari kode di dalam komponen.

Jika Anda melihat jawaban @ Günter, ini adalah solusi yang lebih akurat dan tepat untuk masalah tersebut. Saya akan tetap menyimpan jawaban ini di sini, tetapi harap ikuti jawaban Günter sebagai jawaban yang benar.

John
sumber
2
Itu dia. Saya telah melihat anotasi @Input () sebelumnya tetapi selalu mengaitkannya dengan input formulir, tidak pernah berpikir bahwa saya akan membutuhkannya di sini. Bersulang.
Matt Watson
1
@John terima kasih atas penjelasan tambahannya. Tapi apa yang Anda tambahkan hanya berlaku ketika data diubah oleh data-binding dari luar komponen (data terikat di induk diubah) bukan ketika data diubah dari kode di dalam komponen.
Günter Zöchbauer
Ada pengikatan properti yang dilampirkan saat menggunakan @Input()kata kunci, dan pengikatan itu memastikan bahwa nilainya diperbarui dalam tampilan. nilai akan menjadi bagian dari peristiwa siklus hidup. Posting ini berisi beberapa informasi lebih lanjut tentang kata @Input()kunci
Yohanes
Aku akan menyerah. Saya tidak melihat di mana @Input()terlibat di sini atau mengapa harus dan pertanyaan tidak memberikan informasi yang cukup. Terima kasih atas kesabaran Anda :)
Günter Zöchbauer
3
@Input()lebih merupakan solusi yang menyembunyikan akar masalah. Masalahnya harus terkait dengan zona dan deteksi perubahan.
magiccrafter
2

Dalam kasus saya, saya memiliki masalah yang sangat mirip. Saya memperbarui tampilan saya di dalam fungsi yang dipanggil oleh komponen induk, dan di komponen induk, saya lupa menggunakan @ViewChild (NameOfMyChieldComponent). Saya kehilangan setidaknya 3 jam hanya karena kesalahan bodoh ini. yaitu: Saya tidak perlu menggunakan salah satu metode tersebut:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
pengguna3674783
sumber
1
dapatkah Anda menjelaskan bagaimana Anda memperbarui komponen anak Anda dengan @ViewChild? terima kasih
Lucas Colombo
Saya menduga, orang tua memanggil metode kelas anak yang tidak diberikan, dan hanya ada dalam memori
glukki
1

Alih-alih berurusan dengan zona dan deteksi perubahan - biarkan AsyncPipe menangani kompleksitas. Ini akan menempatkan langganan yang dapat diamati, berhenti berlangganan (untuk mencegah kebocoran memori) dan mengubah deteksi pada bahu Angular.

Ubah kelas Anda untuk membuat observasi, yang akan mengeluarkan hasil dari permintaan baru:

export class RecentDetectionComponent implements OnInit {

    recentDetections$: Observable<Array<RecentDetection>>;

    constructor(private recentDetectionService: RecentDetectionService) {
    }

    ngOnInit() {
        this.recentDetections$ = Observable.interval(5000)
            .exhaustMap(() => this.recentDetectionService.getJsonFromApi())
            .do(recent => console.log(recent[0].macAddress));
    }
}

Dan perbarui tampilan Anda untuk menggunakan AsyncPipe:

<tr *ngFor="let detected of recentDetections$ | async">
    ...
</tr>

Ingin menambahkan, bahwa lebih baik membuat layanan dengan metode yang akan mengambil intervalargumen, dan:

  • buat permintaan baru (dengan menggunakan exhaustMapseperti pada kode di atas);
  • menangani kesalahan permintaan;
  • hentikan browser membuat permintaan baru saat offline.
glukki
sumber