Angular Material and CDK — Forms, Dialogs, and Drag-and-Drop

Angular Material is Google’s official implementation of the Material Design component library for Angular — providing 50+ production-ready, accessible, themeable UI components. The Angular CDK (Component Dev Kit) is the lower-level toolkit that Material itself is built on — providing behaviours like overlay positioning, drag-and-drop, virtual scrolling, accessibility utilities, and more — without any visual design. For a MEAN Stack task manager, Angular Material provides everything from form fields and buttons to drag-and-drop task boards, without writing a single line of custom CSS for common UI patterns.

Angular Material Component Categories

Category Components
Form Controls MatInput, MatSelect, MatCheckbox, MatRadio, MatSlider, MatDatepicker, MatAutocomplete
Navigation MatToolbar, MatSidenav, MatMenu, MatTabs, MatStepper
Layout MatCard, MatExpansionPanel, MatList, MatGrid, MatDivider
Buttons & Indicators MatButton, MatIconButton, MatFab, MatBadge, MatChips, MatProgressBar, MatSpinner
Popups & Modals MatDialog, MatSnackBar, MatTooltip, MatBottomSheet
Data Table MatTable, MatSort, MatPaginator, MatFilter
CDK Behaviours DragDrop, Overlay, Portal, Clipboard, Breakpoints, A11y, VirtualScroll
Note: Angular Material v17+ supports M3 (Material Design 3) theming. Set up theming in styles.scss with @use '@angular/material' as mat and mat.theme() to define your colour palette, typography, and density. The new M3 system uses CSS custom properties throughout, making runtime theme switching (light/dark) trivial — just swap the CSS class on the <html> element. Always include only the Material modules your application uses — individual imports, not the entire MaterialModule.
Tip: Use MatDialog for confirmation dialogs instead of window.confirm(). MatDialog.open(ConfirmDialogComponent, { data: { title, message } }) returns a dialog reference whose afterClosed() Observable emits the user’s choice. This gives you a beautiful, accessible, mobile-friendly dialog instead of the browser’s native unstyled confirm popup, and allows the same dialog component to be reused throughout the application.
Warning: Angular Material’s MatFormField requires Angular Material’s theme to be imported in your global styles.scss. Without the theme import, form fields render with no styling — plain unstyled inputs. Also, Material components import their own animations — ensure provideAnimations() is in your app.config.ts. Missing animations causes menus and dialogs to appear and disappear abruptly without transitions.

Complete Angular Material + CDK Examples

# Install Angular Material
ng add @angular/material
# Prompts: choose theme, set up typography, enable animations
# Adds Material CSS to styles.scss and provideAnimations() to app.config.ts
// ── Task form with Material components ───────────────────────────────────
import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule }     from '@angular/material/input';
import { MatSelectModule }    from '@angular/material/select';
import { MatDatepickerModule} from '@angular/material/datepicker';
import { MatNativeDateModule} from '@angular/material/core';
import { MatButtonModule }    from '@angular/material/button';
import { MatChipsModule }     from '@angular/material/chips';
import { MatIconModule }      from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { COMMA, ENTER }       from '@angular/cdk/keycodes';

