# @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](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Dialog Presets](#dialog-presets)
- [Advanced Usage](#advanced-usage)
- [Testing](#testing)
- [Architecture Notes](#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

```typescript
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

```typescript
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

```typescript
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:

```typescript
// 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:

```typescript
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 type
- `D` - Input data type (inferred from component)
- `R` - Result type (inferred from component)

**Parameters:**
- `componentType: ComponentType<C>` - The dialog content component class
- `injectOptions?: InjectDialogOptions` - Default options for the dialog

**Returns:** Function `(openOptions?) => DialogRef<R>`

**Usage:**
```typescript
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:**
```typescript
#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:**
```typescript
#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:**
```typescript
#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:**
```typescript
#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 close
- `minWidth: '20rem'` - Compact size for feedback
- `classList: ['gap-0']` - Removes default spacing

**Usage:**
```typescript
#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:**
```typescript
#openErrorDialog = injectFeedbackErrorDialog();

showError(error: Error) {
  this.#openErrorDialog({
    data: {
      message: `An error occurred: ${error.message}`
    }
  });
}
```

## Usage Examples

### Basic Message Dialog

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
@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

```typescript
// 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

```bash
# 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

```typescript
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:

```typescript
// 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):

```typescript
// 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`:

```typescript
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:**
- `displayClose` defaults to `false` (#5275)
- Changing default to `true` would require reviewing all existing dialogs
- Some dialogs may have logic that depends on `undefined` being returned when X is clicked

**Consideration:**
- Explicit opt-in prevents breaking changes
- Future: Consider migrating to `true` default with migration guide

**Impact:** Medium - Affects dialog UX consistency

#### 2. DialogRef Observable Pattern

**Current State:**
- `DialogRef.closed` is 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

1. **OnPush Change Detection** - All dialog components use OnPush
2. **Lazy Loading** - Dialog content only instantiated when opened
3. **CDK Overlay** - Efficient positioning and backdrop management
4. **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 library
- `rxjs` - Reactive programming

### Path Alias

Import from: `@isa/ui/dialog`

## License

Internal ISA Frontend library - not for external distribution.
