Кнопка углового материала не активируется при переборе новых объектов при инициализации входного массива из метода получения Typescript - PullRequest
4 голосов
/ 19 мая 2019

У меня возникает следующая недоумение угловая проблема.

Редактировать: мне удалось воспроизвести это в примере Stackblitz, и обновил текст здесь.

Пример Stackblitz здесь .

Здесь показаны четыре набора из трех записей списка со значком удаления со следующими парами:

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 ]

1 голос
/ 20 мая 2019

Одна из проблем здесь (хотя и не является основной причиной) заключается в том, что при использовании цикла ngFor Angular использует идентификатор объекта для отслеживания необходимости повторной визуализации. См. документы для обсуждения. И поэтому (см. Ниже) добавление функции trackBy решает проблему.

В плохом случае здесь:

  • Использование метода получения для ввода со списком приводит к двойному вызову ngOnChanges с одинаковым значением. Предположительно, Angular получает доступ ко входу дважды (я не знаю почему), но когда это происходит, метод получения возвращает каждый раз новый список, который имеет различную идентичность, поэтому ngOnChanges вызывается дважды.
  • Это не вызывается дважды с помощью геттера string; string объекты идентичны.
  • Когда вызывается ngOnChanges, он сбрасывает список ошибок (items1), чтобы содержать новые объекты.

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

Теперь: я не знаю , почему это приводит к тому, что кнопки не нажимаются. Но Angular позволяет вам переопределить функцию trackBy, которую он использует, чтобы выяснить, изменился ли элемент списка. И изменение его для отслеживания по индексу (который здесь не изменяется) устраняет проблему.

Forked Stackblitz пример решение проблемы, которое добавляет:

К типизированному тексту компонента:

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>
0 голосов
/ 21 мая 2019

Вы по сути ... вроде ... создаете рекурсию.

  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

обертывание блока внутри ngOnChanges с setTimeout и помещение туда 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];
            }
        }
    
0 голосов
/ 19 мая 2019

В hello.component.ts, если вы измените
this.items1 = [new Number(42)]; до this.items1 = [42];
это работает.

Если честно, я не могу точно сказать, почему, но это работает :-) Кажется, что-то связано с использованием общего типа Number, возможно, в сочетании с ChangeDetection от Angular в ngOnChanges(). Я лично знаю, что эти типы существуют, но никогда не видел, чтобы кто-то их использовал.
В связи с этим я прочитал кое-что о TypeScript Do и Dont's :

Никогда не используйте типы Number, String, Boolean или Object. Эти типы относятся к не примитивным коробочным объектам, которые почти никогда не используются надлежащим образом в коде JavaScript.

Это означало бы, что вам лучше использовать строчные буквы, такие как number для указания ваших типов, а не использовать конструктор для создания новых Number s.

Надеюсь, это поможет!

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