角材料表类型错误:无法读取未定义的属性“汽车”

Posted

技术标签:

【中文标题】角材料表类型错误:无法读取未定义的属性“汽车”【英文标题】:Angular Material Table TypeError: Cannot read property 'cars' of undefined 【发布时间】:2019-05-24 08:07:49 【问题描述】:

我在使用 Angular 材料表时遇到问题 (Angular Material Table)

我运行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"  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

如here (***) 中所述,您必须使用空白对象初始化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);
  

现在它可以工作了,但这有点像 hack。我不知道为什么,但是每当 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 应用程序的链接,现在它尽可能简洁地代表问题。

【问题讨论】:

【参考方案1】:

constructor 之前创建cars 对象。 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) PD:这只是为了快速修复,我会尝试找到一种更优雅的方法来做到这一点,因为我以前处理过这样的问题,而且补丁有效,但没有看到遵循 OOP。

【讨论】:

我试过这个。它不会改变任何东西,因为 ngOnInit 在视图呈现之前从数据库初始化 this.cars 也祝你圣诞快乐。您的解决方案不起作用,因为dataSource 未定义,而不是cars,我可以尝试使用虚拟数据初始化dataSource,以便它不是未定义的,但这不是重点。您可以尝试打开我分享的 Stackblitz 链接,其中包含代码和右侧正在运行的应用程序的窗口,看看您是否可以在那里做点什么。 我已经编辑了我的答案。您只需要告诉 Angular cars 对象在这里(已初始化),无需添加虚拟数据,这是不必要的(除非您想要它,用于测试视觉设计)。 我为什么要把这个委托给 Angular? @Injectable 意味着 Angular 应该调用构造函数,创建一个单例,然后在我为 DI 使用的任何地方注入它。你有没有试过这是否有效?您的解决方案使 Angular 抛出 StaticInjectorError(AppModule)[MatPaginator],因为它不知道用什么来填充构造函数参数。【参考方案2】:

connect() 方法返回一个Observable&lt;CarsTableItem[]&gt;。由于getPagedDatagetSortedData 没有返回Observable,所以在初始化CarsTableDataSource 和材料表时会出现由于延迟而导致的未定义。

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

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

【讨论】:

该错误表明当时没有dataSource (undefined)。我编辑了我的问题。在connect() 中,dataMutations 数组有一个Observable 和2 个EventEmitters,所以它有点像Observables 的数组。然后我认为当它合并Observables 时,pipe 订阅并将每个值更改为汽车数组。我说的对吗? 我编辑了我的问题,你可能会看到我的发现。

以上是关于角材料表类型错误:无法读取未定义的属性“汽车”的主要内容,如果未能解决你的问题,请参考以下文章

错误类型错误:无法读取未定义的属性“切片”---Typescript

无法读取未定义和未处理的承诺拒绝的属性“捕获”

未捕获的类型错误:无法读取未定义的属性“mData”

未捕获的类型错误:无法读取未定义的属性“aDataSort”

未捕获的类型错误:无法读取未定义的属性“aDataSort”

jquery 数据表:“未捕获的类型错误:无法读取未定义的属性‘长度’”在销毁和重新初始化后。表作为数据源