Изменение моделей ContentChildren на QueryList.changes - PullRequest
7 голосов
/ 21 марта 2019

Предположим, у меня есть родительский компонент с @ContentChildren(Child) children. Предположим, что каждый Child имеет поле index в своем классе компонентов. Я бы хотел, чтобы эти index поля обновлялись при изменении потомков родителей, делая что-то следующее:

this.children.changes.subscribe(() => {
  this.children.forEach((child, index) => {
    child.index = index;
  })
});

Однако, когда я пытаюсь сделать это, я получаю ошибку «ExpressionChangedAfter ...», я полагаю, из-за того, что это index обновление происходит вне цикла изменений. Вот стек, показывающий эту ошибку: https://stackblitz.com/edit/angular-brjjrl.

Как я могу обойти это? Один очевидный способ - просто связать index в шаблоне. Второй очевидный способ - просто вызывать detectChanges() для каждого дочернего элемента при обновлении его индекса. Предположим, я не могу использовать ни один из этих подходов, есть ли другой подход?

Ответы [ 5 ]

7 голосов
/ 26 марта 2019

Как указано, ошибка возникает из-за изменения значения после оценки цикла изменения <div>{{index}}</div>.

Более конкретно, представление использует локальную переменную компонента index для присвоения 0 ... который затем изменяется при добавлении нового элемента в массив ... ваша подписка устанавливает истинный индекс для предыдущего элемента только после того, как он был создан и добавлен в DOM со значением индекса 0.


setTimout или .pipe(delay(0)) (это, по сути, одно и то же) работают, потому что они сохраняют изменения, связанные с циклом изменений, в котором произошло this.model.push({}) ... гдебез него цикл изменения уже завершен, и 0 из предыдущего цикла изменяется при новом / следующем цикле при нажатии кнопки.

Установите длительность 500 мс для подхода setTimeout ивы увидите, что он действительно делает.

 ngAfterContentInit() {
    this.foos.changes.pipe(delay(0)).subscribe(() => {
      this.foos.forEach((foo, index) => {
        setTimeout(() => {
          foo.index = index;
        }, 500)
      });
    });
  }
  • Это действительно позволяет устанавливать значение после рендеринга элемента в DOM, избегая ошибки, однако у вас не будет значения, доступного в компоненте во время конструктора или ngOnInit если вам это нужно.

Следующее в FooComponent всегда приведет к 0 с решением setTimeout.

ngOnInit(){
    console.log(this.index)
  }

Передачаindex в качестве входных данных, как показано ниже, сделает значение доступным во время конструктора или ngOnInit из FooComponent


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

Вы можете принять ввод для индекса внутри FooComponent

