简体   繁体   English

在QueryList.changes上更改ContentChildren模型

[英]Changing ContentChildren models on QueryList.changes

Suppose I have a parent component with @ContentChildren(Child) children . 假设我有一个带有@ContentChildren(Child) children元素的父组件。 Suppose that each Child has an index field within its component class. 假设每个Child在其组件类中都有一个index字段。 I'd like to keep these index fields up-to-date when the parent's children change, doing something as follows: 当父母的孩子改变时,我想保持这些index字段是最新的,执行如下操作:

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

However, when I attempt to do this, I get an "ExpressionChangedAfter..." error, I guess due to the fact that this index update is occurring outside of a change cycle. 但是,当我尝试这样做时,我得到一个“ExpressionChangedAfter ...”错误,我想由于这个index更新发生在更改周期之外。 Here's a stackblitz demonstrating this error: https://stackblitz.com/edit/angular-brjjrl . 这是一个stackblitz演示此错误: https ://stackblitz.com/edit/angular-brjjrl。

How can I work around this? 我该如何解决这个问题? One obvious way is to simply bind the index in the template. 一种显而易见的方法是简单地绑定模板中的index A second obvious way is to just call detectChanges() for each child when you update its index. 第二种显而易见的方法是在更新索引时为每个子项调用detectChanges() Suppose I can't do either of these approaches, is there another approach? 假设我不能做这些方法中的任何一种,是否有另一种方法?

As stated, the error comes from the value changing after the change cycle has evaluated <div>{{index}}</div> . 如上所述,错误来自更改周期评估后的值更改<div>{{index}}</div>

More specifically, the view is using your local component variable index to assign 0 ... which is then changed as a new item is pushed to the array... your subscription sets the true index for the previous item only after, it has been created and added to the DOM with an index value of 0 . 更具体地说,视图使用您的本地组件变量index来指定0 ...然后在将新项目推送到数组时更改...您的订阅仅在之后设置了上一项的真实索引,它已被创建并添加到DOM,索引值为0


The setTimout or .pipe(delay(0)) (these are essentially the same thing) work because it keeps the change linked to the change cycle that this.model.push({}) occurred in... where without it, the change cycle is already complete, and the 0 from the previous cycle is changed on the new/next cycle when the button is clicked. setTimout.pipe(delay(0)) (这些基本上是相同的东西)起作用,因为它保持变化链接到this.model.push({})发生在...的更改周期...没有它的地方,更改周期已经完成,单击按钮时,在新/下一个周期中更改上一个周期的0。

Set a duration of 500 ms to the setTimeout approach and you will see what it is truly doing. 将持续时间设置为500毫秒到setTimeout方法,您将看到它正在做什么。

 ngAfterContentInit() {
    this.foos.changes.pipe(delay(0)).subscribe(() => {
      this.foos.forEach((foo, index) => {
        setTimeout(() => {
          foo.index = index;
        }, 500)
      });
    });
  }
  • It does indeed allow the value to be set after the element is rendered on the DOM while avoiding the error however, you will not have the value available in the component during the constructor or ngOnInit if you need it. 它确实允许在元素在DOM上呈现之后设置值,同时避免错误,但是,如果需要,在构造函数或ngOnInit期间不会有组件中的值。

The following in FooComponent will always result in 0 with the setTimeout solution. FooComponent的以下FooComponent将始终使用setTimeout解决方案得到0

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

Passing the index as an input like below, will make the value available during the constructor or ngOnInit of FooComponent 将索引作为输入传递如下,将使值在构造函数或ngOnInitFooComponent期间可用


You mention not wanting to bind to the index in the template, but it unfortunately would be the only way to pass the index value prior to the element being rendered on the DOM with a default value of 0 in your example. 你提到不想绑定到模板中的索引,但不幸的是,它是在示例中在DOM上呈现元素之前传递索引值的唯一方法,默认值为0

You can accept an input for the index inside of the FooComponent 您可以接受FooComponent内部索引的输入

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

Then pass the index from your loop to the input 然后将索引从循环传递给输入

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

Then use the input in the view 然后使用视图中的输入

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

This would allow you to manage the index at the app.component level via the *ngFor , and pass it into the new element on the DOM as it is rendered... essentially avoiding the need to assign the index to the component variable, and also ensuring the true index is provided when the change cycle needs it, at the time of render / class initialization. 这将允许您通过*ngFor管理app.component级别的索引,并在app.component将其传递给DOM上的新元素...基本上避免了将索引分配给组件变量的需要,以及在渲染/类初始化时,还确保在更改周期需要时提供true索引。

Stackblitz Stackblitz

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

One way is update the index value using a Macro-Task. 一种方法是使用宏任务更新索引值。 This is essentially a setTimeout , but bear with me. 这本质上是一个setTimeout ,但请耐心setTimeout

