Угловой ChangeDetection NgIf

Итак, я пытаюсь лучше понять Angulars ChangeDetection и наткнулся на проблему: https://plnkr.co/edit/M8d6FhmDhGWIvSWNVpPm?p=preview

Этот Plunkr представляет собой упрощенную версию кода моего приложения и в основном имеет родительский и дочерний компоненты. У обоих ChangeDetectionStrategy.OnPush включен.

parent.component.ts

@Component({
    selector: 'parent',
    template: `
            <button (click)="click()">Load data</button>
            {{stats.dataSize > 0}}
            <span *ngIf="stats.dataSize > 0">Works</span>
            <child [data]="data" [stats]="stats" (stats)="handleStatsChange()"></child>
        `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent implements OnCheck, OnChanges {

    data = [];
    stats = {
        dataSize: 0
    };

   constructor(private cdr: ChangeDetectorRef) {
   }

    click() {
        console.log("parent: loading data");
        setTimeout(() => {
            this.data = ["Data1", "Data2"];
            this.cdr.markForCheck();
        });
    }

    handleStatsChange() {
        console.log('parent: stats change');
        this.cdr.markForCheck();
    }
}

child.component.ts

import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from "@angular/core";

@Component({
    selector: 'child',
    template: `
        <div *ngFor="let item of data">{{item}}</div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, OnChanges {

    @Input() data;
    @Input() stats;
    @Output('stats') statsEmitter = new EventEmitter();

    constructor() {
    }

    ngOnInit() {
    }


    ngOnChanges(changes: SimpleChanges): void {
        console.log("child changes: ", changes);

        this.stats.dataSize = changes['data'].currentValue.length;
        this.statsEmitter.emit(this.stats);
    }
}

Таким образом, родитель обновляет data при нажатии кнопки, которая запускает ngOnChanges в дочернем элементе. Каждый раз при изменении данных child.component изменяет значение в stats. Я хочу, чтобы это значение dataSize использовалось в <span *ngIf="stats.dataSize > 0">Works</span> в родительском элементе. По какой-то причине *ngIf не обновляется. В остальном шаблон {{stats.dataSize > 0}} обновляется без проблем.

Что я заметил: если я удалю OnPush на родительском элементе, Angular выдаст исключение Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.. Я предполагаю, что это происходит из-за того, что *ngIf="stats.dataSize > 0" сначала было ложным, а теперь истинным после второй итерации обнаружения изменений в режиме разработки.

Вот почему я попытался установить this.cdr.markForCheck(); в родительском в handleStatsChange. handleStatsChange будет вызываться в дочернем. Это не имеет никаких последствий, в любом случае создается исключение.

Я предполагаю, что обнаружение изменений в родительском элементе не запускается, потому что в родительском элементе @Input не изменилось, поэтому ngIf не обновляется ?? Нажав кнопку два раза, вы увидите Works. I это потому, что теперь запускается новый цикл дайджеста (запускается событием) и родители ChangeDetectorRef теперь обновляют шаблон?

Так почему же Angular обновляет {{stats.dataSize > 0}} и выдает ошибку в ngIf?

Любая помощь очень ценится :)


person Wagner Michael    schedule 17.03.2017    source источник


Ответы (2)


В Angular вы не можете изменить значение во время цикла обнаружения изменений. Так,

ngOnChanges(changes: SimpleChanges): void {
        console.log("child changes: ", changes);

        this.stats.dataSize = changes['data'].currentValue.length;  <-- this is not allowed
        this.statsEmitter.emit(this.stats);
    }

Вот почему вы получаете сообщение об изменении значения после проверки.

person snorkpete    schedule 17.03.2017
comment
Значит, вы имеете в виду, что я должен обновить их в ngAfterContentChecked? - person Wagner Michael; 17.03.2017
comment
Нет, это все равно вызовет ошибку. Почему вы не обновляете статистику при обновлении данных? я не понимаю, почему вы ждете крючка жизненного цикла в вашем случае - person snorkpete; 17.03.2017

Углубившись в документацию по крючкам жизненного цикла, я заметил, что они предоставляют действительно хорошие примеры обработки обнаружения изменений.

В их примере счетчика в CounterParentComponent они должны обновить свой LoggerService, запустив новую «галочку», что означает задержку выполнения с помощью setTimeout.

прилавок:

updateCounter() {
    this.value += 1;
    this.logger.tick();
}

регистратор:

tick() {  this.tick_then(() => { }); }
tick_then(fn: () => any) { setTimeout(fn, 0); }

Это именно то, что мне нужно было сделать, чтобы мой код заработал. Они также упоминают об этом в своих документах:

Правило однонаправленного потока данных Angular запрещает обновление представления после того, как оно было составлено. Оба этих крючка срабатывают после того, как представление компонента было составлено. Angular выдает ошибку, если ловушка немедленно обновляет свойство комментария с привязкой к данным (попробуйте!). LoggerService.tick_then () откладывает обновление журнала на один оборот цикла JavaScript браузера, и этого достаточно надолго.

person Wagner Michael    schedule 18.03.2017
comment
plnkr.co/edit/tAZvxaXKaJKqoproQW7U?p=preview запускает цикл обнаружения изменений из setTimeout корневой компонент. Вам просто нужно обновить текущий вид - person yurzui; 18.03.2017