Consistent feedback UX — loading states, success confirmations, and error messages — requires a centralised notification system so every component does not independently manage its own toast/snackbar calls. A NotificationService wrapping MatSnackBar provides typed methods (success(), error(), info()) that any service or component can call with a single line. Combined with MatProgressBar for page-level loading and MatProgressSpinner for inline loading states, this gives the BlogApp production-quality feedback patterns.
NotificationService and Progress Indicators
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
// ── Notification service — centralises all toast messages ─────────────────
@Injectable({ providedIn: 'root' })
export class NotificationService {
private snackBar = inject(MatSnackBar);
private show(message: string, panelClass: string, config?: MatSnackBarConfig): void {
this.snackBar.open(message, 'Dismiss', {
duration: 4000,
horizontalPosition: 'end',
verticalPosition: 'top',
panelClass: [panelClass],
...config,
});
}
success(message: string): void { this.show(message, 'snack-success'); }
error(message: string): void { this.show(message, 'snack-error', { duration: 8000 }); }
info(message: string): void { this.show(message, 'snack-info'); }
warn(message: string): void { this.show(message, 'snack-warn'); }
// With action button (returns reference for result Observable)
confirm(message: string, action = 'Undo'): Observable<void> {
const ref = this.snackBar.open(message, action, {
duration: 5000,
panelClass: ['snack-info'],
});
return ref.onAction(); // emits when user clicks the action button
}
}
// ── Global styles for snackbar panels ──────────────────────────────────────
// styles.scss:
// .snack-success .mdc-snackbar__surface { background: var(--mat-sys-primary) !important; }
// .snack-error .mdc-snackbar__surface { background: var(--mat-sys-error) !important; }
// .snack-warn .mdc-snackbar__surface { background: var(--mat-sys-tertiary) !important; }
// ── Page-level progress bar ────────────────────────────────────────────────
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, MatProgressBarModule],
template: `
<!-- Shows during route navigation ─────────────────────────────────── -->
@if (navLoading.navigating()) {
<mat-progress-bar mode="indeterminate" class="nav-progress" />
}
<app-shell>
<router-outlet />
</app-shell>
`,
styles: [`.nav-progress { position: fixed; top: 0; left: 0; right: 0; z-index: 1000; }`],
})
export class AppComponent {
navLoading = inject(NavigationLoadingService);
}
// ── Button with spinner loading state ─────────────────────────────────────
@Component({
standalone: true,
imports: [MatButtonModule, MatProgressSpinnerModule],
template: `
<button mat-raised-button color="primary"
[disabled]="isSaving()"
(click)="onSave()">
@if (isSaving()) {
<mat-spinner diameter="18" class="btn-spinner" />
Saving...
} @else {
<mat-icon>save</mat-icon>
Save Post
}
</button>
`,
styles: [`.btn-spinner { display: inline-block; margin-right: 0.5rem; }`],
})
export class SaveButtonComponent {
isSaving = signal(false);
private notify = inject(NotificationService);
onSave(): void {
this.isSaving.set(true);
this.api.savePost(this.form.getRawValue()).subscribe({
next: () => {
this.isSaving.set(false);
this.notify.success('Post saved successfully!');
},
error: () => {
this.isSaving.set(false);
this.notify.error('Failed to save post. Please try again.');
},
});
}
}
MatSnackBar.open() returns a MatSnackBarRef. Call ref.onAction() to get an Observable that emits when the user clicks the action button — perfect for undo functionality: show “Post deleted”, Undo button; if user clicks Undo within 5 seconds, call the restore API. ref.afterDismissed() emits when the snackbar closes (timeout or user dismiss). Use these for confirmation flows that need the snackbar result without a full dialog.styles.scss (global, not component-scoped) because Angular Material renders snackbars in an overlay portal outside the component’s DOM — component-scoped styles cannot reach them. Use the MDC surface class selector: .snack-success .mdc-snackbar__surface { background: green } for Angular Material 15+. The panelClass option adds CSS classes to the snackbar container, which you then style globally.Common Mistakes
Mistake 1 — Styling snackbar panels in component SCSS (styles don’t apply)
❌ Wrong — .snack-error { background: red } in component.scss; snackbar is in a portal outside component DOM; no effect.
✅ Correct — put snackbar panel styles in global styles.scss.
Mistake 2 — Showing snackbar notifications for every HTTP call (notification fatigue)
❌ Wrong — showing a “Loaded posts” snackbar every time the list loads; users dismiss them reflexively after the first few.
✅ Correct — reserve snackbars for user-triggered mutations (save, delete, publish) and errors, not routine data loads.