Angular Components in Practice: Build a Reusable Confirm Dialog
Reusable components are one of the biggest productivity wins in Angular. Instead of copying the same modal, button, and confirmation logic across multiple pages, you can build one focused component and use it wherever a user action needs confirmation.
In this article, we will build a practical ConfirmDialogComponent for Angular. It will accept inputs such as a title, message, and button labels, and it will emit events when the user confirms or cancels. The example is intentionally small, but the same pattern scales well in real applications.
What we are building
Imagine a dashboard where users can delete projects, archive invoices, or deactivate accounts. These actions should not run immediately after a click. A confirmation dialog gives the user one last chance to cancel.
Our component will support:
- A customizable title.
- A customizable message.
- Separate confirm and cancel buttons.
- Event outputs for
confirmandcancel. - Simple conditional rendering from a parent component.
Create the confirm dialog component
First, generate a standalone Angular component:
ng generate component shared/confirm-dialog --standalone
Now update the component TypeScript file:
import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'app-confirm-dialog', standalone: true, templateUrl: './confirm-dialog.component.html', styleUrl: './confirm-dialog.component.css' }) export class ConfirmDialogComponent { @Input() title = 'Please confirm'; @Input() message = 'Are you sure you want to continue?'; @Input() confirmLabel = 'Confirm'; @Input() cancelLabel = 'Cancel'; @Output() confirmed = new EventEmitter<void>(); @Output() cancelled = new EventEmitter<void>(); onConfirm(): void { this.confirmed.emit(); } onCancel(): void { this.cancelled.emit(); } }
The @Input() properties let the parent component pass data into the dialog. The @Output() properties let the dialog notify the parent when something happens.
This keeps the component reusable. The dialog does not know whether it is deleting a user, archiving a post, or cancelling an order. It only knows how to ask a question and report the answer.
Add the dialog template
Next, create the HTML for the dialog:
<div class="dialog-backdrop" role="presentation"> <section class="dialog" role="dialog" aria-modal="true" [attr.aria-label]="title"> <h2>{{ title }}</h2> <p>{{ message }}</p> <div class="dialog-actions"> <button type="button" class="btn btn-secondary" (click)="onCancel()"> {{ cancelLabel }} </button> <button type="button" class="btn btn-danger" (click)="onConfirm()"> {{ confirmLabel }} </button> </div> </section> </div>
The template uses normal Angular interpolation with {{ title }} and {{ message }}. The click handlers call methods from the component class, which then emit events to the parent.
The role="dialog" and aria-modal="true" attributes are useful accessibility hints. In a production application, you may also want focus trapping and keyboard support for the Escape key, but this version keeps the core component pattern easy to understand.
Style the component
Add basic CSS to make the dialog usable:
.dialog-backdrop { position: fixed; inset: 0; display: grid; place-items: center; padding: 1rem; background: rgba(15, 23, 42, 0.55); z-index: 1000; } .dialog { width: min(100%, 420px); padding: 1.5rem; border-radius: 12px; background: #ffffff; box-shadow: 0 20px 50px rgba(15, 23, 42, 0.25); } .dialog h2 { margin: 0 0 0.75rem; font-size: 1.25rem; } .dialog p { margin: 0 0 1.25rem; color: #475569; line-height: 1.5; } .dialog-actions { display: flex; justify-content: flex-end; gap: 0.75rem; } .btn { border: 0; border-radius: 8px; padding: 0.65rem 1rem; cursor: pointer; font-weight: 600; } .btn-secondary { background: #e2e8f0; color: #0f172a; } .btn-danger { background: #dc2626; color: #ffffff; }
This CSS uses a fixed backdrop and centers the dialog with CSS Grid. The component is intentionally self-contained, which makes it easier to move between features or applications.
Use the dialog from a parent component
Now let’s use the dialog in a page that displays a list of projects. The parent component controls when the dialog is visible and what happens after the user confirms.
import { Component } from '@angular/core'; import { NgFor, NgIf } from '@angular/common'; import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component'; type Project = { id: number; name: string; }; @Component({ selector: 'app-projects', standalone: true, imports: [NgFor, NgIf, ConfirmDialogComponent], templateUrl: './projects.component.html' }) export class ProjectsComponent { projects: Project[] = [ { id: 1, name: 'Marketing Website' }, { id: 2, name: 'Admin Dashboard' }, { id: 3, name: 'Customer Portal' } ]; showDeleteDialog = false; selectedProject: Project | null = null; askToDelete(project: Project): void { this.selectedProject = project; this.showDeleteDialog = true; } cancelDelete(): void { this.selectedProject = null; this.showDeleteDialog = false; } confirmDelete(): void { if (!this.selectedProject) { return; } this.projects = this.projects.filter( project => project.id !== this.selectedProject!.id ); this.selectedProject = null; this.showDeleteDialog = false; } }
Notice that the child component does not delete anything. The parent owns the data and business logic. This is a good Angular habit: reusable UI components should usually receive data and emit events, while container or page components handle application behavior.
Add the parent template
Here is the template for the projects page:
<h2>Projects</h2> <ul class="project-list"> <li *ngFor="let project of projects"> <span>{{ project.name }}</span> <button type="button" (click)="askToDelete(project)"> Delete </button> </li> </ul> <app-confirm-dialog *ngIf="showDeleteDialog" title="Delete project?" [message]="'This will permanently delete ' + selectedProject?.name + '.'" confirmLabel="Delete" cancelLabel="Keep project" (confirmed)="confirmDelete()" (cancelled)="cancelDelete()" />
The *ngIf directive controls whether the dialog exists in the DOM. When showDeleteDialog is true, Angular renders the dialog. When the user confirms or cancels, the parent updates the state and the dialog disappears.
The [message] binding is dynamic because it includes the selected project name. The other values are static strings, so they can be passed directly.
A cleaner version with a computed message
For very small examples, building a string in the template is acceptable. In production code, it is often cleaner to move that logic into the component class:
get deleteMessage(): string { if (!this.selectedProject) { return ''; } return `This will permanently delete ${this.selectedProject.name}.`; }
Then the template becomes easier to read:
<app-confirm-dialog *ngIf="showDeleteDialog" title="Delete project?" [message]="deleteMessage" confirmLabel="Delete" cancelLabel="Keep project" (confirmed)="confirmDelete()" (cancelled)="cancelDelete()" />
This is a useful rule for Angular templates: keep simple binding in the HTML, but move anything that starts to look like logic back into TypeScript.
Common mistakes to avoid
- Putting business logic inside the dialog. The dialog should not call an API directly or know what type of item is being deleted.
- Making the component too specific. Names like
DeleteProjectDialogComponentare fine for one feature, but less reusable across the app. - Forgetting to reset state. After confirm or cancel, clear
selectedProjectso old data does not leak into the next action. - Overusing global services. A service-based modal system can be useful later, but inputs and outputs are easier for junior teams to understand and test.
When to use this pattern
This input/output pattern is best when a parent component needs to control a reusable child component. It works especially well for dialogs, dropdowns, cards, filters, form controls, and reusable action panels.
Use @Input() when the parent passes information down. Use @Output() when the child reports an event up. That one-way flow makes your Angular components easier to reason about because data and events move in predictable directions.
Conclusion
A reusable Angular component does not need to be complicated. The key is to give it a focused job. In this example, the confirm dialog displays a message and emits a user decision. The parent decides what that decision means.
For junior and mid-level developers, this is one of the most valuable Angular patterns to practice. Once you are comfortable with @Input(), @Output(), conditional rendering, and parent-owned state, you can build cleaner features with less duplicated code.
Leave a Reply