Angular 2 - перегляд не оновлюється після зміни моделі


129

У мене є простий компонент, який викликає REST api кожні кілька секунд і отримує назад деякі дані JSON. З моїх заяв журналу та мережевого трафіку я бачу, що дані JSON, що повертаються, змінюються, і моя модель оновлюється, однак подання не змінюється.

Мій компонент виглядає так:

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

А мій погляд виглядає так:

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

Я бачу з результатів того, console.log(this.recentDetections[0].macAddress)що останній об’єкт Detetections оновлюється, але таблиця у вікні ніколи не змінюється, якщо я не перезавантажую сторінку.

Я намагаюся бачити, що я тут роблю неправильно. Хтось може допомогти?


Я рекомендую , щоб зробити код більш чистим і менш складним: stackoverflow.com/a/48873980/571030
glukki

Відповіді:


216

Можливо, код у вашій службі якимось чином виривається із зони Angular. Це визначає порушення виявлення змін. Це має працювати:

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

Інші способи виклику виявлення змін див. Ввімкнення функції виявлення змін вручну в куті

Альтернативними способами викликати виявлення змін є

ChangeDetectorRef.detectChanges()

негайно запустити виявлення змін для поточного компонента та його дітей

ChangeDetectorRef.markForCheck()

щоб включити поточний компонент наступного разу, коли Angular запускає виявлення змін

ApplicationRef.tick()

запустити виявлення змін для всієї програми


9
Ще одна альтернатива - ввести ChangeDetectorRef і зателефонувати cdRef.detectChanges()замість zone.run(). Це може бути ефективнішим, оскільки воно не буде запускати виявлення змін у всьому дереві компонентів, як zone.run()це робиться.
Марк Райчкок

1
Погодьтеся. Використовуйте лише, detectChanges()якщо будь-які внесені вами зміни є локальними для компонента та його нащадків. Я розумію, що detectChanges()буде перевіряти компонент і його нащадків, але zone.run()і ApplicationRef.tick()буде перевіряти всі дерево компонентів.
Марк Райкок

2
саме те, що я шукав .. користуватися @Input()не мало сенсу і не працювало.
йорч

15
Я дійсно не розумію, навіщо нам потрібні всі ці посібники або чому angular2 боги залишили таке необхідне. Чому @Input не означає, що цей компонент насправді залежить від того, що сказав батьківський компонент, від якого залежали зміни його даних? Це здається вкрай неелегантним, що це не просто працює. Що я пропускаю?

13
Працювали для мене. Але це ще один приклад, чому я маю відчуття, що розробники кутових рамок втрачають складність кутових рамок. Використовуючи angular як розробник програми, вам доведеться витратити стільки часу, щоб зрозуміти, як angular робить це чи робить це, і як вирішити такі речі, як питання, про яке було сказано вище. Страшний !!
Карл

32

Спочатку це відповідь у коментарях від @Mark Rajcok, але я хочу розмістити її тут як перевірену і працюючу як рішення за допомогою ChangeDetectorRef , я бачу тут хороший момент:

Іншою альтернативою є введення ChangeDetectorRefта виклик cdRef.detectChanges()замість zone.run(). Це може бути ефективнішим, оскільки воно не буде запускати виявлення змін у всьому дереві компонентів, як zone.run()це робиться. - Марк Райкок

Отже код повинен бути таким:

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

Редагувати : Використання .detectChanges()всередині subscibe може призвести до спроби використовувати знищений вигляд: detectChanges

Щоб вирішити це, вам потрібно unsubscribeзнищити компонент, тому повний код буде таким:

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

}

this.cdRef.detectChanges (); виправили мою проблему. Спасибі!
mangesh

Дякую за вашу пропозицію, але після декількох спроб дзвінка я отримую помилку: провал: Помилка: ViewDestroyedError: Спроба використовувати знищений вигляд: detectChanges Для мене zone.run () допомагає мені вийти з цієї помилки.
shivam srivastava

8

Спробуйте використовувати @Input() recentDetections: Array<RecentDetection>;

EDIT: Причина,@Input()яка важлива, полягає в тому, що ви хочете прив'язати значення в файлі typecript / javascript до подання (html). Перегляд оновиться сам, якщо@Input()зміненозначення, оголошене здекоратором. Якщо@Input()or або@Output()декоратор буде змінено, будеngOnChangesзапущено -event, а представлення оновить себе новим значенням. Ви можете сказати, що@Input()воля двосторонньо пов'язує значення.

шукайте введення на цьому посиланні за допомогою кутового: глосарій для отримання додаткової інформації

EDIT: дізнавшись більше про розробку Angular 2, я зрозумів, що робити@Input()насправді не є рішенням, і, як згадується в коментарях,

@Input() застосовується лише тоді, коли дані змінюються прив'язкою даних за межами компонента (зв'язані дані у батьківській зміні), а не коли дані змінюються з коду всередині компонента.

Якщо у вас є погляд на відповідь @ Гюнтера, це більш точне і правильне рішення проблеми. Я все одно буду зберігати цю відповідь тут, але будь ласка, дотримуйтесь відповіді Гюнтера як правильної.


2
Це було все. Я бачив анотацію @Input () раніше, але завжди пов'язував її з введеннями форми, ніколи не думав, що мені тут знадобиться. Ура.
Метт Уотсон

1
@John дякую за додаткове пояснення. Але те, що ви додали, застосовується лише тоді, коли дані змінюються прив'язкою даних за межами компонента (зв'язані дані у батьківській зміні), а не тоді, коли дані змінюються з коду всередині компонента.
Günter Zöchbauer

При використанні @Input()ключового слова додається прив'язка до властивості , і це прив'язка гарантує, що значення оновлено у вікні перегляду. значення буде частиною події життєвого циклу. У цій публікації міститься додаткова інформація про @Input()ключове слово
Джон

Я віддамся. Я не бачу, де @Input()тут бере участь чи чому це має бути, і питання не дає достатньої інформації. Дякую за терпіння все одно :)
Günter Zöchbauer

3
@Input()це більше рішення, яке приховує корінь проблеми. Проблеми повинні бути пов'язані із зонами та виявленням змін.
magiccrafter

2

У моєму випадку у мене була дуже схожа проблема. Я оновлював свій погляд усередині функції, яку викликав батьківський компонент, і в своєму батьківському компоненті я забув використовувати @ViewChild (NameOfMyChieldComponent). Я втратив щонайменше 3 години просто за цю дурну помилку. тобто: мені не потрібно було використовувати жоден із цих методів:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()

1
чи можете ви пояснити, як ви оновили свій дочірній компонент за допомогою @ViewChild? спасибі
Лукас Коломбо

Я підозрюю, що батьки називали метод дочірнього класу, який не був наданий, а існував лише в пам'яті
glukki

1

Замість того, щоб мати справу з зонами та виявляти зміни - нехай AsyncPipe вирішує складність. Це покладе спостережувану підписку, підписку (щоб запобігти витоку пам'яті) та зміни виявлення на кутових плечах.

Змініть свій клас, щоб зробити його спостережуваним, що дасть результати нових запитів:

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

І оновіть свій погляд, щоб використовувати AsyncPipe:

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

Хочете додати, що краще зробити сервіс методом, який буде приймати intervalаргументи, і:

  • створювати нові запити (використовуючи, exhaustMapяк у коді вище);
  • обробляти помилки запитів;
  • зупинити браузер не робити нових запитів у режимі офлайн.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.