开发者问题收集

Angular Material Table TypeError:无法读取未定义的属性“cars”

2018-12-25
8056

我在使用 Angular Material Table ( Angular Material Table ) 时遇到了问题

我运行了 ng generate @angular/material:material-table --name=car-table 来生成默认的 angular 表,它工作正常。 但是,如果我尝试将数据(汽车)注入 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() ,如文档中所述:

Since the table optimizes for performance, it will not automatically check for changes to the data array. Instead, when objects are added, removed, or moved on the data array, you can trigger an update to the table's rendered rows by calling its renderRows() method.

我尝试让 ngOnInit 使用 Observable s 而不是 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

另一种解决方法是在视图中中断的位置添加空条件运算符,如下所示:

<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 应用程序的链接,现在它尽可能简约以表示问题。

2个回答

构造函数 之前创建 cars 对象。运行应用程序时,Angular 不知道该属性。

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

constructor () { }

这只是告诉 angular 模板将包含 cars 类型的数组。 (1)

已编辑

CarsTableDataSource 中执行与上述相同的操作。

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

并从构造函数中删除它们。 (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) P.D.:这只是为了快速修复,我会尝试找出一种更优雅的方法,因为我之前处理过这样的问题,并且那个补丁有效,但没有看到遵循 OOP。

Yoarthur
2018-12-25

connect() 方法返回 Observable<CarsTableItem[]> 。由于 getPagedDatagetSortedData 未返回 Observable,因此在初始化 CarsTableDataSource 和 Material Table 时发生因延迟而导致的未定义。

尝试向这些方法添加 .asObservable() 或其他内容。

作为最佳实践,您应该在 CarsTableDataSource 的实现中注入 CarsService ,并让它处理数据加载和分页内容。

Ali Ben Messaoud
2018-12-25