Capítulo 28 - Parte 4: Overlays: MatDialog, MatSnackBar, MatBottomSheet¶
Parte 4 de 4 · Capítulo 28 · PARTE XIII - Librerías Esenciales del Ecosistema
Los overlays son las piezas que interrumpen al usuario de forma controlada: un dialog para confirmar una acción destructiva, un snackbar para notificar el resultado de una operación, un bottom sheet para ofrecer opciones en dispositivos táctiles. Angular Material los construye sobre el sistema de overlays del CDK, así que son accesibles, animados y programables. Veamos cómo usarlos correctamente.
MatDialog: abrir, pasar datos y recibir resultado¶
El flujo de un dialog tiene tres actores: el componente que abre, el servicio MatDialog y el componente que se muestra dentro del dialog.
// productos/confirmar-eliminacion-dialog.component.ts
import { Component, inject } from '@angular/core';
import { MAT_DIALOG_DATA,
MatDialogRef,
MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
interface DatosConfirmacion {
nombreProducto: string;
id: string;
}
@Component({
selector: 'app-confirmar-eliminacion-dialog',
standalone: true,
imports: [MatDialogModule, MatButtonModule],
template: `
<h2 mat-dialog-title>Confirmar eliminación</h2>
<mat-dialog-content>
<p>¿Estás seguro de que querés eliminar
<strong>{{ datos.nombreProducto }}</strong>?
</p>
<p class="advertencia">Esta acción no se puede deshacer.</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="false">Cancelar</button>
<button mat-flat-button color="warn" [mat-dialog-close]="true">
Eliminar
</button>
</mat-dialog-actions>
`
})
export class ConfirmarEliminacionDialogComponent {
readonly datos = inject<DatosConfirmacion>(MAT_DIALOG_DATA);
readonly dialogRef = inject(MatDialogRef<ConfirmarEliminacionDialogComponent>);
}
Y así lo usamos desde el componente padre:
// productos/tabla-productos.component.ts (fragmento)
import { MatDialog } from '@angular/material/dialog';
import { inject } from '@angular/core';
import { ConfirmarEliminacionDialogComponent } from './confirmar-eliminacion-dialog.component';
export class TablaProductosComponent {
private readonly dialog = inject(MatDialog);
private readonly productoService = inject(ProductoService);
eliminarProducto(producto: Producto): void {
const refDialog = this.dialog.open(ConfirmarEliminacionDialogComponent, {
width: '420px',
disableClose: true,
data: { nombreProducto: producto.nombre, id: producto.id }
});
refDialog.afterClosed().subscribe((confirmado: boolean) => {
if (confirmado) {
this.productoService.eliminar(producto.id).subscribe(() => {
this.cargarProductos();
});
}
});
}
}
disableClose: true impide que el usuario cierre el dialog con Escape o haciendo clic fuera; útil en formularios que requieren una decisión explícita. [mat-dialog-close]="false" vincula el botón de cancelar al cierre del dialog y pasa false como resultado. El observable afterClosed() emite ese valor cuando el dialog se destruye.
MatSnackBar: notificaciones rápidas¶
El snackbar es la notificación menos intrusiva. Aparece brevemente en la parte inferior de la pantalla y desaparece solo:
// shared/notificacion.service.ts
import { Injectable, inject } from '@angular/core';
import { MatSnackBar,
MatSnackBarConfig } from '@angular/material/snack-bar';
@Injectable({ providedIn: 'root' })
export class NotificacionService {
private readonly snackBar = inject(MatSnackBar);
private readonly configBase: MatSnackBarConfig = {
duration: 4000,
horizontalPosition: 'end',
verticalPosition: 'bottom'
};
exito(mensaje: string): void {
this.snackBar.open(mensaje, 'Cerrar', {
...this.configBase,
panelClass: ['snack-exito']
});
}
error(mensaje: string): void {
this.snackBar.open(mensaje, 'Reintentar', {
...this.configBase,
duration: 0, // No se cierra solo, el usuario debe actuar
panelClass: ['snack-error']
});
}
con(mensaje: string, accion: string): Promise<boolean> {
const ref = this.snackBar.open(mensaje, accion, this.configBase);
return ref.onAction().pipe(
map(() => true)
).toPromise() as Promise<boolean>;
}
}
Cuando duration es 0, el snackbar persiste hasta que el usuario interactúe, lo cual tiene sentido para errores que requieren acción.
MatBottomSheet: acciones en móvil¶
El bottom sheet sube desde la parte inferior de la pantalla y es ideal en dispositivos táctiles para ofrecer un conjunto de acciones sin un menú flotante:
// productos/acciones-producto-sheet.component.ts
import { Component, inject } from '@angular/core';
import { MAT_BOTTOM_SHEET_DATA,
MatBottomSheetRef,
MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
interface AccionProducto { etiqueta: string; icono: string; valor: string; }
@Component({
selector: 'app-acciones-producto-sheet',
standalone: true,
imports: [MatBottomSheetModule, MatListModule, MatIconModule],
template: `
<mat-nav-list>
@for (accion of datos.acciones; track accion.valor) {
<a mat-list-item (click)="seleccionar(accion.valor)">
<mat-icon matListItemIcon>{{ accion.icono }}</mat-icon>
<span matListItemTitle>{{ accion.etiqueta }}</span>
</a>
}
</mat-nav-list>
`
})
export class AccionesProductoSheetComponent {
readonly datos = inject<{ acciones: AccionProducto[] }>(MAT_BOTTOM_SHEET_DATA);
private readonly sheetRef = inject(MatBottomSheetRef<AccionesProductoSheetComponent>);
seleccionar(valor: string): void {
this.sheetRef.dismiss(valor);
}
}
// productos/tabla-productos.component.ts (fragmento bottom sheet)
import { MatBottomSheet } from '@angular/material/bottom-sheet';
export class TablaProductosComponent {
private readonly bottomSheet = inject(MatBottomSheet);
abrirAcciones(producto: Producto): void {
const ref = this.bottomSheet.open(AccionesProductoSheetComponent, {
data: {
acciones: [
{ etiqueta: 'Editar', icono: 'edit', valor: 'editar' },
{ etiqueta: 'Duplicar', icono: 'copy_all', valor: 'duplicar' },
{ etiqueta: 'Eliminar', icono: 'delete', valor: 'eliminar' },
]
}
});
ref.afterDismissed().subscribe((accionSeleccionada: string | undefined) => {
if (accionSeleccionada === 'eliminar') {
this.eliminarProducto(producto);
}
});
}
}
MatTooltipModule¶
Los tooltips son la forma más ligera de overlay: aparecen al hacer hover o al recibir foco por teclado, sin necesidad de código TypeScript:
<button mat-icon-button
matTooltip="Eliminar producto"
matTooltipPosition="above"
(click)="eliminarProducto(prod)">
<mat-icon>delete</mat-icon>
</button>
matTooltipPosition acepta 'above', 'below', 'left' y 'right'. En dispositivos táctiles el tooltip aparece al mantener presionado el elemento.
Flujo completo del dialog de confirmación¶
sequenceDiagram
participant U as Usuario
participant T as TablaProductos
participant D as MatDialog
participant C as ConfirmarEliminacionDialog
participant S as ProductoService
U->>T: Click "Eliminar"
T->>D: open(ConfirmarEliminacion, { data })
D->>C: Instancia el componente
C-->>U: Muestra el dialog
U->>C: Click "Eliminar" (confirmar)
C->>D: mat-dialog-close="true"
D-->>T: afterClosed() emite: true
T->>S: eliminar(id)
S-->>T: Observable completado
T-->>U: Tabla actualizada + SnackBar "Eliminado"
Puntos clave¶
MAT_DIALOG_DATAyMAT_BOTTOM_SHEET_DATAson tokens de inyección; usarlos coninject<T>(TOKEN)y el tipo genérico correcto evita el uso deany.afterClosed()emiteundefinedsi el dialog se cierra con Escape o clic en el backdrop (cuandodisableCloseesfalse). Siempre verificar el valor antes de actuar.- Centralizar el snackbar en un servicio (
NotificacionService) evita duplicar configuración y permite cambiar el comportamiento global en un solo lugar. MatBottomSheetRef.dismiss(valor)es el equivalente deMatDialogRef.close(valor); el valor se recibe enafterDismissed().- Los tooltips son accesibles por defecto: aparecen en foco por teclado y tienen el rol ARIA correcto.
¿Qué sigue?¶
En el capítulo 29 bajamos un nivel y exploramos el CDK directamente: construiremos overlays a medida con Overlay y Portal, aprenderemos Drag and Drop para tableros kanban y dominaremos las herramientas de accesibilidad que hacen a nuestros componentes usables por todos.