从Typescript吸气剂初始化输入数组时,在遍历新对象时,角材料按钮不可单击

时间:2019-05-18 23:20:08

标签: angular angular-material

我有以下令人困惑的Angular问题。

编辑:我设法在Stackblitz示例中重现了这一点,并在此处更新了文本。

Stackblitz示例here

这将显示四组三个列表条目,带有一个删除图标,并具有以下配对:

string input/passed from a getter
string input/passed from a field
array input/passed from a getter
array input/passed from a field

在数组/获取器的情况下,当我单击第一个(来自items1)时,没有任何内容记录到控制台;但是单击第二个(来自items2)或第三个(来自items3),则会记录'delete'。在其他情况下,它会总是按预期删除日志。

这里可能会发生什么?

后跟代码,尽管Stackblitz很容易玩。

首先是父HTML,它设置了四种情况:

String with getter
<hello [name]="name1">
</hello>
<hr/>
String without getter
<hello [name]="name2">
</hello>
<hr/>
Array with getter
<hello [names]="names1">
</hello>
<hr/>
Array without getter
<hello [names]="names2">
</hello>

和打字稿:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent {
  get name1(): string {
    return 'Angular';
  }
  name2 = 'Angular';
  get names1(): string[] {
    return ['Angular'];
  }
  names2 = ['Angular'];
}

然后是组件HTML:

<mat-list>
  <ng-container *ngFor="let item of items1">
    <mat-list-item>
      <h2 mat-line>Some Text</h2>
      <button mat-icon-button (click)="delete()"><mat-icon>delete</mat-icon></button>
    </mat-list-item>
  </ng-container>
  <ng-container *ngFor="let item of items2">
    <mat-list-item>
      <h2 mat-line>Some Text</h2>
      <button mat-icon-button (click)="delete()"><mat-icon>delete</mat-icon></button>
    </mat-list-item>
  </ng-container>
  <ng-container *ngFor="let item of items3">
    <mat-list-item>
      <h2 mat-line>Some Text</h2>
      <button mat-icon-button (click)="delete()"><mat-icon>delete</mat-icon></button>
    </mat-list-item>
  </ng-container>
</mat-list>

和打字稿:

@Component({
  selector: 'hello',
  templateUrl: './hello.component.html',
  styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent  {
  @Input() name: string;
  @Input() names: string[];

  items1: Number[] = [];
  items2: Number[] = [];
  items3 = [new Number(42)];

  ngOnChanges() {
    this.items1 = [new Number(42)];
    this.items2 = [42];
  }

  delete() {
    console.log('delete');
  }
}

3 个答案:

答案 0 :(得分:1)

这里的一个问题(虽然不是根本原因)是,在使用ngFor循环时,Angular使用对象标识来跟踪是否需要重新渲染。参见docs进行讨论。因此,(见下文)添加一个trackBy函数可以解决此问题。

在最坏的情况下:

  • 使用获取器来输入列表会导致ngOnChanges被调用两次,且值相同。大概Angular会两次访问输入(我不知道为什么),但是当这样做时,getter每次都会返回一个具有不同标识的新列表,因此ngOnChanges被调用两次。
  • 它不是{em> 用string吸气剂被叫过两次; string对象是相同的。
  • 调用ngOnChanges时,它将重置错误列表(items1)以包含新对象。

结果是,在这种情况下,Angular两次渲染了该列表,因为它认为所有元素都从一次传递更改为另一次传递。

现在:我不知道,为什么这会导致按钮不可点击。但是Angular允许您覆盖trackBy函数,该函数用于确定列表的元素是否已更改。并将其更改为按索引进行跟踪(此处未更改)可以解决此问题。

Forked Stackblitz example解决了该问题,并添加了:

到组件的打字稿:

trackByIndex(index, item) {
  return index;
}

,并在HTML循环中:

<ng-container *ngFor="let item of items1; trackBy: trackByIndex">
  <mat-list-item>
    <h2 mat-line>Some Text</h2>
    <button mat-icon-button (click)="delete()"><mat-icon>delete</mat-icon></button>
  </mat-list-item>
</ng-container>

答案 1 :(得分:0)

hello.component.ts中,如果您更改
this.items1 = [new Number(42)];this.items1 = [42];
它正在工作。

说实话,我无法确切地说出原因,但是它可以工作:-)似乎与使用通用类型Number有关,也许与{{中的Angular的ChangeDetection 1}}。我个人知道这些类型存在,但从未见过有人使用它们。
关于这一点,我在TypeScript Do's and Dont's上读了一些东西:

  

永远不要使用数字,字符串,布尔值或对象类型。这些类型是指非原始的盒装对象,这些对象几乎从未在JavaScript代码中正确使用。

这意味着您最好使用小写字母,例如ngOnChanges()来指定类型,而不要使用构造函数来创建新的number

希望对您有帮助!

答案 2 :(得分:0)

您本质上是……在创建递归。

  1. 通过传递输入以触发ngOnChanges来初始化*ngFor ...的值
  2. 然后更新视图,因为初始值为空...
  3. 然后,当视图“更改”并更改基础标识时,它又触发另一个ngOnChanges事件...
  4. 等等...等等...

正如您指出的那样,每次迭代都会更改“身份”。

  

迭代器中元素的标识可以更改,而   数据没有。 *例如,如果迭代器为   从RPC生成到服务器,并且* RPC重新运行。即使   数据未更改,第二个响应生成带有*的对象   不同的身份,Angular必须拆除整个DOM,   重建它(好像所有旧的*元素都已删除,而所有新的*元素都已删除   元素)。

https://github.com/angular/angular/blob/1c3ee41902ee742f6c62e053ddf3ee53ac78c7b5/packages/common/src/directives/ng_for_of.ts#L110

ngOnChangessetTimeout内的块包装并在其中放置console.log,将暴露出我试图说明的基本问题并破坏堆栈。

  • 我知道这是一个非常生动的例子来说明这里发生的事情,但是我认为将其发挥到极致以了解您尝试的方法正在发生的事情很重要。

    ngOnChanges() {
            console.log('test')
            //setTimeout(()=>{
            // this.items1 = [new Number(42)];
            // this.items2 = [42];
            // })
    
          }
    
    ngAfterViewInit(){
          //passing the input to the *ngFor or figuring out some other way to update the 
          `hello.component` array would be a more stable solution. Leveraging `ngOnChanges` 
          to modify the view via component variable will not be a stable approach.
    
           for (const name in this.names) {
              this.items1 = [new Number(42)];
              this.items2 = [42];
            }
        }