Angular Material’s core components — toolbar, sidenav, card, table, and dialog — are the building blocks of any data-driven Angular application. The BlogApp admin uses mat-sidenav for the navigation shell, mat-table for the post management list, MatDialog for confirmation and edit modals, and mat-card for the public post listing. Understanding these components and how they compose gives you a production-quality UI foundation.
App Shell with Toolbar and Sidenav
// app-shell.component.ts
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-shell',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive,
MatToolbarModule, MatSidenavModule,
MatListModule, MatIconModule, MatButtonModule],
template: `
<mat-sidenav-container class="app-container">
<!-- Side navigation ───────────────────────────────────────────── -->
<mat-sidenav #sidenav [mode]="sidenavMode()" [opened]="sidenavOpen()"
(closedStart)="sidenavOpen.set(false)">
<mat-nav-list>
<a mat-list-item routerLink="/posts" routerLinkActive="active-link"
(click)="closeSidenavOnMobile()">
<mat-icon matListItemIcon>article</mat-icon>
<span matListItemTitle>Posts</span>
</a>
@if (auth.isLoggedIn()) {
<a mat-list-item routerLink="/admin" routerLinkActive="active-link">
<mat-icon matListItemIcon>dashboard</mat-icon>
<span matListItemTitle>Admin</span>
</a>
}
</mat-nav-list>
</mat-sidenav>
<!-- Main content area ─────────────────────────────────────────── -->
<mat-sidenav-content>
<mat-toolbar color="primary">
<button mat-icon-button (click)="toggleSidenav()"
aria-label="Toggle navigation">
<mat-icon>menu</mat-icon>
</button>
<span class="toolbar-title">BlogApp</span>
<span class="spacer"></span>
@if (auth.isLoggedIn()) {
<button mat-button [matMenuTriggerFor]="userMenu">
{{ auth.displayName() }}
<mat-icon>expand_more</mat-icon>
</button>
<mat-menu #userMenu>
<a mat-menu-item routerLink="/profile">
<mat-icon>person</mat-icon>Profile
</a>
<button mat-menu-item (click)="auth.logout()">
<mat-icon>logout</mat-icon>Sign Out
</button>
</mat-menu>
} @else {
<a mat-button routerLink="/auth/login">Sign In</a>
}
</mat-toolbar>
<main class="main-content">
<router-outlet />
</main>
</mat-sidenav-content>
</mat-sidenav-container>
`,
styles: [`
.app-container { position: absolute; top: 0; bottom: 0; left: 0; right: 0; }
.main-content { padding: 1.5rem; max-width: 1200px; margin: 0 auto; }
.spacer { flex: 1; }
.active-link { background: var(--mat-sys-secondary-container); }
`],
})
export class AppShellComponent {
auth = inject(AuthService);
sidenavOpen = signal(false);
sidenavMode = signal<'over' | 'side'>('side');
toggleSidenav() { this.sidenavOpen.update(v => !v); }
closeSidenavOnMobile() {
if (this.sidenavMode() === 'over') this.sidenavOpen.set(false);
}
}
mat-sidenav has two modes: over (overlays the content, has a backdrop — mobile) and side (pushes the content — desktop). Switching between modes based on screen size creates a responsive navigation: desktop users see a permanent side navigation while mobile users get a hamburger-triggered overlay. This is implemented by reading the breakpoint from BreakpointObserver (Lesson 3) and setting the sidenavMode signal accordingly.MatTableDataSource which wraps an array and provides built-in sorting (MatSort) and pagination (MatPaginator) integration. Set the data with dataSource.data = posts when data arrives. Connect the paginator: ngAfterViewInit() { this.dataSource.paginator = this.paginator; }. The DataSource abstraction also supports server-side sorting and pagination by implementing DataSource<T> directly and responding to sort/page events with new API calls.MatDialog creates components in a separate Angular portal — the dialog component is not a child of the component that opened it. This means services injected in the dialog use the root injector, not component-level providers. If you need to pass data into a dialog, use the MAT_DIALOG_DATA injection token: dialog.open(ConfirmComponent, { data: { title, message } }) and inject with data = inject(MAT_DIALOG_DATA) in the dialog component.MatTable for Admin Post Management
// Posts admin table (key parts)
@Component({
standalone: true,
imports: [MatTableModule, MatSortModule, MatPaginatorModule,
MatButtonModule, MatIconModule, MatChipsModule],
template: `
<mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let post">{{ post.title }}</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
<mat-cell *matCellDef="let post">
<mat-chip [class]="'status-' + post.status">{{ post.status }}</mat-chip>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let post">
<button mat-icon-button [routerLink]="['/admin/posts', post.id, 'edit']">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="confirmDelete(post)">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator [pageSizeOptions]="[10, 25, 50]" showFirstLastButtons />
`,
})
export class PostsAdminComponent {
displayedColumns = ['title', 'status', 'actions'];
dataSource = new MatTableDataSource<PostSummaryDto>([]);
}
Common Mistakes
Mistake 1 — Setting dataSource.data before paginator is connected (pagination ignored)
❌ Wrong — assigning dataSource.data in ngOnInit before @ViewChild paginator is available.
✅ Correct — connect paginator in ngAfterViewInit: this.dataSource.paginator = this.paginator, then set data.
Mistake 2 — Passing plain objects instead of MAT_DIALOG_DATA (undefined in dialog)
❌ Wrong — trying to access dialog constructor arguments directly; dialog components are created outside the component tree.
✅ Correct — pass data via { data: { ... } } in open() and inject with inject(MAT_DIALOG_DATA) in the dialog.