angular-material-19

📁 avvale/aurora-front 📅 13 days ago
1
总安装量
1
周安装量
#52517
全站排名
安装命令
npx skills add https://github.com/avvale/aurora-front --skill angular-material-19

Agent 安装分布

claude-code 1

Skill 文档

When to Use

  • Using Angular Material 19 components
  • Creating custom form controls with signal-based patterns
  • Implementing Material 3 theming with CSS variables
  • Setting up dark/light mode switching
  • Using CDK features (2D Drag & Drop, Overlay, A11y)
  • Working with the new timepicker component
  • Customizing component styles with override mixins

Angular Material 19 Key Changes

Material 3 (M3) is Stable

Material 3 is now the default design system. Key features:

  • Design tokens as CSS variables – All styling via --mat-* variables
  • light-dark() function – Native CSS for theme switching
  • Component override mixins – Granular customization without specificity wars
  • New “Styling” tab – Each component’s docs now shows override API

New Components in v19

  • Timepicker (mat-timepicker) – Native time selection
  • 2D Drag & Drop – Mixed orientation support in CDK
  • Tab reordering – Draggable tabs with CDK

Timepicker (New in v19)

Basic Usage

import { Component } from '@angular/core';
import { MatTimepickerModule } from '@angular/material/timepicker';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { provideNativeDateAdapter } from '@angular/material/core';

@Component({
    selector: 'app-time-picker',
    providers: [provideNativeDateAdapter()],
    imports: [MatTimepickerModule, MatFormFieldModule, MatInputModule],
    template: `
        <mat-form-field appearance="outline">
            <mat-label>Select time</mat-label>
            <input
                [matTimepicker]="picker"
                matInput
                formControlName="time"
            />
            <mat-timepicker-toggle
                [for]="picker"
                matIconSuffix
            />
            <mat-timepicker #picker />
        </mat-form-field>
    `,
})
export class TimePickerComponent {}

Interval-Based Options

<!-- 30-minute intervals -->
<mat-timepicker
    #picker
    interval="30m"
/>

<!-- 15-minute intervals -->
<mat-timepicker
    #picker
    interval="15m"
/>

<!-- 1-hour intervals -->
<mat-timepicker
    #picker
    interval="1h"
/>

<!-- Custom seconds -->
<mat-timepicker
    #picker
    interval="900"
/>
<!-- 15 min in seconds -->

Min/Max Validation

<input
    [matTimepicker]="picker"
    [matTimepickerMin]="minTime"
    [matTimepickerMax]="maxTime"
    matInput
    formControlName="appointmentTime"
/>
<mat-timepicker #picker />
// In component
minTime = new Date(); // Set hours/minutes as needed
maxTime = new Date();

constructor();
{
    this.minTime.setHours(9, 0, 0); // 9:00 AM
    this.maxTime.setHours(17, 0, 0); // 5:00 PM
}

With Reactive Forms

@Component({
    // ... imports
})
export class AppointmentComponent {
    form = new FormGroup({
        date: new FormControl<Date | null>(null),
        time: new FormControl<Date | null>(null),
    });

    ngOnInit(): void {
        // Set initial time
        const initialTime = new Date();
        initialTime.setHours(14, 30, 0);
        this.form.get('time').setValue(initialTime);
    }
}

Validation Errors

<mat-form-field appearance="outline">
    <mat-label>Time</mat-label>
    <input
        [matTimepicker]="picker"
        matInput
        formControlName="time"
    />
    <mat-timepicker-toggle
        [for]="picker"
        matIconSuffix
    />
    <mat-timepicker #picker />
    <mat-error *ngIf="form.get('time').hasError('matTimepickerParse')">
        Invalid time format
    </mat-error>
    <mat-error *ngIf="form.get('time').hasError('matTimepickerMin')">
        Time must be after {{ minTime | date:'shortTime' }}
    </mat-error>
    <mat-error *ngIf="form.get('time').hasError('matTimepickerMax')">
        Time must be before {{ maxTime | date:'shortTime' }}
    </mat-error>
</mat-form-field>

Material 3 Theming

Basic Theme Setup

// styles.scss
@use '@angular/material' as mat;