export class FooComponent  {
  // index: number = 0;
  @Input('index') _index:number;

Затем передайте индекс из вашего цикла на вход

<foo *ngFor="let foo of model; let i = index" [index]="i"></foo>

Затем используйте вход в представлении

selector: 'foo',
  template: `<div>{{_index}}</div>`,

Этопозволит вам управлять индексом на app.component уровень через *ngFor и передать его в новый элемент в DOM по мере его визуализации ... по существу, избегая необходимости назначать индекс для переменной компонента, а также гарантируя, что индекс true предоставляется, когдацикл изменения нуждается в этом во время инициализации рендера / класса.

Stackblitz

https://stackblitz.com/edit/angular-ozfpsr?embed=1&file=src/app/app.component.html

3 голосов
/ 25 марта 2019

Одним из способов является обновление значения индекса с помощью макрокоманды. По сути это setTimeout, но потерпите меня.

Это делает вашу подписку с вашего StackBlitz похожей на это:

ngAfterContentInit() {
  this.foos.changes.subscribe(() => {

    // Macro-Task
    setTimeout(() => {
      this.foos.forEach((foo, index) => {
        foo.index = index;
      });
    }, 0);

  });
}

Вот рабочий StackBlitz .

Итак, цикл событий javascript вступает в игру. Причиной ошибки «ExpressionChangedAfter ...» является то, что изменения вносятся в другие компоненты, что по сути означает, что другой цикл обнаружения изменений должен выполняться, в противном случае вы можете получить противоречивые результаты в пользовательском интерфейсе. Этого следует избегать.

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


Ресурсы

Весь цикл обработки событий присутствует в javascript, потому что есть только один поток, с которым можно играть, поэтому полезно знать, что происходит.

Эта статья из Always Be Coding гораздо лучше объясняет цикл событий Javascript и подробно описывает очереди микро / макросов.

Для более глубокой и детальной проработки кода я нашел сообщение от Джейка Арчибальда очень хорошим: Задачи, микрозадачи, очереди и расписания

1 голос
/ 26 марта 2019

Проблема здесь в том, что вы что-то меняете после того, как процесс генерации представления дополнительно модифицирует данные, которые он пытается отобразить в первую очередь. Идеальное место для изменения было бы в хуке жизненного цикла до отображения представления , но здесь возникает другая проблема, т.е. this.foos равен undefined, когда эти хуки вызываются, поскольку QueryList заполняется только до ngAfterContentInit.

К сожалению, на данный момент осталось не так много вариантов. @ matt-tester подробное объяснение микро / макро-задачи - очень полезный ресурс, чтобы понять, почему работает хакер setTimeout.

Но решение для Observable заключается в использовании большего количества наблюдаемых / операторов (предназначенных для каламбура), поэтому, по моему мнению, использование оператора задержки является более чистой версией, поскольку setTimeout заключено в нем.

ngAfterContentInit() {
    this.foos.changes.pipe(delay(0)).subscribe(() => {
        this.foos.forEach((foo, index) => {
          foo.index = index;
        });
    });
}

здесь рабочая версия

1 голос
/ 25 марта 2019

используйте код ниже, чтобы внести изменения в следующем цикле

this.foos.changes.subscribe(() => {

  setTimeout(() => {
    this.foos.forEach((foo, index) => {
      foo.index = index;
    });
  });

});
0 голосов
/ 01 апреля 2019

Я действительно не знаю тип приложения, но чтобы не играть с упорядоченными индексами, часто хорошей идеей является использование uid в качестве индекса.Таким образом, нет необходимости перенумеровывать индексы при добавлении или удалении компонентов, поскольку они уникальны.Вы поддерживаете только список идентификаторов в родительском элементе.

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

относительнопример, который вы предоставили на stackblitz (https://stackblitz.com/edit/angular-bxrn1e), его легко решить без отслеживания изменений:

замените следующим кодом:

app.component.html

<hello [(model)]="model">
  <foo *ngFor="let foo of model;let i=index" [index]="i"></foo>
</hello>

hello.component.ts

  • удалить мониторинг изменений
  • добавлен параметр индекса foocomponent

    import {ContentChildren, ChangeDetectorRef, Component, Input, Host, Inject, forwardRef, QueryList} из '@ angular / core';

    @Component({
      selector: 'foo',
      template: `<div>{{index}}</div>`,
    })
    export class FooComponent  {
      @Input() index: number = 0;
    
      constructor(@Host() @Inject(forwardRef(()=>HelloComponent)) private hello) {}
    
      getIndex() {
        if (this.hello.foos) {
          return this.hello.foos.toArray().indexOf(this);
        }
    
        return -1;
      }
    }
    
    @Component({
      selector: 'hello',
      template: `<ng-content></ng-content>
        <button (click)="addModel()">add model</button>`,
    })
    export class HelloComponent  {
      @Input() model = [];
      @ContentChildren(FooComponent) foos: QueryList<FooComponent>;
    
      constructor(private cdr: ChangeDetectorRef) {}
    
    
    
      addModel() {
        this.model.push({});
      }
    }
    

Я разбудил эту рабочую реализацию: https://stackblitz.com/edit/angular-uwad8c

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...