Модуль 5 · урок 2 з 10
Angular Material Table
Material Table — це не готовий грид, а конструктор. Він дає розмітку, сортування й пагінацію, але верстку рядків і логіку даних ти пишеш сам. Для навчального проєкту це плюс: видно, що відбувається всередині.
Мінімальна таблиця
import { MatTableModule } from '@angular/material/table';
@Component({
imports: [MatTableModule],
template: `
<table mat-table [dataSource]="rows()" class="grid">
<ng-container matColumnDef="code">
<th mat-header-cell *matHeaderCellDef>Код</th>
<td mat-cell *matCellDef="let row">{{ row.code }}</td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>Назва</th>
<td mat-cell *matCellDef="let row">{{ row.title }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: columns"></tr>
</table>
`,
})
export class TaskTable {
rows = input.required<readonly Task[]>();
protected readonly columns = ['code', 'title'] as const;
}
Логіка тут така: кожна колонка описується окремим ng-container з іменем,
а масив columns визначає, які саме й у якому порядку показувати. Тому приховати
колонку — це прибрати рядок із масиву, а не чіпати розмітку.
*matHeaderCellDef
Тут зірочка — не старий синтаксис *ngIf, а структурна директива самої таблиці:
вона позначає шаблон комірки, який Material підставлятиме для кожного рядка.
Новий @if її не замінює — це різні речі.
Сортування
<table mat-table [dataSource]="rows()" matSort (matSortChange)="onSort($event)">
<ng-container matColumnDef="dueDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Термін</th>
<td mat-cell *matCellDef="let row">{{ row.dueDate | date:'dd.MM' }}</td>
</ng-container>
onSort(sort: Sort): void {
this.store.setSort(sort.active, sort.direction); // напрямок '' означає «скинути»
}
Важлива деталь: MatSort вміє лише одну колонку. Багатоколонкове сортування,
якого просить лід, доведеться робити самому — про це урок 5. Material тут дасть тільки стрілки
в шапці.
Пагінація
<mat-paginator
[length]="total()"
[pageSize]="state().size"
[pageIndex]="state().page - 1"
[pageSizeOptions]="[10, 25, 50, 100]"
(page)="onPage($event)" />
onPage(e: PageEvent): void {
this.store.setPage(e.pageIndex + 1, e.pageSize);
}
MatPaginator рахує сторінки з нуля, а в людських інтерфейсах прийнято з одиниці.
Дрібниця, на якій легко помилитись — тому конвертація робиться в одному місці, на межі
компонента.
MatTableDataSource чи власне джерело
MatTableDataSource | Масив або власний DataSource | |
|---|---|---|
| Сортування й фільтр | Вбудовані, але однією колонкою й одним рядком-фільтром | Твої, будь-якої складності |
| Джерело даних | Масив у памʼяті | Будь-який потік |
| Скільки коду | Мало на старті | Більше, але прозоро |
| Коли брати | Простий список без вимог | Коли є стор із модуля 4 |
Для нашого випадку правильна відповідь — передавати вже готовий масив. Стор із модуля 4
вже вміє фільтрувати, сортувати й різати на сторінки; дублювати цю логіку всередині
MatTableDataSource означає мати два джерела правди.
// dataSource приймає і масив, і DataSource — тому просто віддаємо готові рядки
<table mat-table [dataSource]="rows()">
Липка шапка й колонка дій
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button (click)="edit.emit(row); $event.stopPropagation()">Змінити</button>
</td>
</ng-container>
Тут спрацьовує знання з модуля 3: липка шапка липне до найближчого предка зі скролом,
тож таблиця має лежати в панелі з overflow: auto, а не розсувати сторінку.
І stopPropagation на кнопці — щоб клік по дії не відкривав рядок.
trackBy — обовʼязково
<table mat-table [dataSource]="rows()" [trackBy]="trackById">
protected trackById = (_: number, row: Task) => row.id;
Без нього кожне оновлення даних перебудовує всі рядки — з утратою фокуса, скролу й стану
вкладених компонентів. Це рівно та сама історія, що з track у @for
з модуля 2, тільки для Material-таблиці її задають окремим входом.