@Component({
    selector:   'app-material-task-form',
    standalone: true,
    imports: [
        ReactiveFormsModule,
        MatFormFieldModule, MatInputModule, MatSelectModule,
        MatDatepickerModule, MatNativeDateModule, MatButtonModule,
        MatChipsModule, MatIconModule, MatProgressSpinnerModule,
    ],
    template: `
        <form [formGroup]="form" (ngSubmit)="onSubmit()">

            <!-- Title with Material form field -->
            <mat-form-field appearance="outline" class="full-width">
                <mat-label>Task Title</mat-label>
                <input matInput formControlName="title"
                       placeholder="Enter task title">
                <mat-icon matPrefix>task_alt</mat-icon>
                <mat-hint>{{ form.get('title')?.value?.length ?? 0 }}/200</mat-hint>
                <mat-error *ngIf="form.get('title')?.hasError('required')">
                    Title is required
                </mat-error>
                <mat-error *ngIf="form.get('title')?.hasError('maxlength')">
                    Title cannot exceed 200 characters
                </mat-error>
            </mat-form-field>

            <!-- Priority select -->
            <mat-form-field appearance="outline">
                <mat-label>Priority</mat-label>
                <mat-select formControlName="priority">
                    <mat-option value="low">🟢 Low</mat-option>
                    <mat-option value="medium">🟡 Medium</mat-option>
                    <mat-option value="high">🔴 High</mat-option>
                </mat-select>
            </mat-form-field>

            <!-- Due date picker -->
            <mat-form-field appearance="outline">
                <mat-label>Due Date</mat-label>
                <input matInput [matDatepicker]="picker" formControlName="dueDate">
                <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
                <mat-datepicker #picker></mat-datepicker>
            </mat-form-field>

            <!-- Tags chips -->
            <mat-chip-grid #chipGrid aria-label="Task tags">
                @for (tag of tags(); track tag) {
                    <mat-chip-row (removed)="removeTag(tag)">
                        {{ tag }}
                        <button matChipRemove [attr.aria-label]="'remove ' + tag">
                            <mat-icon>cancel</mat-icon>
                        </button>
                    </mat-chip-row>
                }
                <input placeholder="Add tag..."
                       [matChipInputFor]="chipGrid"
                       [matChipInputSeparatorKeyCodes]="separatorCodes"
                       (matChipInputTokenEnd)="addTag($event)">
            </mat-chip-grid>

            <!-- Submit button -->
            <button mat-raised-button color="primary"
                    type="submit"
                    [disabled]="form.invalid || submitting()">
                @if (submitting()) {
                    <mat-spinner diameter="20"></mat-spinner>
                } @else {
                    <mat-icon>save</mat-icon> Save Task
                }
            </button>

        </form>
    `,
})
export class MaterialTaskFormComponent {
    private fb = inject(FormBuilder);
    tags   = signal<string[]>([]);
    submitting = signal(false);
    readonly separatorCodes = [ENTER, COMMA] as const;

    form = this.fb.group({
        title:    ['', [Validators.required, Validators.maxLength(200)]],
        priority: ['medium', Validators.required],
        dueDate:  [null],
    });

    addTag(event: any): void {
        const v = (event.value ?? '').trim();
        if (v) this.tags.update(t => [...t, v]);
        event.chipInput?.clear();
    }
    removeTag(tag: string): void {
        this.tags.update(t => t.filter(x => x !== tag));
    }
    onSubmit(): void { /* ... */ }
}

// ── MatDialog for confirmation ────────────────────────────────────────────
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from './confirm-dialog.component';

@Component({ ... })
export class TaskListComponent {
    private dialog = inject(MatDialog);

    deleteTask(taskId: string): void {
        const ref = this.dialog.open(ConfirmDialogComponent, {
            width: '400px',
            data: { title: 'Delete Task', message: 'This cannot be undone.' },
        });
        ref.afterClosed().subscribe((confirmed: boolean) => {
            if (confirmed) this.store.delete(taskId);
        });
    }
}

// ── CDK Drag and Drop — Kanban board ─────────────────────────────────────
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

@Component({
    standalone: true,
    imports: [DragDropModule, CommonModule],
    template: `
        <div class="kanban">
            @for (column of columns; track column.id) {
                <div class="kanban__column">
                    <h3>{{ column.title }}</h3>
                    <div cdkDropList
                         [id]="column.id"
                         [cdkDropListData]="column.tasks"
                         [cdkDropListConnectedTo]="connectedLists"
                         (cdkDropListDropped)="onDrop($event)">
                        @for (task of column.tasks; track task._id) {
                            <div cdkDrag class="kanban__card">
                                <span cdkDragHandle>⋮⋮</span>
                                {{ task.title }}
                            </div>
                        }
                    </div>
                </div>
            }
        </div>
    `,
})
export class KanbanBoardComponent {
    columns = [
        { id: 'pending',     title: 'Pending',     tasks: [] as Task[] },
        { id: 'in-progress', title: 'In Progress',  tasks: [] as Task[] },
        { id: 'completed',   title: 'Completed',   tasks: [] as Task[] },
    ];

    get connectedLists() { return this.columns.map(c => c.id); }

    onDrop(event: CdkDragDrop<Task[]>): void {
        if (event.previousContainer === event.container) {
            moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
        } else {
            transferArrayItem(
                event.previousContainer.data,
                event.container.data,
                event.previousIndex,
                event.currentIndex,
            );
            // Update task status based on new column
            const task   = event.container.data[event.currentIndex];
            const newStatus = event.container.id as Task['status'];
            this.store.update(task._id, { status: newStatus });
        }
    }
}

How It Works

Step 1 — MatFormField Wraps Native Inputs with Material Styling

The mat-form-field component wraps any matInput directive, mat-select, or mat-datepicker, providing the floating label, underline, hint, and error display automatically. Adding matInput to a native <input> integrates it with the Material form field system — it communicates its state (focus, error, disabled) to the parent mat-form-field so the label floats and errors display correctly.

Step 2 — MatFormField Reads Validation State from FormControl

mat-error elements inside a mat-form-field are only displayed when the form control is both invalid AND touched. Material reads this state directly from the FormControl connected via formControlName — no need for manual *ngIf="ctrl.invalid && ctrl.touched" wrappers on each error element. The mat-hint is displayed when there is no error.

Step 3 — MatDialog Manages Overlays and Focus Trapping

MatDialog.open(Component, config) creates an overlay over the current content, renders the dialog component inside it, traps keyboard focus (Tab/Shift+Tab cycle within the dialog), handles Escape key to close, and restores focus to the triggering element when closed. This accessible behaviour would require significant manual implementation without Material. The afterClosed() Observable emits the value passed to dialogRef.close(result).

Step 4 — CDK DragDrop Manages Drag State and Transfers

cdkDropList marks a container as a drop zone. cdkDrag marks items as draggable. When an item is dropped, cdkDropListDropped fires with a CdkDragDrop event containing the previous and current containers and indices. moveItemInArray() reorders within the same list; transferArrayItem() moves between lists. cdkDropListConnectedTo specifies which other lists can receive items from this list.

Step 5 — CDK Clipboard and Breakpoints for Utility Behaviours

The CDK provides utility behaviours beyond visual components: Clipboard.copy(text) copies to the clipboard without the deprecated execCommand API; BreakpointObserver.observe('(max-width: 768px)') provides a reactive Observable of media query matches; FocusTrap traps keyboard focus in a region; LiveAnnouncer posts messages to screen reader ARIA live regions. These non-visual behaviours are the CDK’s most underused but valuable features.

Quick Reference

Task Import / Usage
Install Material ng add @angular/material
Form field MatFormFieldModule<mat-form-field><input matInput formControlName="x"></mat-form-field>
Select MatSelectModule<mat-select formControlName="x"><mat-option value="v">Label</mat-option></mat-select>
Dialog inject(MatDialog).open(Comp, config).afterClosed().subscribe(result => ...)
Snackbar inject(MatSnackBar).open('Message', 'Dismiss', { duration: 3000 })
Drag and drop DragDropModulecdkDropList + cdkDrag + (cdkDropListDropped)
Chips MatChipsModule<mat-chip-grid><mat-chip-row></mat-chip-row></mat-chip-grid>
Responsive breakpoints inject(BreakpointObserver).observe('(max-width: 768px)')
Clipboard inject(Clipboard).copy(text)

🧠 Test Yourself

A mat-form-field wraps a formControlName="title" input. The control has a required validator. The user submits without entering anything. When does the mat-error display?