html {
    // Define theme with M3 palette
    @include mat.theme(
        (
            color: mat.$violet-palette,
            typography: Roboto,
            density: 0,
        )
    );
}

Dark Mode with light-dark()

Angular Material 19 uses the CSS light-dark() function for automatic theme switching:

@use '@angular/material' as mat;

html {
    // Enable automatic light/dark switching
    color-scheme: light dark;

    @include mat.theme(
        (
            color: mat.$azure-palette,
            typography: Roboto,
            density: 0,
        )
    );
}

// OR: Force specific mode
html.light-mode {
    color-scheme: light;
}

html.dark-mode {
    color-scheme: dark;
}

Custom Color Overrides

@use '@angular/material' as mat;

html {
    color-scheme: light dark;

    @include mat.theme(
        (
            color: mat.$violet-palette,
            typography: Roboto,
            density: 0,
        ),
        $overrides: (
            // Surface colors
            surface: light-dark(#fefbff, #1c1b1f),
            on-surface: light-dark(#1c1b1f, #e6e1e5),
            surface-container: light-dark(#f3edf7, #211f26),
            // Primary colors
            primary: light-dark(#6750a4, #d0bcff),
            on-primary: light-dark(#ffffff, #381e72),
            // Error colors
            error: light-dark(#b3261e, #f2b8b5),
            on-error: light-dark(#ffffff, #601410)
        )
    );
}

Component Override Mixins

Each component has an override mixin for granular customization:

@use '@angular/material' as mat;

// Button overrides
@include mat.button-overrides(
    (
        filled-container-color: #6200ea,
        filled-label-text-color: white,
        outlined-outline-color: #6200ea,
    )
);

// Form field overrides
@include mat.form-field-overrides(
    (
        filled-container-color: rgba(0, 0, 0, 0.04),
        outlined-outline-color: #79747e,
        focused-outline-color: #6200ea,
    )
);

// Card overrides
@include mat.card-overrides(
    (
        container-color: light-dark(#ffffff, #2d2d2d),
        container-elevation: 2,
    )
);

// Dialog overrides
@include mat.dialog-overrides(
    (
        container-color: light-dark(#fefbff, #1c1b1f),
        container-shape: 28px,
    )
);

Theme Service with Signals

import { Injectable, signal, effect } from '@angular/core';

export type ThemeMode = 'light' | 'dark' | 'system';

@Injectable({ providedIn: 'root' })
export class ThemeService {
    private readonly STORAGE_KEY = 'theme-mode';
    currentTheme = signal<ThemeMode>(this.getStoredTheme());

    constructor() {
        effect(() => {
            const theme = this.currentTheme();
            this.applyTheme(theme);
            localStorage.setItem(this.STORAGE_KEY, theme);
        });
    }

    setTheme(mode: ThemeMode): void {
        this.currentTheme.set(mode);
    }

    toggleTheme(): void {
        const current = this.currentTheme();
        const next = current === 'light' ? 'dark' : 'light';
        this.currentTheme.set(next);
    }

    private applyTheme(mode: ThemeMode): void {
        const html = document.documentElement;
        html.classList.remove('light-mode', 'dark-mode');

        if (mode === 'system') {
            html.style.colorScheme = 'light dark';
        } else {
            html.style.colorScheme = mode;
            html.classList.add(`${mode}-mode`);
        }
    }

    private getStoredTheme(): ThemeMode {
        return (
            (localStorage.getItem(this.STORAGE_KEY) as ThemeMode) ?? 'system'
        );
    }
}

Theme Toggle Component

@Component({
    selector: 'app-theme-toggle',
    imports: [MatButtonModule, MatIconModule, MatMenuModule],
    template: `
        <button
            [matMenuTriggerFor]="themeMenu"
            mat-icon-button
        >
            <mat-icon>{{ themeIcon() }}</mat-icon>
        </button>
        <mat-menu #themeMenu="matMenu">
            <button
                mat-menu-item
                (click)="setTheme('light')"
            >
                <mat-icon>light_mode</mat-icon>
                <span>Light</span>
            </button>
            <button
                mat-menu-item
                (click)="setTheme('dark')"
            >
                <mat-icon>dark_mode</mat-icon>
                <span>Dark</span>
            </button>
            <button
                mat-menu-item
                (click)="setTheme('system')"
            >
                <mat-icon>settings_brightness</mat-icon>
                <span>System</span>
            </button>
        </mat-menu>
    `,
})
export class ThemeToggleComponent {
    private readonly themeService = inject(ThemeService);

    themeIcon = computed(() => {
        const theme = this.themeService.currentTheme();
        return theme === 'light'
            ? 'light_mode'
            : theme === 'dark'
              ? 'dark_mode'
              : 'settings_brightness';
    });

    setTheme(mode: ThemeMode): void {
        this.themeService.setTheme(mode);
    }
}

CDK 2D Drag & Drop (New in v19)

Mixed Orientation (Grid Layout)

import { Component } from '@angular/core';
import {
    CdkDragDrop,
    DragDropModule,
    moveItemInArray,
} from '@angular/cdk/drag-drop';

@Component({
    selector: 'app-grid-drag',
    imports: [DragDropModule],
    template: `
        <div
            class="grid-container"
            cdkDropList
            cdkDropListOrientation="mixed"
            (cdkDropListDropped)="drop($event)"
        >
            @for (item of items; track item.id) {
                <div
                    class="grid-item"
                    cdkDrag
                >
                    {{ item.name }}
                </div>
            }
        </div>
    `,
    styles: `
        .grid-container {
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            padding: 16px;
            max-width: 600px;
        }
        .grid-item {
            width: 120px;
            height: 120px;
            display: flex;
            align-items: center;
            justify-content: center;
            background: var(--mat-sys-surface-container);
            border-radius: 8px;
            cursor: move;
        }
        .cdk-drag-preview {
            box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2);
        }
        .cdk-drag-placeholder {
            opacity: 0.3;
        }
    `,
})
export class GridDragComponent {
    items = [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' },
        { id: 4, name: 'Item 4' },
        { id: 5, name: 'Item 5' },
        { id: 6, name: 'Item 6' },
    ];

    drop(event: CdkDragDrop<typeof this.items>): void {
        moveItemInArray(this.items, event.previousIndex, event.currentIndex);
    }
}

Tab Reordering

import { Component } from '@angular/core';
import {
    CdkDragDrop,
    DragDropModule,
    moveItemInArray,
} from '@angular/cdk/drag-drop';
import { MatTabsModule } from '@angular/material/tabs';

@Component({
    selector: 'app-draggable-tabs',
    imports: [MatTabsModule, DragDropModule],
    template: `
        <mat-tab-group>
            <div
                class="tab-header-container"
                cdkDropList
                cdkDropListOrientation="horizontal"
                (cdkDropListDropped)="dropTab($event)"
            >
                @for (tab of tabs; track tab.id) {
                    <mat-tab cdkDrag>
                        <ng-template mat-tab-label>
                            <span cdkDragHandle>{{ tab.label }}</span>
                        </ng-template>
                        <p>{{ tab.content }}</p>
                    </mat-tab>
                }
            </div>
        </mat-tab-group>
    `,
})
export class DraggableTabsComponent {
    tabs = [
        { id: 1, label: 'Tab 1', content: 'Content 1' },
        { id: 2, label: 'Tab 2', content: 'Content 2' },
        { id: 3, label: 'Tab 3', content: 'Content 3' },
    ];

    dropTab(event: CdkDragDrop<typeof this.tabs>): void {
        moveItemInArray(this.tabs, event.previousIndex, event.currentIndex);
    }
}

Custom Form Controls (Signal-Based)

Pattern 1: Simple ControlValueAccessor with Signals

import { Component, forwardRef, signal, computed } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
    selector: 'au-rating-input',
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => RatingInputComponent),
            multi: true,
        },
    ],
    template: `
        @for (star of stars(); track $index) {
            <button
                type="button"
                [class.filled]="$index < value()"
                [disabled]="disabled()"
                (click)="setValue($index + 1)"
            >
                ★
            </button>
        }
    `,
})
export class RatingInputComponent implements ControlValueAccessor {
    value = signal(0);
    disabled = signal(false);
    stars = computed(() => Array(5).fill(0));

    private onChange: (value: number) => void = () => {};
    private onTouched: () => void = () => {};

    writeValue(value: number): void {
        this.value.set(value ?? 0);
    }

    registerOnChange(fn: (value: number) => void): void {
        this.onChange = fn;
    }

    registerOnTouched(fn: () => void): void {
        this.onTouched = fn;
    }

    setDisabledState(isDisabled: boolean): void {
        this.disabled.set(isDisabled);
    }

    setValue(rating: number): void {
        if (this.disabled()) return;
        this.value.set(rating);
        this.onChange(rating);
        this.onTouched();
    }
}

Pattern 2: MatFormFieldControl with Signals

import {
    Component,
    forwardRef,
    signal,
    computed,
    effect,
    inject,
    ElementRef,
    OnDestroy,
} from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material/form-field';
import { FocusMonitor } from '@angular/cdk/a11y';
import { Subject } from 'rxjs';

@Component({
    selector: 'au-phone-input',
    templateUrl: './phone-input.component.html',
    providers: [
        {
            provide: MatFormFieldControl,
            useExisting: forwardRef(() => PhoneInputComponent),
        },
    ],
    host: {
        '[id]': 'id',
        '[attr.aria-describedby]': 'describedBy()',
    },
})
export class PhoneInputComponent
    implements MatFormFieldControl<string>, ControlValueAccessor, OnDestroy
{
    static nextId = 0;

    // Injections
    private readonly fm = inject(FocusMonitor);
    private readonly elRef = inject(ElementRef<HTMLElement>);
    readonly ngControl = inject(NgControl, { optional: true, self: true });

    // MatFormFieldControl required
    readonly id = `phone-input-${PhoneInputComponent.nextId++}`;
    readonly stateChanges = new Subject<void>();
    readonly controlType = 'phone-input';

    // Signal-based state
    value = signal<string>('');
    placeholder = signal<string>('');
    required = signal(false);
    disabled = signal(false);
    focused = signal(false);
    describedBy = signal('');

    // Computed properties
    empty = computed(() => !this.value());
    shouldLabelFloat = computed(() => this.focused() || !this.empty());
    errorState = computed(() => {
        const control = this.ngControl?.control;
        return !!(control?.invalid && control?.touched);
    });

    private onChange: (val: string) => void = () => {};
    private onTouched: () => void = () => {};

    constructor() {
        // Set this as value accessor
        if (this.ngControl) {
            this.ngControl.valueAccessor = this;
        }

        // Monitor focus
        this.fm.monitor(this.elRef.nativeElement, true).subscribe((origin) => {
            this.focused.set(!!origin);
            this.stateChanges.next();
        });

        // Emit state changes when signals change
        effect(() => {
            // Touch all signals to track them
            this.value();
            this.placeholder();
            this.required();
            this.disabled();
            this.focused();
            this.stateChanges.next();
        });
    }

    // MatFormFieldControl methods
    setDescribedByIds(ids: string[]): void {
        this.describedBy.set(ids.join(' '));
    }

    onContainerClick(): void {
        // Focus your input element
    }

    // ControlValueAccessor methods
    writeValue(value: string): void {
        this.value.set(value ?? '');
    }

    registerOnChange(fn: (val: string) => void): void {
        this.onChange = fn;
    }

    registerOnTouched(fn: () => void): void {
        this.onTouched = fn;
    }

    setDisabledState(isDisabled: boolean): void {
        this.disabled.set(isDisabled);
    }

    // Internal methods
    updateValue(newValue: string): void {
        this.value.set(newValue);
        this.onChange(newValue);
        this.onTouched();
    }

    ngOnDestroy(): void {
        this.stateChanges.complete();
        this.fm.stopMonitoring(this.elRef.nativeElement);
    }
}

Material Dialogs

Signal-Based Dialog Data

import { Component, inject, signal } from '@angular/core';
import { MatDialog, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';

// Dialog component
@Component({
    selector: 'confirm-dialog',
    imports: [MatDialogModule, MatButtonModule],
    template: `
        <h2 mat-dialog-title>{{ data.title }}</h2>
        <mat-dialog-content>
            <p>{{ data.message }}</p>
        </mat-dialog-content>
        <mat-dialog-actions align="end">
            <button mat-button mat-dialog-close>Cancel</button>
            <button mat-flat-button color="warn" [mat-dialog-close]="true">
                {{ data.confirmText ?? 'Confirm' }}
            </button>
        </mat-dialog-actions>
    `,
})
export class ConfirmDialogComponent
{
    readonly data = inject<ConfirmDialogData>(MAT_DIALOG_DATA);
    readonly dialogRef = inject(MatDialogRef<ConfirmDialogComponent>);
}

interface ConfirmDialogData
{
    title: string;
    message: string;
    confirmText?: string;
}

// Usage in parent component
@Component({...})
export class ParentComponent
{
    private readonly dialog = inject(MatDialog);
    isDeleting = signal(false);

    async confirmDelete(item: Item): Promise<void>
    {
        const dialogRef = this.dialog.open(ConfirmDialogComponent, {
            width: '400px',
            data: {
                title: 'Delete Item',
                message: `Are you sure you want to delete "${item.name}"?`,
                confirmText: 'Delete',
            },
        });

        const confirmed = await firstValueFrom(dialogRef.afterClosed());

        if (confirmed)
        {
            this.isDeleting.set(true);
            await this.deleteItem(item);
            this.isDeleting.set(false);
        }
    }
}

Material Table with Signals

import { Component, signal, computed, viewChild, effect } from '@angular/core';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule, MatSort, Sort } from '@angular/material/sort';
import {
    MatPaginatorModule,
    MatPaginator,
    PageEvent,
} from '@angular/material/paginator';

@Component({
    selector: 'app-users-table',
    imports: [MatTableModule, MatSortModule, MatPaginatorModule],
    template: `
        <table
            [dataSource]="dataSource()"
            mat-table
            matSort
            (matSortChange)="onSortChange($event)"
        >
            <ng-container matColumnDef="name">
                <th
                    *matHeaderCellDef
                    mat-header-cell
                    mat-sort-header
                >
                    Name
                </th>
                <td
                    *matCellDef="let row"
                    mat-cell
                >
                    {{ row.name }}
                </td>
            </ng-container>

            <ng-container matColumnDef="email">
                <th
                    *matHeaderCellDef
                    mat-header-cell
                    mat-sort-header
                >
                    Email
                </th>
                <td
                    *matCellDef="let row"
                    mat-cell
                >
                    {{ row.email }}
                </td>
            </ng-container>

            <ng-container matColumnDef="actions">
                <th
                    *matHeaderCellDef
                    mat-header-cell
                >
                    Actions
                </th>
                <td
                    *matCellDef="let row"
                    mat-cell
                >
                    <button
                        mat-icon-button
                        (click)="edit(row)"
                    >
                        <mat-icon>edit</mat-icon>
                    </button>
                </td>
            </ng-container>

            <tr
                *matHeaderRowDef="displayedColumns"
                mat-header-row
            ></tr>
            <tr
                *matRowDef="let row; columns: displayedColumns"
                mat-row
            ></tr>
        </table>

        <mat-paginator
            [length]="totalItems()"
            [pageSize]="pageSize()"
            [pageSizeOptions]="[10, 25, 50]"
            (page)="onPageChange($event)"
        ></mat-paginator>
    `,
})
export class UsersTableComponent {
    // Signal-based state
    users = signal<User[]>([]);
    totalItems = signal(0);
    pageSize = signal(10);
    pageIndex = signal(0);
    sortField = signal<string>('name');
    sortDirection = signal<'asc' | 'desc'>('asc');

    // Signal queries
    sort = viewChild(MatSort);
    paginator = viewChild(MatPaginator);

    displayedColumns = ['name', 'email', 'actions'];

    // Computed data source
    dataSource = computed(() => this.users());

    onSortChange(sort: Sort): void {
        this.sortField.set(sort.active);
        this.sortDirection.set(sort.direction as 'asc' | 'desc');
        this.loadData();
    }

    onPageChange(event: PageEvent): void {
        this.pageIndex.set(event.pageIndex);
        this.pageSize.set(event.pageSize);
        this.loadData();
    }

    private loadData(): void {
        // Fetch data based on current state
    }
}

CDK Patterns

CDK Overlay (Signal-Based)

import { Injectable, inject, Injector } from '@angular/core';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';

@Injectable({ providedIn: 'root' })
export class PreviewOverlayService {
    private readonly overlay = inject(Overlay);
    private readonly injector = inject(Injector);

    open<T>(component: Type<T>, data: any): OverlayRef {
        const positionStrategy = this.overlay
            .position()
            .global()
            .centerHorizontally()
            .centerVertically();

        const overlayConfig = new OverlayConfig({
            hasBackdrop: true,
            backdropClass: 'cdk-overlay-dark-backdrop',
            panelClass: 'preview-panel',
            scrollStrategy: this.overlay.scrollStrategies.block(),
            positionStrategy,
        });

        const overlayRef = this.overlay.create(overlayConfig);

        const injector = Injector.create({
            providers: [
                { provide: PREVIEW_DATA, useValue: data },
                { provide: OverlayRef, useValue: overlayRef },
            ],
            parent: this.injector,
        });

        const portal = new ComponentPortal(component, null, injector);
        overlayRef.attach(portal);

        // Close on backdrop click
        overlayRef.backdropClick().subscribe(() => overlayRef.dispose());

        return overlayRef;
    }
}

CDK SelectionModel with Signals

import { Component, signal, computed } from '@angular/core';
import { SelectionModel } from '@angular/cdk/collections';

@Component({...})
export class SelectableListComponent
{
    items = signal<Item[]>([]);

    selection = new SelectionModel<Item>(
        true,                          // Multiple selection
        [],                            // Initial selection
        true,                          // Emit changes
        (a, b) => a.id === b.id        // Compare function
    );

    // Computed properties
    selectedCount = computed(() => this.selection.selected.length);
    allSelected = computed(() =>
        this.items().length > 0 &&
        this.selection.selected.length === this.items().length
    );

    toggleAll(): void
    {
        if (this.allSelected())
        {
            this.selection.clear();
        }
        else
        {
            this.selection.select(...this.items());
        }
    }

    toggle(item: Item): void
    {
        this.selection.toggle(item);
    }
}

Common Imports (Angular Material 19)

// Core
import { provideNativeDateAdapter } from '@angular/material/core';

// Buttons
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFabButton, MatMiniFabButton } from '@angular/material/button';

// Forms
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRadioModule } from '@angular/material/radio';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatTimepickerModule } from '@angular/material/timepicker'; // NEW in v19
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';

// Layout
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatTabsModule } from '@angular/material/tabs';

