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:
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:
<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:
: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:
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:
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:
{
"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:
{
"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:
<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:
- Provider Configuration: The
CustomModalWrapperComponentbecomes the default wrapper for ALL modals - Component Opening: When you call
modalService.open(UserFormComponent, data), the system:- Creates an instance of
CustomModalWrapperComponent(your wrapper) - Passes the
UserFormComponentanddatato the wrapper - The wrapper can then display the
UserFormComponentinside its template
- Creates an instance of
- 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)
- The wrapper component via
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
- Standalone Components: Use standalone components for better modularity and tree-shaking
- Change Detection: Use
OnPushchange detection strategy for better performance - Type Safety: Define proper TypeScript interfaces for modal data
- Accessibility: Ensure your custom modal follows accessibility guidelines
- Testing: Write unit tests for your custom modal wrapper component
- 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.