Core Material Components — Navigation, Cards, Tables and Dialogs

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);
  }
}
Note: 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.
Tip: For the MatTable to work with an HTTP data source, use 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.
Warning: 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.

🧠 Test Yourself

A mat-sidenav is set to mode="over". The user clicks a nav link inside it. Does the sidenav close automatically?