// Navigation
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';

// Data Display
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatChipsModule } from '@angular/material/chips';
import { MatBadgeModule } from '@angular/material/badge';
import { MatTooltipModule } from '@angular/material/tooltip';

// Feedback
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatProgressBarModule } from '@angular/material/progress-bar';

// CDK
import { DragDropModule } from '@angular/cdk/drag-drop';
import { OverlayModule } from '@angular/cdk/overlay';
import { A11yModule } from '@angular/cdk/a11y';
import { PortalModule } from '@angular/cdk/portal';

Anti-Patterns

Avoid Do Instead
@ViewChild(MatSort) decorator viewChild(MatSort) signal query
Manual stateChanges.next() calls everywhere Use effect() to auto-emit on signal changes
Importing entire Material module Import only needed component modules
Using any for dialog data Create typed interfaces
Hardcoded colors in components Use --mat-sys-* CSS variables
@media (prefers-color-scheme) manually Use light-dark() function in theme
Custom dark mode class logic Set color-scheme: light dark on html
Using third-party timepicker Use native mat-timepicker (v19+)

Migration Checklist

  • Update to Material 19 package
  • Replace third-party timepicker with mat-timepicker
  • Convert @ViewChild queries to signal queries
  • Update theming to use M3 mat.theme() mixin
  • Implement dark mode with light-dark() function
  • Replace custom drag-drop with cdkDropListOrientation="mixed"
  • Use component override mixins instead of deep CSS selectors
  • Convert custom controls to use signals internally

Related Skills

Skill When to Use Together
angular-19 Angular 19 patterns, signals, resource API
tailwind Combined Tailwind + Material styling
typescript TypeScript patterns for typed components

Resources