Skip to main content

Creating a Custom Modal Wrapper

Overview

This guide explains how to create a custom modal wrapper component that replaces the base modal from the ModalService. By using the MODAL_COMPONENT_EXTERNAL provider, you can customize the modal's appearance, behavior, and functionality while maintaining integration with the existing modal system.

Why use a Custom Modal Wrapper?

  • Complete Control: Full control over modal styling, layout, and behavior
  • Consistent Branding: Ensure modals match your application's specific design requirements
  • Enhanced Functionality: Add custom features like lifecycle hooks, validation, and specialized interactions
  • Reusability: Create a standardized modal wrapper that can be reused across different components

How to Create a Custom Modal Wrapper

Step 1: Create the Modal Wrapper Component

First, create your custom modal wrapper component. Here's a simple, generic example:

custom-modal-wrapper.component.ts
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { ModalRef } from '@celerofinancas/ui-modals';
import { CommonModule } from '@angular/common';

/**
* Generic modal data interface
*/
interface ModalData {
title: string;
content?: string;
[key: string]: any; // Allow additional properties
}

/**
* Custom modal wrapper component
*/
@Component({
selector: 'app-custom-modal-wrapper',
templateUrl: './custom-modal-wrapper.component.html',
styleUrls: ['./custom-modal-wrapper.component.scss'],
standalone: true,
imports: [
CommonModule,
// Add your specific component imports here
],
providers: [
// Add your custom providers here if needed
// Example:
// { provide: YOUR_SERVICE_TOKEN, useValue: yourServiceConfig }
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomModalWrapperComponent implements OnInit {
/**
* Current modal reference with typed data
*/
public modalRef = inject(ModalRef) as ModalRef<any, ModalData>;

/**
* Initializes the component
*/
public ngOnInit() {
// Add custom initialization logic here
this.setupModalBehavior();
}

/**
* Setup custom modal behavior
*/
private setupModalBehavior(): void {
// Example: Add custom close validation
this.modalRef.beforeClose = () => {
// Add your custom validation logic here
if (this.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Are you sure you want to close?');
}
return true; // Allow modal to close
};
}

/**
* Example method to check for unsaved changes
*/
private hasUnsavedChanges(): boolean {
// Implement your logic to check for unsaved changes
return false;
}

/**
* Example method to handle custom actions
*/
public onCustomAction(): void {
// Implement your custom action logic
console.log('Custom action triggered');
}

/**
* Close the modal with optional result
*/
public closeModal(result?: any): void {
this.modalRef.close(result);
}
}

Step 2: Create the Template

Create the HTML template for your modal wrapper:

custom-modal-wrapper.component.html
<div class="modal-header">
<h2>{{ modalRef.data.title }}</h2>
<button class="close-button" (click)="closeModal()" aria-label="Close modal">
×
</button>
</div>

<div class="modal-content">
<p *ngIf="modalRef.data.content">{{ modalRef.data.content }}</p>

<!-- Add your custom content here -->
<!-- Example: Include your specific component -->
<!-- <your-custom-component [data]="modalRef.data"></your-custom-component> -->
</div>

<div class="modal-footer">
<button class="btn btn-secondary" (click)="closeModal()">Cancel</button>
<button class="btn btn-primary" (click)="onCustomAction()">Confirm</button>
</div>

Step 3: Add Custom Styling

Create custom styles for your modal wrapper:

custom-modal-wrapper.component.scss
:host {
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow: hidden;
}

.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;

h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #111827;
}

.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;

&:hover {
color: #374151;
}
}
}

.modal-content {
padding: 24px;
flex: 1;
overflow-y: auto;

p {
margin: 0 0 16px 0;
color: #374151;
line-height: 1.5;
}
}

.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #e5e7eb;

.btn {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;

&.btn-secondary {
background: white;
border-color: #d1d5db;
color: #374151;

&:hover {
background: #f9fafb;
}
}

&.btn-primary {
background: #3b82f6;
border-color: #3b82f6;
color: white;

&:hover {
background: #2563eb;
}
}
}
}

Step 4: Create a Component to Display Inside the Modal

First, create a component that will be displayed inside your custom modal wrapper:

user-form.component.ts
import { Component, inject } from '@angular/core';
import { ModalRef } from '@celerofinancas/ui-modals';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

interface UserFormData {
name?: string;
email?: string;
title: string;
}

@Component({
selector: 'app-user-form',
template: `
<div class="user-form">
<h3>{{ modalRef.data.title }}</h3>
<form>
<div class="form-group">
<label for="name">Name:</label>
<input id="name" type="text" [(ngModel)]="formData.name" />
</div>
<div class="form-group">
<label for="email">Email:</label>
<input id="email" type="email" [(ngModel)]="formData.email" />
</div>
<div class="form-actions">
<button type="button" (click)="save()">Save</button>
<button type="button" (click)="cancel()">Cancel</button>
</div>
</form>
</div>
`,
standalone: true,
imports: [CommonModule, FormsModule]
})
export class UserFormComponent {
public modalRef = inject(ModalRef) as ModalRef<any, UserFormData>;

public formData = {
name: this.modalRef.data.name || '',
email: this.modalRef.data.email || ''
};

public save(): void {
this.modalRef.close(this.formData);
}

public cancel(): void {
this.modalRef.close();
}
}

Step 5: Configure the Provider in Your Module or Component

To replace the base modal with your custom wrapper, use the MODAL_COMPONENT_EXTERNAL provider. Important: Once configured, ALL modals opened through ModalService will use your custom wrapper:

