Modal Components Skill
CRITICAL: All modals MUST use the global Modal component with the correct API
Overview
The Diddit! app has a centralized, reusable Modal component at apps/frontend/src/app/components/modals/modal/modal.ts. This component handles ALL modal functionality in one place:
- •✅ Backdrop click to close
- •✅ ESC key to close
- •✅ Close button (X) to close
- •✅ Focus trap
- •✅ Body scroll prevention
- •✅ Accessibility (ARIA attributes, roles)
- •✅ Animations
NEVER reimplement these features. ALWAYS use the global Modal component.
Correct API
Inputs
open = input<boolean>(false); // Whether modal is open
title = input<string>(''); // Modal title text
closeOnBackdropClick = input<boolean>(true); // Allow backdrop close (default: true)
closeOnEsc = input<boolean>(true); // Allow ESC close (default: true)
Outputs
closeModal = output<void>(); // Emitted when modal should close
CRITICAL: The output is closeModal, NOT closeRequested, NOT close, NOT anything else.
Template Pattern
✅ CORRECT Usage
<app-modal [open]="open()" (closeModal)="handleClose()" title="Modal Title">
<!-- Your modal content here -->
<form>
<!-- Form fields -->
</form>
<!-- Action buttons (optional) -->
<div class="modal-actions" modal-actions>
<button type="button" class="btn btn-secondary" (click)="onCancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</app-modal>
Key Points
- •Use
[open]input - Pass a signal or boolean to control visibility - •Use
(closeModal)output - Listen to the EXACT event namecloseModal - •Use
titleinput - Pass title as input property, NOT using slots - •Use
modal-actionsattribute - For footer buttons (optional) - •Content goes in default slot - Just put content inside
<app-modal>tags
❌ WRONG - Do NOT Do This
<!-- WRONG: Using wrong event name -->
<app-modal [open]="open()" (closeRequested)="handleClose()">
<!-- WRONG: Using slots for title -->
<app-modal [open]="open()" (closeModal)="handleClose()">
<h2 slot="title">Modal Title</h2>
<!-- WRONG: Using old @Output name -->
<app-modal [open]="open()" (close)="handleClose()">
<!-- WRONG: Not binding to signal/property -->
<app-modal open="true" (closeModal)="handleClose()"></app-modal></app-modal></app-modal
></app-modal>
Component TypeScript Pattern
✅ CORRECT Component
import { Component, input, output, signal } from '@angular/core';
import { Modal } from '../modals/modal/modal';
@Component({
selector: 'app-my-modal',
imports: [Modal],
templateUrl: './my-modal.html',
styleUrl: './my-modal.css',
})
export class MyModal {
// Inputs
open = input<boolean>(false);
someData = input<string>('');
// Outputs
closeRequested = output<void>(); // Emit to parent when modal should close
// Internal state
submitting = signal(false);
/**
* Handle modal close (called by Modal's closeModal event)
*/
handleClose(): void {
this.closeRequested.emit();
}
/**
* Handle cancel button
*/
onCancel(): void {
this.closeRequested.emit();
}
/**
* Handle form submission
*/
onSubmit(): void {
// Do work...
this.closeRequested.emit();
}
}
Key Points
- •Import Modal component - Add to
importsarray - •Accept
openinput - Let parent control visibility - •Emit
closeRequestedoutput - Let parent handle closing - •Create
handleClose()method - Connect Modal's(closeModal)to yourcloseRequestedoutput - •Don't manage open state internally - Parent controls it
Real Working Examples
Study these working modals in the codebase:
add-child-modal
- •Location:
apps/frontend/src/app/components/modals/add-child-modal/ - •Pattern: Form modal with validation
- •Template: Uses
(closeModal),titleinput,modal-actionsslot
invite-modal
- •Location:
apps/frontend/src/app/components/modals/invite-modal/ - •Pattern: Form modal with role selection
- •Template: Uses
(closeModal),titleinput,modal-actionsslot
task-form-modal
- •Location:
apps/frontend/src/app/components/modals/task-form-modal/ - •Pattern: Complex form with conditional fields, delete confirmation
- •Template: Uses
(closeModal), dynamic[title],modal-actionsslot
Common Mistakes
❌ Mistake 1: Wrong Event Name
Problem: Listening for (closeRequested) or (close) instead of (closeModal)
<!-- WRONG --> <app-modal [open]="open()" (closeRequested)="handleClose()"></app-modal>
Result: Modal won't close when clicking X, backdrop, or pressing ESC
Fix: Use (closeModal)
<!-- CORRECT --> <app-modal [open]="open()" (closeModal)="handleClose()"></app-modal>
❌ Mistake 2: Using Slots for Title
Problem: Trying to use slot-based API that doesn't exist
<!-- WRONG --> <app-modal [open]="open()" (closeModal)="handleClose()"> <h2 slot="title">My Title</h2></app-modal>
Result: Title appears in modal body, not header. Empty header shows.
Fix: Use title input
<!-- CORRECT --> <app-modal [open]="open()" (closeModal)="handleClose()" title="My Title"></app-modal>
❌ Mistake 3: Duplicate Headers
Problem: Setting title on Modal AND adding header in content
<!-- WRONG --> <app-modal [open]="open()" (closeModal)="handleClose()" title="Add Task"> <h2>Add Task</h2> <!-- Duplicate header! --> <form>...</form> </app-modal>
Result: Two headers appear
Fix: Only use Modal's title input
<!-- CORRECT --> <app-modal [open]="open()" (closeModal)="handleClose()" title="Add Task"> <form>...</form> <!-- No duplicate header --> </app-modal>
Storybook Requirement
CRITICAL: Every modal component MUST have a Storybook story.
Why?
Storybook catches modal bugs immediately:
- •Close functionality (X button, backdrop, ESC)
- •Header rendering
- •Form validation
- •Loading states
- •Error states
Story Template
// my-modal.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';
import { MyModal } from './my-modal';
const meta: Meta<MyModal> = {
title: 'Components/Modals/MyModal',
component: MyModal,
tags: ['autodocs'],
argTypes: {
open: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<MyModal>;
export const Default: Story = {
args: {
open: true,
},
};
export const Closed: Story = {
args: {
open: false,
},
};
export const WithData: Story = {
args: {
open: true,
someData: 'Example data',
},
};
Testing Checklist in Storybook
When viewing your modal story, verify:
- • Modal opens when
open: true - • Modal closes when clicking X button
- • Modal closes when clicking backdrop
- • Modal closes when pressing ESC key
- • Modal title appears in header (not body)
- • Only ONE header appears (no duplicates)
- • Form validation works
- • Action buttons work
- • Loading states work
- • Error states work
Modal Component Source
For reference, the global Modal component is at:
TypeScript: apps/frontend/src/app/components/modals/modal/modal.ts
- •Inputs:
open,title,closeOnBackdropClick,closeOnEsc - •Outputs:
closeModal - •Methods:
onBackdropClick(),onEscapeKey(),onCloseClick(),onTabKey()
Template: apps/frontend/src/app/components/modals/modal/modal.html
- •Overlay with backdrop click handler
- •Header with title and close button
- •Body with
<ng-content>for default content - •Footer with
<ng-content select="[modal-actions]">for action buttons
Styles: apps/frontend/src/app/components/modals/modal/modal.css
- •Animations, layout, accessibility
Quick Checklist
Before creating a modal, verify:
- • Import
Modalcomponent - • Use
(closeModal)output (NOTcloseRequested, NOTclose) - • Use
titleinput (NOT slots) - • Accept
openinput from parent - • Emit
closeRequestedto parent - • Create
handleClose()method to bridge events - • Use
modal-actionsattribute for footer buttons - • Create Storybook story
- • Test all close methods in Storybook (X, backdrop, ESC)
- • Verify no duplicate headers
Summary
The golden rule: Study existing working modals (add-child-modal, invite-modal, task-form-modal) and copy their patterns exactly. Don't invent new APIs or patterns.