Angular2 - выражение изменилось после его проверки - привязка к ширине div с событиями изменения размера

Я проделал некоторое чтение и расследование этой ошибки, но не знаю, какой правильный ответ для моей ситуации. Я понимаю, что в режиме dev обнаружение изменений выполняется дважды, но я не хочу использовать enableProdMode() для маскировки проблемы.

Вот простой пример, когда количество ячеек в таблице должно увеличиваться по мере расширения ширины div. (Обратите внимание, что ширина div не является функцией только ширины экрана, поэтому @Media не может быть легко применена)

Мой HTML выглядит следующим образом (widget.template.html):

<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
   <td>Value1</td>
   <td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
   <td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>

Это само по себе ничего не делает. Я предполагаю, что это происходит потому, что ничего не вызывают обнаружения изменений. Однако, когда я меняю первую строку на следующую и создаю пустую функцию для приема вызова, она начинает работать, но иногда я получаю выражение "Expression" после того, как была проверена ошибка "

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

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

  • Есть ли лучший способ запуска обнаружения изменений?
  • Должен ли я записывать событие изменения размера с помощью функции, чтобы обеспечить происходит обнаружение изменений?
  • Использует #widthParentDiv для доступа к ширина div приемлема?
  • Есть ли лучшее общее решение?

Подробнее о моем проекте см. этот аналогичный вопрос.

Спасибо

Ответ 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().


Это отличный вопрос. Я потратил много времени на различные решения, и я многому научился. Спасибо, что опубликовали этот вопрос.

Ответ 2

Другой способ, которым я использовал это решение:

import { Component, ChangeDetectorRef } from '@angular/core';


@Component({
  selector: 'your-seelctor',
  template: 'your-template',
})

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}

Ответ 3

Просто использовать

setTimeout(() => {
  //Your expression to change if state
});