- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
25 KiB
@isa/ui/dialog
A comprehensive dialog system for Angular applications built on Angular CDK Dialog with preset components for common use cases.
Overview
The Dialog library provides a flexible, type-safe dialog system with five pre-built dialog components covering common scenarios: messages, confirmations, text input, number input, and feedback. Built on Angular CDK Dialog, it offers a simple injection-based API, customizable styling, backdrop control, and full TypeScript support for data and result types.
Table of Contents
- Features
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Dialog Presets
- Advanced Usage
- Testing
- Architecture Notes
Features
- 5 preset dialog types - Message, Confirmation, TextInput, NumberInput, Feedback
- Type-safe API - Full TypeScript support for data and result types
- Injection-based API - Use
injectDialog()in component constructors or dependency injection - CDK Dialog foundation - Built on @angular/cdk/dialog for robust dialog management
- Customizable appearance - Control title, width, height, backdrop, and CSS classes
- Auto-close support - Optional automatic dismiss for feedback dialogs
- Form validation - Built-in validation support for input dialogs
- Flexible return types - Type-safe result handling with DialogRef
- Backdrop control - Enable/disable backdrop clicks to close
- Custom content - Extend base components for custom dialog implementations
Quick Start
1. Import and Inject Dialog
import { Component, inject } from '@angular/core';
import { injectMessageDialog } from '@isa/ui/dialog';
@Component({
selector: 'app-my-component',
template: `<button (click)="showMessage()">Show Message</button>`
})
export class MyComponent {
#openMessageDialog = injectMessageDialog();
showMessage() {
this.#openMessageDialog({
title: 'Information',
data: {
message: 'Operation completed successfully!'
}
});
}
}
2. Confirmation Dialog
import { injectConfirmationDialog } from '@isa/ui/dialog';
@Component({
selector: 'app-delete-button',
template: `<button (click)="confirmDelete()">Delete</button>`
})
export class DeleteButtonComponent {
#openConfirmDialog = injectConfirmationDialog();
async confirmDelete() {
const result = await this.#openConfirmDialog({
title: 'Confirm Deletion',
data: {
message: 'Are you sure you want to delete this item?',
confirmText: 'Delete',
closeText: 'Cancel'
}
}).closed.toPromise();
if (result?.confirmed) {
console.log('User confirmed deletion');
// Perform deletion
}
}
}
3. Text Input Dialog
import { injectTextInputDialog } from '@isa/ui/dialog';
import { Validators } from '@angular/forms';
@Component({
selector: 'app-rename-dialog',
template: `<button (click)="renameItem()">Rename</button>`
})
export class RenameDialogComponent {
#openTextInputDialog = injectTextInputDialog();
async renameItem() {
const result = await this.#openTextInputDialog({
title: 'Rename Item',
data: {
message: 'Enter a new name for this item:',
inputLabel: 'Name',
inputDefaultValue: 'Current Name',
inputValidation: [
{
errorKey: 'required',
inputValidator: Validators.required,
errorText: 'Name is required'
},
{
errorKey: 'minlength',
inputValidator: Validators.minLength(3),
errorText: 'Name must be at least 3 characters'
}
],
confirmText: 'Save',
closeText: 'Cancel'
}
}).closed.toPromise();
if (result?.inputValue) {
console.log('New name:', result.inputValue);
// Update item name
}
}
}
Core Concepts
Dialog System Architecture
The library consists of three layers:
1. Base Components
- DialogComponent - Container shell for all dialogs
- DialogContentDirective - Base class for dialog content components
2. Preset Components
- MessageDialogComponent - Simple informational messages
- ConfirmationDialogComponent - Yes/No confirmations
- TextInputDialogComponent - Text input with validation
- NumberInputDialogComponent - Numeric input with validation
- FeedbackDialogComponent - Success feedback with auto-close
- FeedbackErrorDialogComponent - Error feedback
3. Injection API
- injectDialog() - Generic dialog injector factory
- injectMessageDialog() - Pre-configured message dialog
- injectConfirmationDialog() - Pre-configured confirmation dialog
- injectTextInputDialog() - Pre-configured text input dialog
- injectNumberInputDialog() - Pre-configured number input dialog
- injectFeedbackDialog() - Pre-configured feedback dialog
- injectFeedbackErrorDialog() - Pre-configured error feedback dialog
Type-Safe Data and Results
Each dialog type is strongly typed for both input data and return value:
// Message Dialog
interface MessageDialogData {
message: string;
closeText?: string;
}
// Returns: void
// Confirmation Dialog
interface ConfirmationDialogData {
message: string;
confirmText?: string;
closeText?: string;
}
interface ConfirmationDialogResult {
confirmed: boolean;
}
// Text Input Dialog
interface TextInputDialogData {
message: string;
inputLabel?: string;
inputDefaultValue?: string;
inputValidation?: TextInputValidation[];
confirmText?: string;
closeText?: string;
}
interface TextInputDialogResult {
inputValue?: string;
}
// Number Input Dialog
interface NumberInputDialogData {
message: string;
subMessage?: string;
subMessageValue?: string;
inputLabel?: string;
inputValue?: number;
inputDefaultValue?: number;
inputValidation?: NumberInputValidation[];
confirmText?: string;
closeText?: string;
}
interface NumberInputDialogResult {
inputValue?: number;
}
// Feedback Dialog
interface FeedbackDialogData {
message: string;
autoClose?: boolean;
autoCloseDelay?: number;
}
// Returns: void
Dialog Options
Common options available for all dialogs:
interface InjectDialogOptions {
title?: string; // Dialog title
displayClose?: boolean; // Show close (X) button (default: false)
classList?: string[]; // Additional CSS classes
width?: string; // Fixed width
height?: string; // Fixed height
minWidth?: string; // Minimum width (default: '30rem')
maxWidth?: string; // Maximum width
minHeight?: string; // Minimum height
maxHeight?: string; // Maximum height
hasBackdrop?: boolean; // Show backdrop (default: true)
disableClose?: boolean; // Disable backdrop click to close (default: true)
}
API Reference
injectDialog()
Generic factory function for creating custom dialogs.
Type Parameters:
C extends DialogContentDirective<D, R>- Dialog content component typeD- Input data type (inferred from component)R- Result type (inferred from component)
Parameters:
componentType: ComponentType<C>- The dialog content component classinjectOptions?: InjectDialogOptions- Default options for the dialog
Returns: Function (openOptions?) => DialogRef<R>
Usage:
import { injectDialog } from '@isa/ui/dialog';
import { MyCustomDialogComponent } from './my-custom-dialog.component';
@Component({...})
export class MyComponent {
#openCustomDialog = injectDialog(MyCustomDialogComponent, {
title: 'Custom Dialog',
width: '600px'
});
openDialog() {
const dialogRef = this.#openCustomDialog({
data: { /* custom data */ }
});
dialogRef.closed.subscribe(result => {
console.log('Dialog closed with result:', result);
});
}
}
injectMessageDialog()
Pre-configured message dialog injector.
Returns: Function (options?) => DialogRef<void>
Usage:
#openMessageDialog = injectMessageDialog();
showInfo() {
this.#openMessageDialog({
title: 'Information',
data: {
message: 'This is an informational message.',
closeText: 'OK'
}
});
}
injectConfirmationDialog()
Pre-configured confirmation dialog injector.
Parameters:
options?: OpenDialogOptions<ConfirmationDialogData>- Optional default options
Returns: Function (options?) => DialogRef<ConfirmationDialogResult>
Usage:
#openConfirmDialog = injectConfirmationDialog();
async confirmAction() {
const result = await this.#openConfirmDialog({
title: 'Confirm Action',
data: {
message: 'Are you sure?',
confirmText: 'Yes',
closeText: 'No'
}
}).closed.toPromise();
return result?.confirmed ?? false;
}
injectTextInputDialog()
Pre-configured text input dialog injector.
Returns: Function (options?) => DialogRef<TextInputDialogResult>
Usage:
#openTextInputDialog = injectTextInputDialog();
async getTextInput() {
const result = await this.#openTextInputDialog({
title: 'Enter Value',
data: {
message: 'Please enter a value:',
inputLabel: 'Value',
inputDefaultValue: '',
inputValidation: [
{
errorKey: 'required',
inputValidator: Validators.required,
errorText: 'This field is required'
}
]
}
}).closed.toPromise();
return result?.inputValue;
}
injectNumberInputDialog()
Pre-configured number input dialog injector.
Returns: Function (options?) => DialogRef<NumberInputDialogResult>
Usage:
#openNumberInputDialog = injectNumberInputDialog();
async getQuantity() {
const result = await this.#openNumberInputDialog({
title: 'Enter Quantity',
data: {
message: 'How many items would you like?',
inputLabel: 'Quantity',
inputDefaultValue: 1,
inputValidation: [
{
errorKey: 'min',
inputValidator: Validators.min(1),
errorText: 'Minimum quantity is 1'
},
{
errorKey: 'max',
inputValidator: Validators.max(100),
errorText: 'Maximum quantity is 100'
}
]
}
}).closed.toPromise();
return result?.inputValue ?? 1;
}
injectFeedbackDialog()
Pre-configured feedback dialog injector with auto-close support.
Parameters:
options?: OpenDialogOptions<FeedbackDialogData>- Optional default options
Returns: Function (options?) => DialogRef<void>
Default Options:
disableClose: false- Allows backdrop click to closeminWidth: '20rem'- Compact size for feedbackclassList: ['gap-0']- Removes default spacing
Usage:
#openFeedbackDialog = injectFeedbackDialog();
showSuccess() {
this.#openFeedbackDialog({
data: {
message: 'Changes saved successfully!',
autoClose: true,
autoCloseDelay: 1500
}
});
}
injectFeedbackErrorDialog()
Pre-configured error feedback dialog injector.
Parameters:
options?: OpenDialogOptions<FeedbackErrorDialogData>- Optional default options
Returns: Function (options?) => DialogRef<void>
Usage:
#openErrorDialog = injectFeedbackErrorDialog();
showError(error: Error) {
this.#openErrorDialog({
data: {
message: `An error occurred: ${error.message}`
}
});
}
Usage Examples
Basic Message Dialog
import { Component, inject } from '@angular/core';
import { injectMessageDialog } from '@isa/ui/dialog';
@Component({
selector: 'app-welcome',
template: `<button (click)="showWelcome()">Show Welcome</button>`
})
export class WelcomeComponent {
#openMessageDialog = injectMessageDialog();
showWelcome() {
this.#openMessageDialog({
title: 'Welcome!',
displayClose: true,
data: {
message: 'Welcome to our application. Click OK to continue.',
closeText: 'OK'
}
});
}
}
Confirmation with Custom Styling
import { Component } from '@angular/core';
import { injectConfirmationDialog } from '@isa/ui/dialog';
@Component({
selector: 'app-dangerous-action',
template: `<button (click)="performDangerousAction()">Delete All</button>`
})
export class DangerousActionComponent {
#openConfirmDialog = injectConfirmationDialog({
classList: ['dialog-danger'],
width: '500px'
});
async performDangerousAction() {
const result = await this.#openConfirmDialog({
title: 'Warning!',
data: {
message: 'This action cannot be undone. Delete all items?',
confirmText: 'Delete Everything',
closeText: 'Cancel'
}
}).closed.toPromise();
if (result?.confirmed) {
// Perform dangerous action
console.log('User confirmed dangerous action');
}
}
}
Text Input with Complex Validation
import { Component } from '@angular/core';
import { injectTextInputDialog } from '@isa/ui/dialog';
import { Validators } from '@angular/forms';
@Component({
selector: 'app-email-form',
template: `<button (click)="getEmail()">Enter Email</button>`
})
export class EmailFormComponent {
#openTextInputDialog = injectTextInputDialog();
async getEmail() {
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const result = await this.#openTextInputDialog({
title: 'Email Address',
data: {
message: 'Please enter your email address:',
inputLabel: 'Email',
inputDefaultValue: '',
inputValidation: [
{
errorKey: 'required',
inputValidator: Validators.required,
errorText: 'Email is required'
},
{
errorKey: 'email',
inputValidator: Validators.email,
errorText: 'Invalid email format'
},
{
errorKey: 'pattern',
inputValidator: Validators.pattern(emailPattern),
errorText: 'Email must be valid'
}
],
confirmText: 'Submit',
closeText: 'Cancel'
}
}).closed.toPromise();
if (result?.inputValue) {
console.log('Email entered:', result.inputValue);
// Process email
}
}
}
Number Input for Quantity
import { Component } from '@angular/core';
import { injectNumberInputDialog } from '@isa/ui/dialog';
import { Validators } from '@angular/forms';
@Component({
selector: 'app-order-quantity',
template: `<button (click)="selectQuantity()">Select Quantity</button>`
})
export class OrderQuantityComponent {
#openNumberInputDialog = injectNumberInputDialog();
async selectQuantity() {
const result = await this.#openNumberInputDialog({
title: 'Order Quantity',
data: {
message: 'Select the number of items to order:',
subMessage: 'Available:',
subMessageValue: '50 units',
inputLabel: 'Quantity',
inputValue: 1,
inputDefaultValue: 1,
inputValidation: [
{
errorKey: 'required',
inputValidator: Validators.required,
errorText: 'Quantity is required'
},
{
errorKey: 'min',
inputValidator: Validators.min(1),
errorText: 'Minimum quantity is 1'
},
{
errorKey: 'max',
inputValidator: Validators.max(50),
errorText: 'Maximum quantity is 50'
}
],
confirmText: 'Add to Cart',
closeText: 'Cancel'
}
}).closed.toPromise();
if (result?.inputValue) {
console.log('Quantity selected:', result.inputValue);
// Add to cart
}
}
}
Feedback Dialog with Auto-Close
import { Component } from '@angular/core';
import { injectFeedbackDialog } from '@isa/ui/dialog';
@Component({
selector: 'app-save-button',
template: `<button (click)="saveData()">Save</button>`
})
export class SaveButtonComponent {
#openFeedbackDialog = injectFeedbackDialog();
async saveData() {
try {
// Simulate save operation
await this.performSave();
// Show success feedback
this.#openFeedbackDialog({
data: {
message: 'Data saved successfully!',
autoClose: true,
autoCloseDelay: 2000
}
});
} catch (error) {
console.error('Save failed:', error);
}
}
private async performSave(): Promise<void> {
// Simulate async save
return new Promise(resolve => setTimeout(resolve, 1000));
}
}
Custom Dialog Component
// custom-dialog.component.ts
import { Component } from '@angular/core';
import { DialogContentDirective } from '@isa/ui/dialog';
import { ButtonComponent } from '@isa/ui/buttons';
interface CustomDialogData {
title: string;
items: string[];
}
interface CustomDialogResult {
selectedItem: string;
}
@Component({
selector: 'app-custom-dialog',
template: `
<h2>{{ data.title }}</h2>
<ul>
@for (item of data.items; track item) {
<li>
<button uiButton (click)="selectItem(item)">
{{ item }}
</button>
</li>
}
</ul>
<button uiButton (click)="close()">Cancel</button>
`,
imports: [ButtonComponent]
})
export class CustomDialogComponent extends DialogContentDirective<
CustomDialogData,
CustomDialogResult
> {
selectItem(item: string) {
this.close({ selectedItem: item });
}
}
// Using the custom dialog
import { injectDialog } from '@isa/ui/dialog';
import { CustomDialogComponent } from './custom-dialog.component';
@Component({
selector: 'app-selector',
template: `<button (click)="openSelector()">Select Item</button>`
})
export class SelectorComponent {
#openCustomDialog = injectDialog(CustomDialogComponent, {
title: 'Select an Item'
});
async openSelector() {
const result = await this.#openCustomDialog({
data: {
title: 'Choose Your Option',
items: ['Option A', 'Option B', 'Option C']
}
}).closed.toPromise();
if (result) {
console.log('Selected:', result.selectedItem);
}
}
}
Dialog Presets
MessageDialog
Purpose: Display simple informational messages Use Case: Alerts, notifications, information display Returns: void
ConfirmationDialog
Purpose: Get user confirmation for actions
Use Case: Delete confirmations, action confirmations
Returns: { confirmed: boolean }
TextInputDialog
Purpose: Collect text input with validation
Use Case: Rename, create new item, enter search query
Returns: { inputValue?: string }
Features: Full reactive forms validation support
NumberInputDialog
Purpose: Collect numeric input with validation
Use Case: Quantity selection, price entry, numeric configuration
Returns: { inputValue?: number }
Features: Validation, default values, min/max constraints
FeedbackDialog
Purpose: Show success feedback Use Case: Save confirmation, operation success Returns: void Features: Auto-close after configurable delay
FeedbackErrorDialog
Purpose: Show error feedback Use Case: Error messages, operation failures Returns: void Features: Error styling, persistent display
Advanced Usage
Handling Dialog Results
// Using async/await
async openDialogAsync() {
const result = await this.#openConfirmDialog({...}).closed.toPromise();
if (result?.confirmed) {
// Handle confirmation
}
}
// Using subscribe
openDialogSubscribe() {
const dialogRef = this.#openConfirmDialog({...});
dialogRef.closed.subscribe(result => {
if (result?.confirmed) {
// Handle confirmation
}
});
}
// Using observable operators
openDialogOperators() {
this.#openConfirmDialog({...}).closed
.pipe(
filter(result => result?.confirmed),
switchMap(() => this.performAction())
)
.subscribe();
}
Dynamic Dialog Configuration
@Component({...})
export class DynamicDialogComponent {
#openConfirmDialog = injectConfirmationDialog();
confirmWithDynamicConfig(isDangerous: boolean) {
this.#openConfirmDialog({
title: isDangerous ? 'Warning!' : 'Confirm',
classList: isDangerous ? ['dialog-danger'] : [],
width: isDangerous ? '600px' : '400px',
data: {
message: isDangerous
? 'This action is irreversible!'
: 'Do you want to proceed?',
confirmText: isDangerous ? 'I Understand' : 'OK'
}
});
}
}
Preventing Backdrop Close
// Dialog cannot be closed by clicking backdrop
#openDialog = injectDialog(MyDialogComponent, {
disableClose: true,
hasBackdrop: true
});
// Dialog can be closed by clicking backdrop
#openDialog = injectDialog(MyDialogComponent, {
disableClose: false,
hasBackdrop: true
});
Testing
The library uses Jest with Spectator for testing.
Running Tests
# Run tests for this library
npx nx test ui-dialog --skip-nx-cache
# Run tests with coverage
npx nx test ui-dialog --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-dialog --watch
Test Coverage
The library includes comprehensive unit tests covering:
- Dialog opening and closing - Lifecycle management
- Data passing - Input data and result handling
- Validation - Form validation in input dialogs
- Auto-close - Feedback dialog auto-dismiss
- Configuration - All dialog options and variants
- Type safety - TypeScript type inference
Example Test
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { DialogContentDirective } from './dialog-content.directive';
describe('DialogContentDirective', () => {
let spectator: Spectator<DialogContentDirective<any, any>>;
const createComponent = createComponentFactory(DialogContentDirective);
beforeEach(() => {
spectator = createComponent();
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should close with result', () => {
const result = { value: 'test' };
spectator.component.close(result);
// Assert dialog closed with result
});
});
Architecture Notes
Design Patterns
1. Injection-Based API
Uses Angular's dependency injection for dialog configuration:
// Inject dialog factory in constructor/field
#openDialog = injectDialog(MyComponent, defaultOptions);
// Call factory to open dialog
this.#openDialog({ data: {...}, ...overrideOptions });
Benefits:
- Type-safe configuration
- Compile-time checking
- Clear API surface
- Easy testing
2. Two-Tier Configuration
Supports both default options (inject-time) and override options (open-time):
// Default options at injection
#openDialog = injectDialog(Component, {
width: '400px',
title: 'Default Title'
});
// Override at open-time
this.#openDialog({
title: 'Custom Title', // Overrides default
// width: '400px' inherited from default
data: {...}
});
3. Base Component Extension
All preset dialogs extend DialogContentDirective:
export class MessageDialogComponent extends DialogContentDirective<
MessageDialogData,
void
> {}
Benefits:
- Automatic data/dialogRef injection
- Consistent API
- Type inference
- Reusable close() method
Known Architectural Considerations
1. Close Button Display (Important)
Current State:
displayClosedefaults tofalse(#5275)- Changing default to
truewould require reviewing all existing dialogs - Some dialogs may have logic that depends on
undefinedbeing returned when X is clicked
Consideration:
- Explicit opt-in prevents breaking changes
- Future: Consider migrating to
truedefault with migration guide
Impact: Medium - Affects dialog UX consistency
2. DialogRef Observable Pattern
Current State:
DialogRef.closedis an Observable- Requires
.toPromise()for async/await usage - Multiple subscription patterns supported
Consideration:
- Could provide both Promise and Observable APIs
- Observable is more flexible but requires RxJS knowledge
Impact: Low - Current pattern is Angular-idiomatic
Performance Considerations
- OnPush Change Detection - All dialog components use OnPush
- Lazy Loading - Dialog content only instantiated when opened
- CDK Overlay - Efficient positioning and backdrop management
- Auto-Close Cleanup - Feedback dialogs properly clean up timers
Dependencies
Required Libraries
@angular/core- Angular framework@angular/cdk/dialog- Dialog primitives@angular/cdk/portal- Component portal system@angular/forms- Reactive forms (for input dialogs)@isa/ui/buttons- Button components@isa/ui/input-controls- Input components (for input dialogs)@ng-icons/core- Icon system@isa/icons- ISA icon libraryrxjs- Reactive programming
Path Alias
Import from: @isa/ui/dialog
License
Internal ISA Frontend library - not for external distribution.