app.module.ts or your-component.component.ts
import { Component, inject } from '@angular/core';
import { ModalService, MODAL_COMPONENT_EXTERNAL } from '@celerofinancas/ui-modals';
import { CustomModalWrapperComponent } from './custom-modal-wrapper/custom-modal-wrapper.component';
import { UserFormComponent } from './user-form/user-form.component';

@Component({
selector: 'app-example',
templateUrl: './example.component.html',
standalone: true,
providers: [
{
provide: MODAL_COMPONENT_EXTERNAL,
useValue: CustomModalWrapperComponent
}
]
})
export class ExampleComponent {
/**
* Modal service for handling modals
*/
private modalService = inject(ModalService);

/**
* Open a component inside the custom modal wrapper
* The CustomModalWrapperComponent will automatically wrap the UserFormComponent
*/
public openUserForm(): void {
this.modalService.open(UserFormComponent, {
title: 'Edit User Information',
name: 'John Doe',
email: 'john@example.com'
});
}

/**
* Open another component inside the same custom wrapper
*/
public openAnotherComponent(): void {
// Any component you open will be wrapped by CustomModalWrapperComponent
this.modalService.open(AnyOtherComponent, {
// Your component data here
});
}
}

Step 5: Add the ui-modals to Assets (if not already configured)

Ensure that the ui-modals files are included in the assets section of your angular.json file:

angular.json
{
"projects": {
"your-project-name": {
"architect": {
"build": {
"options": {
"assets": [
{
"glob": "**/*",
"input": "node_modules/@celerofinancas/ui-modals/assets",
"output": "/assets/"
}
]
}
}
}
}
}
}

Step 6: Add the ui-modals to Assets (if not already configured)

Ensure that the ui-modals files are included in the assets section of your angular.json file:

angular.json
{
"projects": {
"your-project-name": {
"architect": {
"build": {
"options": {
"assets": [
{
"glob": "**/*",
"input": "node_modules/@celerofinancas/ui-modals/assets",
"output": "/assets/"
}
]
}
}
}
}
}
}

Step 7: Use the Custom Modal in Your Template

Add buttons to open components inside your custom modal wrapper:

example.component.html
<button (click)="openUserForm()">Open User Form</button>
<button (click)="openAnotherComponent()">Open Another Component</button>

How It Works

When you configure the MODAL_COMPONENT_EXTERNAL provider:

  1. Provider Configuration: The CustomModalWrapperComponent becomes the default wrapper for ALL modals
  2. Component Opening: When you call modalService.open(UserFormComponent, data), the system:
    • Creates an instance of CustomModalWrapperComponent (your wrapper)
    • Passes the UserFormComponent and data to the wrapper
    • The wrapper can then display the UserFormComponent inside its template
  3. Data Flow: The data you pass to modalService.open() becomes available in both:
    • The wrapper component via modalRef.data
    • The inner component (if it also injects ModalRef)

Key Features of the Custom Modal Wrapper

1. Modal Reference Integration

The ModalRef injection provides access to:

  • Modal data: Access passed parameters through modalRef.data
  • Modal lifecycle: Control modal closing behavior with modalRef.beforeClose
  • Modal state: Manage modal state and interactions

2. Custom Providers

Configure specific services and tokens for your modal:

providers: [
{
provide: YOUR_SERVICE_TOKEN,
useValue: yourServiceConfig,
},
{
provide: YOUR_CONFIG_TOKEN,
useValue: yourConfigDefaults,
},
// Add more providers as needed for your specific use case
]

3. Lifecycle Hooks

Implement custom logic during modal lifecycle:

public ngOnInit() {
this.modalRef.beforeClose = () => {
// Custom validation or confirmation logic
if (this.hasUnsavedChanges()) {
this.showWarning();
return false; // Prevent modal from closing
}
return true; // Allow modal to close
}
}

4. State Management

Use Angular signals or reactive forms for state management:

// Example with signals
public isLoading = signal<boolean>(false);
public formData = signal<any>({});

public updateFormData(data: any): void {
this.formData.set({ ...this.formData(), ...data });
}

// Example with reactive forms
public form = new FormGroup({
name: new FormControl(''),
email: new FormControl('')
});

Best Practices

  1. Standalone Components: Use standalone components for better modularity and tree-shaking
  2. Change Detection: Use OnPush change detection strategy for better performance
  3. Type Safety: Define proper TypeScript interfaces for modal data
  4. Accessibility: Ensure your custom modal follows accessibility guidelines
  5. Testing: Write unit tests for your custom modal wrapper component
  6. Documentation: Document your custom modal's API and usage patterns

Advanced Customization

Custom Modal Data Interface

Define a specific interface for your modal data:

interface CustomModalData {
title: string;
content?: string;
showActions?: boolean;
data?: any;
// Add more properties as needed for your specific use case
}

public modalRef = inject(ModalRef) as ModalRef<any, CustomModalData>;

Multiple Modal Wrappers

You can create different modal wrappers for different use cases:

// For different contexts, use different providers
{
provide: MODAL_COMPONENT_EXTERNAL,
useValue: ConfirmationModalWrapper // For confirmation dialogs
}

{
provide: MODAL_COMPONENT_EXTERNAL,
useValue: FormModalWrapper // For form-based modals
}

{
provide: MODAL_COMPONENT_EXTERNAL,
useValue: InfoModalWrapper // For information display
}

Conclusion

By following this guide, you can create powerful custom modal wrappers that replace the base ModalService modal while maintaining full integration with the existing modal system. This approach provides maximum flexibility for customization while ensuring consistency and maintainability across your application.

The MODAL_COMPONENT_EXTERNAL provider is the key to replacing the default modal behavior, allowing you to inject your custom component as the modal wrapper while preserving all the functionality of the ModalService.