This makes your subscription from your StackBlitz look like this: 这使您从StackBlitz订阅的内容如下所示:

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

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

  });
}

Here is a working StackBlitz . 这是一个有效的StackBlitz

So the javascript event loop is coming into play. 所以javascript事件循环正在发挥作用。 The reason for the "ExpressionChangedAfter..." error is highlighting the fact that changes are being made to other components which essentially mean that another cycle of change detection should run otherwise you can get inconsistent results in the UI. “ExpressionChangedAfter ...”错误的原因是突出显示正在对其他组件进行更改的事实,这实际上意味着应该运行另一个更改检测周期,否则您可能会在UI中获得不一致的结果。 That's something to avoid. 这是要避免的事情。

What this boils down to is that if we want to update something, but we know it shouldn't cause other side-effects, we can schedule something in the Macro-Task queue. 这归结为如果我们想要更新某些东西,但我们知道它不会引起其他副作用,我们可以在宏任务队列中安排一些事情。 When the change detection process is finished, only then will the next task in the queue be executed. 当更改检测过程完成时,只有那时才会执行队列中的下一个任务。


Resources 资源

The whole event loop is there in javascript because there is only a single-thread to play with, so it's useful to be aware of what's going on. 整个事件循环都在javascript中,因为只有一个单独的线程可以使用,因此了解正在发生的事情是很有用的。

This article from Always Be Coding explains the Javascript Event Loop much better, and goes into the details of the micro/macro queues. Always Be Coding的这篇文章更好地解释了Javascript事件循环,并详细介绍了微/宏队列。

For a bit more depth and running code samples, I found the post from Jake Archibald very good: Tasks, microtasks, queues and schedules 为了更深入和运行代码示例,我发现Jake Archibald的帖子非常好: 任务,微任务,队列和日程安排

use below code, to make that changes in the next cycle 使用下面的代码,在下一个周期中进行更改

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

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

});

The problem here is that you are changing something after the view generation process is further modifying the data it is trying to display in the first place. 这里的问题是,在视图生成过程进一步修改它首先尝试显示的数据之后,您正在更改某些内容。 The ideal place to change would be in the life-cycle hook before the view is displayed , but another issue arises here ie, this.foos is undefined when these hooks are called as QueryList is only populated before ngAfterContentInit . 更改的理想位置是在显示视图之前生命周期钩子中 ,但是这里出现另一个问题,即,当调用这些钩子时, this.foosundefined ,因为QueryList仅在ngAfterContentInit之前ngAfterContentInit

Unfortunately, there aren't many options left at this point. 不幸的是,目前还没有很多选择。 @matt-tester detailed explanation of micro/macro task is a very helpful resource to understand why the hacky setTimeout works. @ matt-tester对微/宏任务的详细解释是一个非常有用的资源,可以理解hacky setTimeout工作原理。

But the solution to an Observable is using more observables/operators (pun intended), so piping a delay operator is a cleaner version in my opinion, as setTimeout is encapsulated within it. 但Observable的解决方案是使用更多的observables /运算符(双关语),因此在我看来,管道延迟运算符是一个更简洁的版本,因为setTimeout被封装在其中。

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

here is the working version 这是工作版

I really don't know the kind of application, but to avoid playing with ordered indexes , it is often a good idea to use uid's as index. 我真的不知道应用程序的类型,但为了避免使用有序索引,使用uid作为索引通常是个好主意。 Like this, there is no need to renumber indexes when you add or remove components since they are unique. 像这样,添加或删除组件时无需重新编号索引,因为它们是唯一的。 You maintain only a list of uids in the parent. 您只在父级中维护一个uid列表。

another solution that may solve your problem , by dynamically creating your components and thus maintain a list of these childs components in the parent . 另一种可以解决您的问题的解决方案,通过动态创建组件,从而在父级中维护这些子组件的列表。

regarding the example you provided on stackblitz ( https://stackblitz.com/edit/angular-bxrn1e ) , it can be easily solved without monitoring changes : 关于您在stackblitz( https://stackblitz.com/edit/angular-bxrn1e )上提供的示例,可以在不监视更改的情况下轻松解决:

replace with the following code : 替换为以下代码:

app.component.html app.component.html

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

hello.component.ts hello.component.ts

  • remove changes monitoring 删除更改监控
  • added foocomponent index parameter 添加了foocomponent索引参数

    import { ContentChildren, ChangeDetectorRef, Component, Input, Host, Inject, forwardRef, QueryList } from '@angular/core'; 从'@ angular / core'导入{ContentChildren,ChangeDetectorRef,Component,Input,Host,Inject,forwardRef,QueryList};

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

I forked this working implementation : https://stackblitz.com/edit/angular-uwad8c 我分叉了这个有效的实现: https//stackblitz.com/edit/angular-uwad8c

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM