Ошибка типа таблицы материалов Angular: невозможно прочитать свойство "cars" неопределенного значения.

У меня проблема с таблицей материалов Angular (Таблица материалов Angular)

Я запустил ng generate @angular/material:material-table --name=car-table, чтобы сгенерировать угловую таблицу по умолчанию, которая отлично работает. Но если я попытаюсь ввести данные (автомобили) в CarsTableDataSource, он перестанет работать. Это должно быть связано с асинхронными функциями и ngOnInit перехватчиками жизненного цикла.

Вы можете увидеть код в StackBlitz. Критическая часть находится в папке src/app/cars/.

cars.component.ts

import {Component, OnInit, ViewChild} from '@angular/core';
import {Car} from '../car';
import {CarService} from '../car.service';
import {MatPaginator, MatSort, MatTable} from '@angular/material';
import {CarsTableDataSource} from './cars-table-datasource';

@Component({
  selector: 'app-cars',
  templateUrl: './cars.component.html',
  styleUrls: ['./cars.component.css']
})
export class CarsComponent implements OnInit {
  cars: Car[];

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;
  @ViewChild(MatTable) table: MatTable<Car>;
  dataSource: CarsTableDataSource;

  /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
  displayedColumns = ['id', 'name', 'img_url'];

  constructor(private carService: CarService) {
  }

  async ngOnInit() {
    console.log('before getting cars: ');
    console.log(this.cars);
    this.cars = await this.carService.getCars().toPromise();
    console.log('got cars:');
    console.log(this.cars);
    this.dataSource = new CarsTableDataSource(this.paginator, this.sort, this.cars);
  }

  add(name: string) {
    name = name.trim();
    if (!name) {
      return;
    }
    this.carService.addCar({name} as Car)
      .subscribe(car => {
        this.cars = [...this.cars, car];
        console.log(this.cars);
        console.log('rendering rows');
        this.table.renderRows();
      });
  }

  delete(car: Car) {
    this.cars = this.cars.filter(c => c !== car);
    this.carService.deleteCar(car).subscribe();
    this.table.renderRows();
  }
}

cars-table-datasource.ts

import {DataSource} from '@angular/cdk/collections';
import {MatPaginator, MatSort} from '@angular/material';
import {map} from 'rxjs/operators';
import {merge, Observable, of as observableOf} from 'rxjs';
import {Car} from '../car';

/**
 * Data source for the CarsTable view. This class should
 * encapsulate all logic for fetching and manipulating the displayed cars
 * (including sorting, pagination, and filtering).
 */
export class CarsTableDataSource extends DataSource<CarsTableItem> {
  // cars: CarsTableItem[];

  constructor(private paginator: MatPaginator, private sort: MatSort, public cars: Car[]) {
    super();
  }

  /**
   * Connect this cars source to the table. The table will only update when
   * the returned stream emits new items.
   * @returns A stream of the items to be rendered.
   */
  connect(): Observable<CarsTableItem[]> {
    // Combine everything that affects the rendered cars into one update
    // stream for the cars-table to consume.
    const dataMutations = [
      observableOf(this.cars),
      this.paginator.page,
      this.sort.sortChange
    ];

    // Set the paginator's length
    this.paginator.length = this.cars.length;

    return merge(...dataMutations).pipe(map(() => {
      return this.getPagedData(this.getSortedData([...this.cars]));
    }));
  }

  /**
   *  Called when the table is being destroyed. Use this function, to clean up
   * any open connections or free any held resources that were set up during connect.
   */
  disconnect() {
  }

  /**
   * Paginate the cars (client-side). If you're using server-side pagination,
   * this would be replaced by requesting the appropriate cars from the server.
   */
  private getPagedData(data: CarsTableItem[]) {
    const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
    return data.splice(startIndex, this.paginator.pageSize);
  }

  /**
   * Sort the cars (client-side). If you're using server-side sorting,
   * this would be replaced by requesting the appropriate cars from the server.
   */
  private getSortedData(data: CarsTableItem[]) {
    if (!this.sort.active || this.sort.direction === '') {
      return data;
    }

    return data.sort((a, b) => {
      const isAsc = this.sort.direction === 'asc';
      switch (this.sort.active) {
        case 'name':
          return compare(a.name, b.name, isAsc);
        case 'id':
          return compare(+a.id, +b.id, isAsc);
        default:
          return 0;
      }
    });
  }
}

/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
function compare(a, b, isAsc) {
  return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}

cars.component.html

<div>
  <label>Car name:
    <input #carName />
  </label>
  <!-- (click) passes input value to add() and then clears the input -->
  <button (click)="add(carName.value); carName.value=''">
    add
  </button>
</div>

<h2>My Cars</h2>
<div class="mat-elevation-z8 centered-table-div">
  <table mat-table class="full-width-table" [dataSource]="dataSource" matSort aria-label="Elements">

    <!-- Image Column -->
    <ng-container matColumnDef="img_url">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Image</th>
      <td mat-cell *matCellDef="let row">
        <img [src]="row.img_url" alt="car image" class="car-image"/>
      </td>
    </ng-container>

    <!-- Id Column -->
    <ng-container matColumnDef="id">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
      <td mat-cell *matCellDef="let row">{{row.id}}</td>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
      <td mat-cell *matCellDef="let row">{{row.name}}</td>
    </ng-container>


    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>

  <mat-paginator #paginator
                 [length]="dataSource.cars.length"
                 [pageIndex]="0"
                 [pageSize]="5"
                 [pageSizeOptions]="[3, 5, 25, 50]">
  </mat-paginator>
</div>

Проблема заключается в ngOnInit и

  <mat-paginator #paginator
                 [length]="dataSource.cars.length"
                 [pageIndex]="0"
                 [pageSize]="5"
                 [pageSizeOptions]="[3, 5, 25, 50]">
  </mat-paginator>

В качестве ошибки я получаю ERROR TypeError: Cannot read property 'cars' of undefined, что означает, что dataSource не определено при разборе шаблона, но функция ngOnInit:

  async ngOnInit() {
    console.log('before getting cars: ');
    console.log(this.cars);
    this.cars = await this.carService.getCars().toPromise();
    console.log('got cars:');
    console.log(this.cars);
    this.dataSource = new CarsTableDataSource(this.paginator, this.sort, this.cars);
  }

распечатывает:

введите описание изображения здесь  введите описание изображения здесь

Страница по-прежнему загружает все, но я не могу, например, добавлять автомобили с помощью этого метода, потому что они добавляются в базу данных, но не обновляются в представлении, несмотря на вызов this.table.renderRows(), как указано в документации:

Поскольку таблица оптимизируется для производительности, она не будет автоматически проверять наличие изменений в массиве данных. Вместо этого, когда объекты добавляются, удаляются или перемещаются в массиве данных, вы можете инициировать обновление отображаемых строк таблицы, вызвав его метод renderRows().

Я пытался заставить ngOnInit использовать Observables вместо async/await, но это тоже не сработало:

  ngOnInit() {
    console.log('before getting cars: ');
    console.log(this.cars);
    this.carService.getCars().subscribe(cars => {
      this.cars = cars;
      console.log('got cars:');
      console.log(this.cars);
      this.dataSource = new CarsTableDataSource(this.paginator, this.sort, this.cars);
    });
  }

Если я не выполняю никаких действий по извлечению базы данных в ngOnInit, то ошибок нет.

Я также не могу добавлять машины с add() прямо сейчас, как было сказано выше.

Если вам нужна другая информация - не стесняйтесь спрашивать меня, и я обязательно отвечу, как только смогу.

Изменить

Если я отредактирую код, чтобы он выглядел так:

async ngOnInit() {
  console.log('before getting cars: ');
  console.log(this.cars);
  console.log('got cars:');
  this.cars = await this.carService.getCars().toPromise();
  console.log(this.cars);
  this.dataSource = new CarsTableDataSource(this.paginator, this.sort, this.cars);
}

Порядок ошибок меняется на:

введите здесь описание изображения  введите описание изображения здесь

что означает, что ошибка происходит в

this.cars = await this.carService.getCars().toPromise();

Я уже пробовал это с .subscribe() и делал все в этом блоке, но там не повезло.

Изменить 2

Как указано в здесь (stackoverflow), вы необходимо инициализировать dataSource пустым объектом, потому что представление анализируется до завершения всех микрозадач в ngOnInit. Инициализировать пагинатор после инициализации просмотра.

  async ngOnInit() {
    this.dataSource = new CarsTableDataSource(this.paginator, this.sort, []);
    console.log('before getting cars: ');
    console.log(this.cars);
    this.cars = await this.carService.getCars().toPromise();
    console.log('got cars:');
    console.log(this.cars);
    this.dataSource = new CarsTableDataSource(this.paginator, this.sort, this.cars);
  }

Сейчас работает, но это своего рода взлом. Я не знаю почему, но всякий раз, когда в ловушке жизненного цикла в Angular находится асинхронный код, ловушка завершается до того, как завершится асинхронный код. Не знаю почему. После того, как он видит await, он немедленно выходит из функции, и только после этого dataSource инициализируется. Буду очень признателен за объяснение.

Изменить 3

Другой обходной путь - это добавление условного оператора null в представление, где он прерывается следующим образом:

  <mat-paginator #paginator
                 [length]="dataSource?.cars.length"
                 [pageIndex]="0"
                 [pageSize]="5"
                 [pageSizeOptions]="[3, 5, 25, 50]">
  </mat-paginator>

С этой строкой:

[length]="dataSource?.cars.length"

Поскольку представление выполняется, когда ngOnInit наполовину завершен, вы должны добавлять это везде, где вы используете это свойство, чтобы оно не попало в окончательный html при анализе представления.

Изменить 4

Я обновил ссылку на приложение Stackblitz, теперь она максимально минималистична, чтобы представить проблему.


person Denis Barzanov    schedule 25.12.2018    source источник


Ответы (2)


Создайте объект cars перед constructor. Angular не знает этого свойства при запуске приложения.

cars: Car[] = [new Car()]

constructor () { }

Это просто для того, чтобы сообщить angular, что шаблон будет содержать массив типов автомобилей. (1)

Отредактировано

В CarsTableDataSource сделайте то же, что и выше.

автомобили: Автомобиль [] = [новый автомобиль ()]

и удалите их из конструктора. (1)

Другое решение - сделать CarsTableDataSource @Injectable, чтобы делегировать DI Angular.

...
@Injectable({providedIn: 'root'})
export class CarsTableDataSource extends DataSource<CarsTableItem> {

constructor ( private paginator: MatPaginator, private sort: MatSort, public cars: Car[] )

...

}

(1) ПД: Это просто для быстрого исправления, я постараюсь найти более элегантный способ сделать это, потому что я уже сталкивался с такой проблемой раньше, и этот патч работает, но не видит, что следует за ООП.

person Yoarthur    schedule 25.12.2018
comment
Я пробовал это. Это ничего не меняет, так как ngOnInit инициализирует this.cars из базы данных перед визуализацией представления. - person Denis Barzanov; 26.12.2018
comment
И тебе счастливого Рождества. Ваше решение не работает, поскольку dataSource не определено, а не cars, я мог бы попытаться инициализировать dataSource фиктивными данными, чтобы он не был неопределенным, но дело не в этом. Вы можете попробовать открыть ссылку на Stackblitz, которой я поделился, с кодом и окном с запущенным приложением справа, и посмотреть, сможете ли вы там что-нибудь сделать. - person Denis Barzanov; 26.12.2018
comment
Я отредактировал свой ответ. Вам нужно только сообщить angular, что объект cars здесь (инициализирован), нет необходимости добавлять фиктивные данные, это необязательно (если вы этого не хотите, для проверки визуального дизайна). - person Yoarthur; 26.12.2018
comment
Зачем мне делегировать это на angular? @Injectable означает, что Angular должен вызвать конструктор, создать синглтон, а затем внедрить его везде, где он есть для DI. Вы пробовали, работает ли это? Ваше решение заставляет Angular бросать StaticInjectorError(AppModule)[MatPaginator], потому что он не знает, чем заполнить аргументы конструктора. - person Denis Barzanov; 26.12.2018

Метод connect () возвращает Observable<CarsTableItem[]>. И поскольку getPagedData и getSortedData не возвращают Observable, неопределенное значение из-за задержки возникает при инициализации CarsTableDataSource и таблицы материалов.

Попробуйте добавить к этим методам .asObservable() или еще что-нибудь.

Лучше всего внедрить CarsService в реализацию CarsTableDataSource и позволить ему заниматься загрузкой данных и разбиением на страницы.

person Ali Ben Messaoud    schedule 25.12.2018
comment
В ошибке указано, что в данный момент нет dataSource (undefined). Я отредактировал свой вопрос. В connect() массив dataMutations имеет Observable и 2 EventEmitter, так что это своего рода массив Observables. Тогда я думаю, что когда он объединяет Observables, pipe подписывается и меняет каждое значение в массиве cars. Я прав? - person Denis Barzanov; 26.12.2018
comment
Я отредактировал свой вопрос, вы можете увидеть, что я обнаружил. - person Denis Barzanov; 26.12.2018