Ответ 1
Чтобы решить вашу проблему, вам просто нужно получить и сохранить размер div
в свойстве компонента после каждого события изменения размера и использовать это свойство в шаблоне. Таким образом, значение останется постоянным, когда 2-й раунд обнаружения изменений запускается в режиме dev.
Я также рекомендую использовать @HostListener
вместо добавления (window:resize)
в ваш шаблон. Мы будем использовать @ViewChild
, чтобы получить ссылку на div
. И мы будем использовать привязку жизненного цикла ngAfterViewInit()
для установки начального значения.
import {Component, ViewChild, HostListener} from '@angular/core';
@Component({
selector: 'my-app',
template: `<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="divWidth > 350">Value2</td>
<td *ngIf="divWidth > 700">Value3</td>
</tr>
</table>`,
})
export class AppComponent {
divWidth = 0;
@ViewChild('widgetParentDiv') parentDiv:ElementRef;
@HostListener('window:resize') onResize() {
// guard against resize before view is rendered
if(this.parentDiv) {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
ngAfterViewInit() {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
Слишком плохо, что не работает. Получаем
Выражение изменилось после его проверки. Предыдущее значение: "false". Текущее значение: "true".
Ошибка вызывает наши выражения NgIf
- при первом запуске divWidth
равен 0, затем ngAfterViewInit()
запускается и изменяет значение на что-то отличное от 0, затем запускается второй раунд обнаружения изменений (в режиме dev). К счастью, есть простое/известное решение, и это одноразовая проблема, а не постоянная проблема, как в OP:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
Обратите внимание, что этот метод ожидания одного тика документирован здесь: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
Часто в ngAfterViewInit()
и ngAfterViewChecked()
нам нужно использовать трюк setTimeout()
, потому что эти методы вызывается после составления представления компонента.
Здесь работает plunker.
Мы можем сделать это лучше. Я думаю, что мы должны дросселировать события изменения размера, чтобы Angular изменение обнаружения выполнялось, скажем, каждые 100-250 мс, а не каждый раз при возникновении события изменения размера. Это должно помешать тому, чтобы приложение стало вялым, когда пользователь изменил размер окна, потому что прямо сейчас каждое событие изменения размера приводит к тому, что обнаружение изменений запускается (дважды в режиме dev). Вы можете проверить это, добавив следующий метод к предыдущему плункеру:
ngDoCheck() {
console.log('change detection');
}
Наблюдаемые могут легко дросселировать события, поэтому вместо того, чтобы использовать @HostListener
для привязки к событию изменения размера, мы создадим наблюдаемое:
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
Это работает, но... при этом экспериментируя с этим, я обнаружил что-то очень интересное... хотя мы дросселируем событие изменения размера, Angular обнаружение изменений все еще выполняется каждый раз, когда есть событие изменения размера. I.e., дросселирование не влияет на частоту смены обнаружения. (Тобиас Бош подтвердил это: https://github.com/angular/angular/issues/1773#issuecomment-102078250.)
Мне нужно только для обнаружения изменений, если событие проходит время дроссельной заслонки. И мне нужно только для обнаружения изменений на этом компоненте. Решением является создание наблюдаемого вне зоны Angular, затем вручную вызывать обнаружение изменений внутри обратного вызова подписки:
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
// set initial value, but wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
this.ngzone.runOutsideAngular( () =>
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
this.cdref.detectChanges();
})
);
}
Здесь работает рабочий plunker.
В плунтере я добавил counter
, что я увеличиваю каждый цикл обнаружения изменений, используя крючок жизненного цикла ngDoCheck()
. Вы можете видеть, что этот метод не называется – значение счетчика не изменяется при изменении размера.
detectChanges()
запустит обнаружение изменений этого компонента и его дочерних элементов. Если вы предпочтете запустить обнаружение изменений из корневого компонента (т.е. Запустите полную проверку обнаружения изменений), вместо этого используйте ApplicationRef.tick()
(это прокомментировано в plunker), Обратите внимание, что tick()
вызывает вызов ngDoCheck()
.
Это отличный вопрос. Я потратил много времени на различные решения, и я многому научился. Спасибо, что опубликовали этот вопрос.