简体   繁体   English

使用 Map 将数据从父组件传递到子组件时出现 Angular ExpressionChangedAfterItHasBeenCheckedError

[英]Angular ExpressionChangedAfterItHasBeenCheckedError when passing data from Parent Component to Child Component using Map

I have a very simple setup.我有一个非常简单的设置。 I have an input field in app.component.html in which the user can type in a string.我在 app.component.html 中有一个输入字段,用户可以在其中输入字符串。 I am adding that string to a Map<string, number>.我将该字符串添加到 Map<string, number>。 I am passing that map to child component.我正在将该地图传递给子组件。 I am accessing the map in child component using @Input() and iterating over it and displaying the values in child component.我正在使用 @Input() 访问子组件中的地图并对其进行迭代并显示子组件中的值。

app.component.html -> Parent Component app.component.html -> 父组件

<div class="shopping-cart">
    <app-shopping-cart-item [shoppingItems]="shoppingItems"></app-shopping-cart-item>
</div>
<p>
  <button class="btn btn-primary" (click)="updateName()">Update Name</button>
</p>

app.component.ts app.component.ts

  shoppingItems = new Map<string, number>();

  updateName(){
    this.shoppingItems.set('test', 1);
  }

shopping-cart-component.html购物车-component.html

<div *ngFor="let article of this.shoppingItems.entries()" class="shopping-cart-item">
  <div class="article-name">{{ article[0] }}</div>
</div>

Shopping-Cart-Item.component.ts购物车-Item.component.ts

  @Input()
  shoppingItems;

The issue is when I enter a value in the input value (in parent) and click update then it updates the values displayed in the child component.问题是当我在输入值(在父组件中)输入一个值并单击更新时,它会更新子组件中显示的值。 But I get a ExpressionChangedAfterItHasBeenCheckedError.但我得到一个 ExpressionChangedAfterItHasBeenCheckedError。

I know that we get this error when we don't have unidirectional data flow.我知道当我们没有单向数据流时会出现此错误。 But I don't understand why I get this error in this scenario.但我不明白为什么在这种情况下会出现此错误。

core.js:6210 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '[object Map Iterator]'. Current value: '[object Map Iterator]'.
    at throwErrorIfNoChangesMode (core.js:8129)
    at bindingUpdated (core.js:19991)
    at Module.ɵɵproperty (core.js:21142)
    at ShoppingCartItemComponent_Template (shopping-cart-item.component.html:3)
    at executeTemplate (core.js:12059)
    at refreshView (core.js:11906)
    at refreshComponent (core.js:13358)
    at refreshChildComponents (core.js:11635)
    at refreshView (core.js:11958)
    at refreshComponent (core.js:13358)

This is almost definitely caused by using the entries() function in template.这几乎肯定是由在模板中使用entries()函数引起的。 I can't speak to the specifics of why this is causing the issue, as I don't know the inner workings of Map that well, but I'm assuming it's something along the lines of a new array (or Iterator) gets created everytime that function is called, and that function is called on every change detection cycle, which is making angular believe something is changing during the change detection cycle caused by the button click.我无法详细说明为什么会导致此问题,因为我不太了解Map的内部工作原理,但我假设它与创建的新数组(或迭代器)类似每次调用该函数时,都会在每个更改检测周期调用该函数,这使 Angular 相信在按钮单击引起的更改检测周期中发生了某些变化。 Or perhaps Angular doesn't handle Itertors very well.或者也许 Angular 不能很好地处理迭代器。

either way, using the keyvalue pipe to iterate instead should solve the problem, as it will only change the array when it needs to:无论哪种方式,使用keyvalue管道进行迭代应该可以解决问题,因为它只会在需要时更改数组:

<div *ngFor="let article of this.shoppingItems | keyvalue" class="shopping-cart-item">
  <div class="article-name">{{ article.key }}</div>
</div>

the problem is the entries() function.问题是 entry() 函数。 That function internally creates an iterable object that it is changing each time you add something to it.该函数在内部创建了一个可迭代对象,每次向其添加内容时它都会更改。 It's known as bad practice to call functions from the component view so if you want to maintain the Map in you parent component then I recommend you that child component receives an array from the parent instead of a Map.从组件视图调用函数被称为不好的做法,因此如果您想在父组件中维护 Map,那么我建议您子组件从父组件接收数组而不是 Map。 To do so, you have to create an array property in parent component as Map and pass it to child component.为此,您必须在父组件中创建一个数组属性作为 Map 并将其传递给子组件。 Each time you execute updateName function you should maintain both, Map and Array.每次执行 updateName 函数时,您都应该同时维护 Map 和 Array。

  shoppingItems = new Map<string, number>();
  childArray = [];

  updateName(){
    this.shoppingItems.set('test', 1);
    this.childArray.push(this.shoppingItems.get('test'));
  }

A small commercial: There is a important situation with your updateName() function.一个小广告:你的 updateName() 函数有一个重要的情况。 First, it is not a parameterized function because each time you execute it you are adding the same thing.首先,它不是一个参数化函数,因为每次执行它时都在添加相同的东西。 So, a Map is a structure which would not let you add more than once same key, in this case 'test' so that is replacing the same key value each time.因此,Map 是一种结构,它不允许您添加多次相同的键,在本例中为“测试”,以便每次都替换相同的键值。 In addition to that, when you would like to parameterize the function you have to be sure before that key is already added in array to avoid duplicating items and replace the index if apply.除此之外,当您想参数化该函数时,您必须确保在该键已经添加到数组中之前,以避免重复项并替换索引(如果适用)。 Just for your information.仅供参考。 So, next.那么,接下来。

Then, we have to change the child and parent views too:然后,我们也必须更改子视图和父视图:

Parent:家长:

<div class="shopping-cart">
    <app-shopping-cart-item [shoppingItems]="childArray"></app-shopping-cart-item>
</div>
<p>
  <button class="btn btn-primary" (click)="updateName()">Update Name</button>
</p>

Child:孩子:

<div *ngFor="let article of this.shoppingItems" class="shopping-cart-item">
  <div class="article-name">{{ article }}</div>
</div>

So in that manner we are taking care of duplicates with Map and therefore in the Array too because we are depending on Map.因此,以这种方式,我们使用 Map 处理重复项,因此也在 Array 中处理重复项,因为我们依赖于 Map。

Try it.尝试一下。

Note: There is another important and advanced topic called Change detection .注意:还有另一个重要且高级的主题,称为更改检测

Try like this. 

    import { Component, ElementRef, OnInit, ViewChild, ChangeDetectorRef, ViewRef } from '@angular/core';
    
      constructor(
        private dtr: ChangeDetectorRef,
      ) {
      }
// write this code where you want to detect change in browser
     if (this.dtr && !(this.dtr as ViewRef).destroyed) {
                this.dtr.detectChanges();
              }
updateName(){

setTimeout(() => {
this.shoppingItems.set('test', 1);   
},
 0);
     
}

try this, you should no more get that error (place the code within the function in setTimeout)试试这个,你应该不会再得到那个错误(将代码放在 setTimeout 的函数中)

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

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