Angular Material Table TypeError: Невозможно прочитать свойство 'cars' из неопределенного - PullRequest
0 голосов
/ 26 декабря 2018

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

Я запустил 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);
  }

печатает:

enter image description here enter image description here

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

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

Я попытался сделать 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, тогда не будет 't никаких ошибок.

Я также не могу добавить автомобили с 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);
}

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

enter image description here enter image description here

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

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

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

Редактировать 2

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

  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 ]

0 голосов
/ 26 декабря 2018

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

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

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

0 голосов
/ 26 декабря 2018

Создайте объект 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) PD: Это просто для быстрого исправления, я постараюсь найти более элегантный способ сделать это, потому что я уже сталкивался с такой проблемой раньше, и этот патч работает, но не видит, что следует за ООП..

...