Перейти к содержимому

36. Angular Material

Angular Material — официальная библиотека UI-компонентов от команды Angular, реализующая Material Design от Google. Это не просто набор красивых кнопок — это полноценная дизайн-система с доступностью (a11y), темизацией и CDK под капотом 💅


Окно терминала
ng add @angular/material

Команда автоматически:

  • Установит @angular/material и @angular/cdk
  • Добавит тему в styles.scss
  • Настроит анимации
  • Добавит шрифты Material Icons

В Angular 17+ каждый компонент Material — это standalone:

feature/dashboard/dashboard.component.ts
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTableModule } from '@angular/material/table';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatChipsModule } from '@angular/material/chips';
import { MatBadgeModule } from '@angular/material/badge';
@Component({
standalone: true,
imports: [
MatButtonModule,
MatCardModule,
MatInputModule,
MatFormFieldModule,
// ... и т.д.
]
})
export class DashboardComponent {}

<!-- Варианты кнопок -->
<button mat-button>Text</button>
<button mat-raised-button>Raised</button>
<button mat-stroked-button>Stroked</button>
<button mat-flat-button>Flat</button>
<button mat-icon-button><mat-icon>favorite</mat-icon></button>
<button mat-fab><mat-icon>add</mat-icon></button>
<button mat-mini-fab><mat-icon>edit</mat-icon></button>
<!-- Цвета -->
<button mat-raised-button color="primary">Primary</button>
<button mat-raised-button color="accent">Accent</button>
<button mat-raised-button color="warn">Warn</button>
<!-- С иконкой -->
<button mat-raised-button color="primary">
<mat-icon>save</mat-icon>
Сохранить
</button>

<!-- Базовый input -->
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput type="email" [formControl]="emailControl" />
<mat-icon matSuffix>email</mat-icon>
<mat-hint>[email protected]</mat-hint>
<mat-error *ngIf="emailControl.hasError('required')">
Email обязателен
</mat-error>
<mat-error *ngIf="emailControl.hasError('email')">
Некорректный email
</mat-error>
</mat-form-field>
<!-- Textarea -->
<mat-form-field appearance="outline">
<mat-label>Описание</mat-label>
<textarea matInput cdkTextareaAutosize rows="3"></textarea>
</mat-form-field>
<!-- Select -->
<mat-form-field appearance="outline">
<mat-label>Город</mat-label>
<mat-select [formControl]="cityControl">
<mat-option value="moscow">Москва</mat-option>
<mat-option value="spb">Санкт-Петербург</mat-option>
<mat-option value="ekb">Екатеринбург</mat-option>
</mat-select>
</mat-form-field>

dialog/confirm-dialog.component.ts
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { inject } from '@angular/core';
interface DialogData {
title: string;
message: string;
}
@Component({
standalone: true,
imports: [MatDialogModule, MatButtonModule],
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content>{{ data.message }}</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Отмена</button>
<button mat-raised-button color="warn" [mat-dialog-close]="true">
Удалить
</button>
</mat-dialog-actions>
`
})
export class ConfirmDialogComponent {
data = inject<DialogData>(MAT_DIALOG_DATA);
dialogRef = inject(MatDialogRef<ConfirmDialogComponent>);
}
// Открытие диалога
@Component({...})
export class ListComponent {
private dialog = inject(MatDialog);
openConfirm(item: Item) {
const ref = this.dialog.open(ConfirmDialogComponent, {
width: '400px',
data: { title: 'Удалить?', message: \`Удалить "\${item.name}"?\` }
});
ref.afterClosed().subscribe(confirmed => {
if (confirmed) this.deleteItem(item.id);
});
}
}

@Injectable({ providedIn: 'root' })
export class NotificationService {
private snackBar = inject(MatSnackBar);
success(message: string) {
this.snackBar.open(message, '✕', {
duration: 3000,
panelClass: ['snack-success'],
horizontalPosition: 'end',
verticalPosition: 'top',
});
}
error(message: string) {
this.snackBar.open(message, 'Закрыть', {
duration: 5000,
panelClass: ['snack-error'],
});
}
}

@Component({
standalone: true,
imports: [MatTableModule, MatSortModule, MatPaginatorModule],
template: `
<table mat-table [dataSource]="dataSource" matSort>
<!-- Колонка ID -->
<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 -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Имя</th>
<td mat-cell *matCellDef="let row">{{ row.name }}</td>
</ng-container>
<!-- Колонка Actions -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let row">
<button mat-icon-button (click)="edit(row)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(row)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 25]" showFirstLastButtons />
`
})
export class UsersTableComponent implements AfterViewInit {
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
displayedColumns = ['id', 'name', 'actions'];
dataSource = new MatTableDataSource(USERS);
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
}
applyFilter(event: Event) {
const filter = (event.target as HTMLInputElement).value;
this.dataSource.filter = filter.trim().toLowerCase();
}
}

<!-- DatePicker -->
<mat-form-field appearance="outline">
<mat-label>Дата рождения</mat-label>
<input matInput [matDatepicker]="picker" [formControl]="dateControl" />
<mat-hint>ДД.ММ.ГГГГ</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker" />
<mat-datepicker #picker />
</mat-form-field>
<!-- Autocomplete -->
<mat-form-field appearance="outline">
<mat-label>Поиск города</mat-label>
<input
matInput
[formControl]="searchControl"
[matAutocomplete]="auto"
/>
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
@for (option of filteredOptions$ | async; track option.id) {
<mat-option [value]="option">{{ option.name }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>

styles.scss
@use '@angular/material' as mat;
// Включаем Material 3 (MDC)
@include mat.core();
// Создаём тему через CSS переменные
:root {
--mat-sys-primary: #dd0031;
--mat-sys-on-primary: #ffffff;
--mat-sys-primary-container: #ffd8d8;
--mat-sys-surface: #0f172a;
--mat-sys-on-surface: #e2e8f0;
--mat-sys-background: #0f172a;
}
// Кастомная палитра
$my-theme: mat.define-theme((
color: (
theme-type: dark,
primary: mat.$red-palette,
tertiary: mat.$blue-palette,
),
typography: (
brand-family: 'Inter, sans-serif',
),
density: (
scale: 0,
)
));
html {
@include mat.all-component-themes($my-theme);
}

// Своя карточка-статистика
@Component({
standalone: true,
imports: [MatCardModule, MatIconModule],
template: `
<mat-card class="stat-card" [class.highlight]="highlight">
<mat-card-header>
<mat-icon mat-card-avatar [style.color]="color">{{ icon }}</mat-icon>
<mat-card-title>{{ value }}</mat-card-title>
<mat-card-subtitle>{{ label }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<ng-content />
</mat-card-content>
</mat-card>
`,
styles: [`
.stat-card {
transition: box-shadow 0.3s;
cursor: pointer;
&:hover { box-shadow: 0 8px 24px rgba(221, 0, 49, 0.2); }
}
.stat-card.highlight {
border-left: 4px solid #dd0031;
}
`]
})
export class StatCardComponent {
@Input() icon = 'analytics';
@Input() value = '0';
@Input() label = '';
@Input() color = '#dd0031';
@Input() highlight = false;
}