ng-hub-ui-modal 1.2.3 → 21.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,343 +1,672 @@
1
1
  # ng-hub-ui-modal
2
2
 
3
- This library provides a decoupled and independent modal component, originally based on the modals from ng-bootstrap but with additional features and flexibility. It aims to offer a more versatile and customizable modal solution for Angular applications.
4
-
5
- ## Motivation
6
-
7
- The main motivation behind the development of this library was to decouple the modal component from ng-bootstrap, allowing it to be used autonomously without relying on the entire ng-bootstrap library. Additionally, new functionalities and customization options have been introduced to better suit the needs of different projects.
3
+ [![NPM Version](https://img.shields.io/npm/v/ng-hub-ui-modal.svg)](https://www.npmjs.com/package/ng-hub-ui-modal)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
6
+
7
+ > A standalone, fully-featured Angular modal library with flexible content projection, placement support, and full CSS variable theming. No Bootstrap or ng-bootstrap dependency required.
8
+
9
+ > **⚠️ WARNING: BREAKING CHANGES IN VERSION 21.0.0**
10
+ > If you are upgrading from `1.x.x` to `21.x.x` and you have overridden the `.modal` or `.modal-dialog` CSS classes in your global stylesheets, please review the [BREAKING_CHANGES.md](./BREAKING_CHANGES.md) document to migrate your styles to the new `hub-modal` BEM classes.
11
+
12
+ ---
13
+
14
+ ## 🧩 Library Family `ng-hub-ui`
15
+
16
+ This library is part of the **ng-hub-ui** ecosystem:
17
+
18
+ - [**ng-hub-ui-accordion**](https://www.npmjs.com/package/ng-hub-ui-accordion)
19
+ - [**ng-hub-ui-avatar**](https://www.npmjs.com/package/ng-hub-ui-avatar)
20
+ - [**ng-hub-ui-board**](https://www.npmjs.com/package/ng-hub-ui-board)
21
+ - [**ng-hub-ui-breadcrumbs**](https://www.npmjs.com/package/ng-hub-ui-breadcrumbs)
22
+ - [**ng-hub-ui-calendar**](https://www.npmjs.com/package/ng-hub-ui-calendar)
23
+ - [**➡️ ng-hub-ui-modal**](https://www.npmjs.com/package/ng-hub-ui-modal) ← _you are here_
24
+ - [**ng-hub-ui-paginable**](https://www.npmjs.com/package/ng-hub-ui-paginable)
25
+ - [**ng-hub-ui-portal**](https://www.npmjs.com/package/ng-hub-ui-portal)
26
+ - [**ng-hub-ui-stepper**](https://www.npmjs.com/package/ng-hub-ui-stepper)
27
+ - [**ng-hub-ui-utils**](https://www.npmjs.com/package/ng-hub-ui-utils)
28
+
29
+ ---
30
+
31
+ ## 📋 Table of Contents
32
+
33
+ - [Features](#features)
34
+ - [Installation](#installation)
35
+ - [Quick Start](#quick-start)
36
+ - [Examples](#examples)
37
+ - [Open with TemplateRef](#open-with-templateref)
38
+ - [Open with Component](#open-with-component)
39
+ - [Open with String](#open-with-string)
40
+ - [Placement](#placement)
41
+ - [Size and Fullscreen](#size-and-fullscreen)
42
+ - [Scrollable Content](#scrollable-content)
43
+ - [Static Backdrop](#static-backdrop)
44
+ - [Before Dismiss Guard](#before-dismiss-guard)
45
+ - [HubActiveModal in Content Component](#hubactivemodal-in-content-component)
46
+ - [Dismiss and Close Selectors](#dismiss-and-close-selectors)
47
+ - [Multiple Stacked Modals](#multiple-stacked-modals)
48
+ - [Observables: dismissAll and hasOpenModals](#observables-dismissall-and-hasopenmodals)
49
+ - [API Reference](#api-reference)
50
+ - [HubModal Service](#hubmodal-service)
51
+ - [HubModalRef](#hubmodalref)
52
+ - [HubActiveModal](#hubactivemodal-1)
53
+ - [HubModalOptions](#hubmodaloptions)
54
+ - [HubModalUpdatableOptions](#hubmodalupdatableoptions)
55
+ - [HubModalPlacement](#hubmodalplacement-1)
56
+ - [ModalDismissReasons](#modaldismissreasons)
57
+ - [HubModalConfig](#hubmodalconfig)
58
+ - [Styling](#styling)
59
+ - [Contributing](#contributing)
60
+ - [Support & License](#support--license)
61
+
62
+ ---
8
63
 
9
64
  ## Features
10
65
 
11
- - **Standalone Modal Component**: No need to install ng-bootstrap or any other additional dependencies.
12
- - **Bootstrap-based Styling**: While using its own CSS classes, the modal's appearance follows Bootstrap's design guidelines, making it easier to customize.
13
- - **Flexible Content Projection**: Instead of projecting all content into a single `ng-content`, this library allows defining CSS selectors to project content into different parts of the modal (header, body, footer).
14
- - **Customizable Dismiss Triggers**: You can define a CSS selector for elements that will act as dismiss triggers for the modal.
15
- - **Data Binding to Modal Component**: It's possible to pass additional data to the modal component through the configuration options.
16
- - **New Control Methods**: Methods have been added to show and hide the modal on demand.
66
+ - **Zero external dependencies**: No ng-bootstrap, no Bootstrap JS.
67
+ - **Three content types**: Open modals with a `TemplateRef`, a `Component` class, or a plain `string`.
68
+ - **Flexible content projection**: Use CSS selectors to route content to `header`, `body`, and `footer` slots.
69
+ - **Placement support**: Anchor modals to any viewport edge `start`, `end`, `top`, `bottom` or keep them `center`.
70
+ - **Modal stacking**: Open multiple modals; focus management and aria-hidden are handled automatically.
71
+ - **Programmatic dismiss/close guards**: The `beforeDismiss` callback lets you intercept and prevent dismissal.
72
+ - **Full keyboard & backdrop interaction**: ESC key, static backdrop, backdrop click — all configurable.
73
+ - **CSS Variable theming**: Deep customization without overriding internal classes.
74
+ - **BEM class architecture**: All structural classes use the `hub-modal__*` prefix to avoid conflicts.
75
+ - **Lifecycle Observables**: `shown`, `hidden`, `closed`, `dismissed` streams for precise reactive flow.
76
+ - **Global defaults**: Inject `HubModalConfig` to set application-wide defaults.
77
+
78
+ ---
17
79
 
18
80
  ## Installation
19
81
 
20
- ```
82
+ ```bash
21
83
  npm install ng-hub-ui-modal
22
84
  ```
23
85
 
24
- ## Usage
86
+ ---
87
+
88
+ ## Quick Start
89
+
90
+ ### Standalone (recommended)
91
+
92
+ ```typescript
93
+ import { Component, inject, TemplateRef } from '@angular/core';
94
+ import { HubModal } from 'ng-hub-ui-modal';
95
+
96
+ @Component({
97
+ selector: 'app-root',
98
+ standalone: true,
99
+ template: `
100
+ <button (click)="open(tpl)">Open Modal</button>
101
+
102
+ <ng-template #tpl let-close="close">
103
+ <div class="hub-modal__header"><h5>Hello!</h5></div>
104
+ <div class="hub-modal__body">Modal content goes here.</div>
105
+ <div class="hub-modal__footer">
106
+ <button (click)="close('done')">Close</button>
107
+ </div>
108
+ </ng-template>
109
+ `
110
+ })
111
+ export class AppComponent {
112
+ private modal = inject(HubModal);
113
+
114
+ open(tpl: TemplateRef<unknown>) {
115
+ this.modal
116
+ .open(tpl, { headerSelector: '.hub-modal__header', footerSelector: '.hub-modal__footer' })
117
+ .result.catch(() => {});
118
+ }
119
+ }
120
+ ```
25
121
 
26
- 1. Import the `ModalModule` into your Angular module:
122
+ ### NgModule (classic)
27
123
 
28
124
  ```typescript
29
- import { ModalModule } from 'ng-hub-ui-modal';
125
+ import { HubModalModule } from 'ng-hub-ui-modal';
30
126
 
31
127
  @NgModule({
32
- imports: [
33
- // ...
34
- ModalModule
35
- ]
128
+ imports: [HubModalModule]
36
129
  })
37
130
  export class AppModule {}
38
131
  ```
39
132
 
40
- 2. Inject the `ModalService` into your component:
133
+ ---
134
+
135
+ ## Examples
136
+
137
+ ### Open with TemplateRef
138
+
139
+ Open a modal whose content is defined inline as a template.
140
+ The template context exposes `close` and `dismiss` functions.
41
141
 
42
142
  ```typescript
43
- import { ModalService } from 'ng-hub-ui-modal';
44
-
45
- @Component({...})
46
- export class MyComponent {
47
- constructor(private modalService: ModalService) {}
48
-
49
- openModal() {
50
- const modalRef = this.modalService.open(MyModalComponent, {
51
- headerSelector: '.modal-header',
52
- footerSelector: '.modal-footer',
53
- dismissSelector: '[data-dismiss="modal"]',
54
- data: { /* additional data */ }
55
- });
56
- }
143
+ import { Component, inject, TemplateRef } from '@angular/core';
144
+ import { HubModal } from 'ng-hub-ui-modal';
145
+
146
+ @Component({
147
+ selector: 'app-example',
148
+ standalone: true,
149
+ template: `
150
+ <button (click)="open(tpl)">Open Template Modal</button>
151
+
152
+ <ng-template #tpl let-close="close" let-dismiss="dismiss">
153
+ <div class="hub-modal__body">
154
+ <p>This is a template modal.</p>
155
+ <button (click)="dismiss('cancel')">Cancel</button>
156
+ <button (click)="close('ok')">OK</button>
157
+ </div>
158
+ </ng-template>
159
+ `
160
+ })
161
+ export class TemplateModalComponent {
162
+ private modal = inject(HubModal);
163
+
164
+ open(tpl: TemplateRef<unknown>) {
165
+ this.modal
166
+ .open(tpl)
167
+ .result.then((result) => console.log('Closed with', result))
168
+ .catch((reason) => console.log('Dismissed:', reason));
169
+ }
57
170
  }
58
171
  ```
59
172
 
60
- 3. Define the modal component:
173
+ ---
174
+
175
+ ### Open with Component
176
+
177
+ Pass any Angular component class to display it inside the modal.
178
+ The component can inject `HubActiveModal` to close or dismiss the modal from within.
61
179
 
62
180
  ```typescript
63
- import { Component } from '@angular/core';
181
+ import { Component, inject } from '@angular/core';
182
+ import { HubModal, HubActiveModal } from 'ng-hub-ui-modal';
64
183
 
184
+ /** Content component displayed inside the modal */
65
185
  @Component({
66
- template: `
67
- <div class="modal-header">
68
- <h4 class="modal-title">Modal Title</h4>
69
- </div>
70
- <div class="modal-body">
71
- Modal Body
72
- </div>
73
- <div class="modal-footer">
74
- <button type="button" data-dismiss="modal">Close</button>
75
- </div>
76
- `
186
+ selector: 'app-confirm-dialog',
187
+ standalone: true,
188
+ template: `
189
+ <div class="hub-modal__header"><h5>Confirm action</h5></div>
190
+ <div class="hub-modal__body">Are you sure you want to proceed?</div>
191
+ <div class="hub-modal__footer">
192
+ <button (click)="activeModal.dismiss('no')">Cancel</button>
193
+ <button (click)="activeModal.close(true)">Confirm</button>
194
+ </div>
195
+ `
77
196
  })
78
- export class MyModalComponent {}
197
+ export class ConfirmDialogComponent {
198
+ activeModal = inject(HubActiveModal);
199
+ }
200
+
201
+ /** Host component that opens the modal */
202
+ @Component({ selector: 'app-host', standalone: true, template: `<button (click)="openConfirm()">Delete</button>` })
203
+ export class HostComponent {
204
+ private modal = inject(HubModal);
205
+
206
+ openConfirm() {
207
+ this.modal
208
+ .open(ConfirmDialogComponent, {
209
+ headerSelector: '.hub-modal__header',
210
+ footerSelector: '.hub-modal__footer'
211
+ })
212
+ .result.then((confirmed) => {
213
+ if (confirmed) {
214
+ /* perform deletion */
215
+ }
216
+ })
217
+ .catch(() => {});
218
+ }
219
+ }
79
220
  ```
80
221
 
81
- ## Documentation
222
+ ---
82
223
 
83
- ### ModalService
224
+ ### Open with String
84
225
 
85
- The `ModalService` is the main entry point for creating and managing modals in your application.
226
+ Display a quick text message without any additional component or template.
86
227
 
87
- #### `open(component, options?)`
228
+ ```typescript
229
+ this.modal.open('This is a simple string modal.');
230
+ ```
88
231
 
89
- Opens a new modal instance with the provided component and options.
232
+ ---
90
233
 
91
- **Arguments:**
92
- - `component` (`ComponentType<any>`): The component to be displayed in the modal.
93
- - `options` (`ModalOptions` | *optional*): An object containing the configuration options for the modal.
234
+ ### Placement
94
235
 
95
- **Returns:** `ModalRef`
236
+ Anchor the modal to any edge of the viewport using `HubModalPlacement`.
96
237
 
97
- #### `ModalOptions`
238
+ ```typescript
239
+ import { HubModal, HubModalPlacement } from 'ng-hub-ui-modal';
98
240
 
99
- The `ModalOptions` object allows you to configure various aspects of the modal.
241
+ // Right side panel
242
+ this.modal.open(MyComponent, { placement: HubModalPlacement.End });
100
243
 
101
- - `headerSelector` (`string` | *optional*): A CSS selector for the header section of the modal content. Any elements matching this selector will be projected into the modal header.
102
- - `bodySelector` (`string` | *optional*): A CSS selector for the body section of the modal content. Any elements matching this selector will be projected into the modal body.
103
- - `footerSelector` (`string` | *optional*): A CSS selector for the footer section of the modal content. Any elements matching this selector will be projected into the modal footer.
104
- - `dismissSelector` (`string` | *optional*): A CSS selector for elements that should act as dismiss triggers for the modal. When clicked, these elements will dismiss the modal. Default: `'[data-dismiss="modal"]'`.
105
- - `data` (`any` | *optional*): An object containing additional data that will be bound to the modal component instance.
244
+ // Bottom sheet
245
+ this.modal.open(MyComponent, { placement: HubModalPlacement.Bottom });
106
246
 
107
- #### `ModalRef`
247
+ // Left drawer, vertically centered
248
+ this.modal.open(MyComponent, {
249
+ placement: HubModalPlacement.Start,
250
+ centered: true
251
+ });
252
+ ```
108
253
 
109
- The `ModalRef` is a reference to the currently open modal instance. It provides methods to interact with the modal.
254
+ | Value | Effect |
255
+ | -------------------------- | ----------------------------- |
256
+ | `HubModalPlacement.Center` | Centred in viewport (default) |
257
+ | `HubModalPlacement.Start` | Left-anchored drawer |
258
+ | `HubModalPlacement.End` | Right-anchored drawer |
259
+ | `HubModalPlacement.Top` | Top sheet |
260
+ | `HubModalPlacement.Bottom` | Bottom sheet |
110
261
 
111
- - `dismiss(reason?)`: Dismisses the modal with an optional reason.
112
- - `reason` (`any` | *optional*): A value that will be passed to the modal's dismissal event.
113
- - `result`: A promise that resolves when the modal is dismissed, providing the dismissal reason.
262
+ ---
114
263
 
115
- ### Modal Component
264
+ ### Size and Fullscreen
116
265
 
117
- The modal component is the component that you define to be displayed within the modal. It can have any structure and content you desire, but it's recommended to follow the Bootstrap modal structure for consistency.
266
+ ```typescript
267
+ // Predefined sizes
268
+ this.modal.open(MyComponent, { size: 'sm' }); // 'sm' | 'lg' | 'xl'
269
+
270
+ // Always fullscreen
271
+ this.modal.open(MyComponent, { fullscreen: true });
272
+
273
+ // Fullscreen only below 'md' breakpoint
274
+ this.modal.open(MyComponent, { fullscreen: 'md' });
275
+ ```
276
+
277
+ ---
118
278
 
119
- Here's an example modal component:
279
+ ### Scrollable Content
280
+
281
+ When the modal content overflows, enable internal scrolling.
120
282
 
121
283
  ```typescript
122
- import { Component } from '@angular/core';
284
+ this.modal.open(LongContentComponent, { scrollable: true });
285
+ ```
286
+
287
+ ---
288
+
289
+ ### Static Backdrop
290
+
291
+ Prevent dismissal when clicking outside the modal.
292
+
293
+ ```typescript
294
+ this.modal.open(MyComponent, { backdrop: 'static' });
295
+
296
+ // Also disable ESC key
297
+ this.modal.open(MyComponent, { backdrop: 'static', keyboard: false });
298
+ ```
299
+
300
+ ---
301
+
302
+ ### Before Dismiss Guard
303
+
304
+ Use `beforeDismiss` to prevent or delay modal closure, e.g. to show a confirmation first.
305
+
306
+ ```typescript
307
+ this.modal.open(MyFormComponent, {
308
+ beforeDismiss: () => {
309
+ if (this.formIsDirty) {
310
+ return confirm('You have unsaved changes. Really close?');
311
+ }
312
+ return true;
313
+ }
314
+ });
315
+
316
+ // Async guard using a Promise
317
+ this.modal.open(MyComponent, {
318
+ beforeDismiss: () => this.confirmService.ask('Discard changes?')
319
+ });
320
+ ```
321
+
322
+ ---
323
+
324
+ ### HubActiveModal in Content Component
325
+
326
+ Inject `HubActiveModal` into any component used as modal content to control it from within.
327
+
328
+ ```typescript
329
+ import { Component, inject } from '@angular/core';
330
+ import { HubActiveModal, HubModalUpdatableOptions } from 'ng-hub-ui-modal';
123
331
 
124
332
  @Component({
125
- selector: 'app-my-modal',
126
- template: `
127
- <div class="modal-header">
128
- <h4 class="modal-title">{{ title }}</h4>
129
- <button type="button" class="close" data-dismiss="modal" aria-label="Close">
130
- <span aria-hidden="true">&times;</span>
131
- </button>
132
- </div>
133
- <div class="modal-body">
134
- {{ body }}
135
- </div>
136
- <div class="modal-footer">
137
- <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
138
- <button type="button" class="btn btn-primary" (click)="confirm()">Confirm</button>
139
- </div>
140
- `
333
+ selector: 'app-my-modal',
334
+ standalone: true,
335
+ template: `
336
+ <div class="hub-modal__body">
337
+ <button (click)="save()">Save</button>
338
+ <button (click)="cancel()">Cancel</button>
339
+ </div>
340
+ `
141
341
  })
142
342
  export class MyModalComponent {
143
- title: string;
144
- body: string;
145
-
146
- constructor(@Inject(MODAL_DATA) public data: any) {
147
- this.title = data.title;
148
- this.body = data.body;
149
- }
343
+ activeModal = inject(HubActiveModal);
150
344
 
151
- confirm() {
152
- // Perform any necessary actions here
153
- // ...
345
+ save() {
346
+ this.activeModal.close({ saved: true });
347
+ }
154
348
 
155
- // Dismiss the modal
156
- this.modalRef.dismiss('confirmed');
157
- }
349
+ cancel() {
350
+ this.activeModal.dismiss('user_cancelled');
351
+ }
158
352
  }
159
353
  ```
160
354
 
161
- In this example, the modal component receives data through the `MODAL_DATA` injection token, which is populated with the `data` object passed in the `ModalOptions`. The component displays a modal with a header, body, and footer, with a "Cancel" button that dismisses the modal and a "Confirm" button that performs some actions and then dismisses the modal with the reason `'confirmed'`.
355
+ ---
162
356
 
163
- Note that the `modalRef` instance is injected into the modal component automatically by the library, allowing you to interact with the modal from within the component.
357
+ ### Dismiss and Close Selectors
164
358
 
165
- ### Styling
359
+ Automatically bind dismiss/close behaviour to DOM elements inside the modal content using CSS selectors.
166
360
 
167
- The modal component uses Bootstrap's modal styles by default, but you can override them or define your own styles by targeting the appropriate CSS classes. The modal component has the following structure:
361
+ ```typescript
362
+ this.modal.open(MyComponent, {
363
+ dismissSelector: '[data-dismiss="modal"]',
364
+ closeSelector: '[data-close="modal"]'
365
+ });
366
+ ```
168
367
 
169
368
  ```html
170
- <div class="modal">
171
- <div class="modal-dialog">
172
- <div class="modal-content">
173
- <div class="modal-header">
174
- <!-- Header content projected here -->
175
- </div>
176
- <div class="modal-body">
177
- <!-- Body content projected here -->
178
- </div>
179
- <div class="modal-footer">
180
- <!-- Footer content projected here -->
181
- </div>
182
- </div>
183
- </div>
184
- </div>
369
+ <!-- Inside MyComponent template -->
370
+ <button data-dismiss="modal">Cancel</button>
371
+ <button data-close="modal">OK</button>
185
372
  ```
186
373
 
187
- You can target these classes or add your own classes to customize the modal's appearance.
374
+ ---
188
375
 
189
- ## Examples
376
+ ### Multiple Stacked Modals
190
377
 
191
- ### Basic Modal
378
+ Open modals from within a modal — the stack is managed automatically and focus is trapped to the topmost one.
192
379
 
193
380
  ```typescript
194
- import { Component } from '@angular/core';
195
- import { ModalService } from 'ng-hub-ui-modal';
196
-
197
- @Component({
198
- selector: 'app-example',
199
- template: `
200
- <button (click)="openModal()">Open Modal</button>
201
- `
202
- })
203
- export class ExampleComponent {
204
- constructor(private modalService: ModalService) {}
381
+ @Component({ ... })
382
+ export class ParentModalComponent {
383
+ private modal = inject(HubModal);
205
384
 
206
- openModal() {
207
- const modalRef = this.modalService.open(BasicModalComponent);
385
+ openNested() {
386
+ this.modal.open(ChildModalComponent);
208
387
  }
209
388
  }
210
-
211
- @Component({
212
- selector: 'app-basic-modal',
213
- template: `
214
- <div class="modal-header">
215
- <h4 class="modal-title">Basic Modal</h4>
216
- </div>
217
- <div class="modal-body">
218
- This is a basic modal example.
219
- </div>
220
- <div class="modal-footer">
221
- <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
222
- </div>
223
- `
224
- })
225
- export class BasicModalComponent {}
226
389
  ```
227
390
 
228
- ### Modal with Data
391
+ ---
392
+
393
+ ### Observables: dismissAll and hasOpenModals
394
+
395
+ Use the service methods to interact with the entire modal stack.
229
396
 
230
397
  ```typescript
231
- import { Component, Inject } from '@angular/core';
232
- import { ModalService, MODAL_DATA } from 'ng-hub-ui-modal';
398
+ import { HubModal } from 'ng-hub-ui-modal';
233
399
 
234
- @Component({
235
- selector: 'app-example',
236
- template: `
237
- <button (click)="openModal()">Open Modal</button>
238
- `
239
- })
240
- export class ExampleComponent {
241
- constructor(private modalService: ModalService) {}
400
+ export class AppComponent {
401
+ private modal = inject(HubModal);
242
402
 
243
- openModal() {
244
- const modalRef = this.modalService.open(DataModalComponent, {
245
- data: { name: 'John Doe' }
246
- });
247
- }
403
+ closeAll() {
404
+ this.modal.dismissAll('route_change');
405
+ }
406
+
407
+ get anyOpen(): boolean {
408
+ return this.modal.hasOpenModals();
409
+ }
248
410
  }
411
+ ```
249
412
 
250
- @Component({
251
- selector: 'app-data-modal',
252
- template: `
253
- <div class="modal-header">
254
- <h4 class="modal-title">Modal with Data</h4>
255
- </div>
256
- <div class="modal-body">
257
- Hello, {{ name }}!
258
- </div>
259
- <div class="modal-footer">
260
- <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
261
- </div>
262
- `
263
- })
264
- export class DataModalComponent {
265
- name: string;
413
+ Listen to `activeInstances` for reactive updates:
266
414
 
267
- constructor(@Inject(MODAL_DATA) public data: any) {
268
- this.name = data.name;
269
- }
270
- }
415
+ ```typescript
416
+ this.modal.activeInstances.subscribe((refs) => {
417
+ console.log(`${refs.length} modals open`);
418
+ });
271
419
  ```
272
420
 
273
- ### Modal with Content Projection
421
+ ---
422
+
423
+ ## API Reference
424
+
425
+ ### HubModal Service
426
+
427
+ The main entry point for opening and managing modals.
428
+
429
+ | Method | Signature | Description |
430
+ | ----------------- | -------------------------------------- | ----------------------------------------------------- |
431
+ | `open` | `open(content, options?): HubModalRef` | Opens a new modal with the given content and options. |
432
+ | `dismissAll` | `dismissAll(reason?): void` | Dismisses all currently open modals. |
433
+ | `hasOpenModals` | `hasOpenModals(): boolean` | Returns `true` if at least one modal is open. |
434
+ | `activeInstances` | `EventEmitter<HubModalRef[]>` | Emits whenever the stack of open modals changes. |
435
+
436
+ ---
437
+
438
+ ### HubModalRef
439
+
440
+ A reference to an open modal returned by `HubModal.open()`.
441
+
442
+ | Member | Type | Description |
443
+ | ------------------- | ------------------ | ----------------------------------------------------------- |
444
+ | `result` | `Promise<any>` | Resolves on `close()`, rejects on `dismiss()`. |
445
+ | `componentInstance` | `T \| void` | Instance of the content component (if used). |
446
+ | `close(result?)` | `void` | Closes the modal and resolves `result`. |
447
+ | `dismiss(reason?)` | `void` | Dismisses the modal and rejects `result`. |
448
+ | `update(options)` | `void` | Updates modal options after opening. |
449
+ | `closed` | `Observable<any>` | Emits when the modal is closed via `close()`. |
450
+ | `dismissed` | `Observable<any>` | Emits when dismissed via `dismiss()` or user interaction. |
451
+ | `shown` | `Observable<void>` | Emits once the open animation finishes. |
452
+ | `hidden` | `Observable<void>` | Emits once the close animation finishes and DOM is removed. |
453
+
454
+ ---
455
+
456
+ ### HubActiveModal
457
+
458
+ Inject into your content component to control the modal from within.
459
+
460
+ | Method | Description |
461
+ | ------------------ | ---------------------------------------------------- |
462
+ | `close(result?)` | Closes the modal with an optional result. |
463
+ | `dismiss(reason?)` | Dismisses the modal with an optional reason. |
464
+ | `update(options)` | Updates live options (same as `HubModalRef.update`). |
465
+
466
+ ---
467
+
468
+ ### HubModalOptions
469
+
470
+ All options accepted by `HubModal.open()`.
471
+
472
+ | Option | Type | Default | Description |
473
+ | ------------------ | ------------------------------------------------------------ | ------------------------ | ----------------------------------------------------------- |
474
+ | `animation` | `boolean` | `true` | Enables fade in/out transitions. |
475
+ | `ariaLabelledBy` | `string` | — | ID of the element that labels the modal. |
476
+ | `ariaDescribedBy` | `string` | — | ID of the element that describes the modal. |
477
+ | `backdrop` | `boolean \| 'static'` | `true` | `false` = no backdrop, `'static'` = click does not close. |
478
+ | `beforeDismiss` | `() => boolean \| Promise<boolean>` | — | Guard called before dismissal. Return `false` to cancel. |
479
+ | `centered` | `boolean` | `false` | Centers modal on the cross-axis for side placements. |
480
+ | `placement` | `HubModalPlacement` | `Center` | Viewport anchor for the modal. |
481
+ | `container` | `string \| HTMLElement` | `body` | CSS selector or element to which modals are appended. |
482
+ | `fullscreen` | `boolean \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl' \| string` | `false` | Fullscreen always or below a specific breakpoint. |
483
+ | `injector` | `Injector` | — | Custom injector for content component dependencies. |
484
+ | `keyboard` | `boolean` | `true` | Whether ESC key dismisses the modal. |
485
+ | `scrollable` | `boolean` | `false` | Makes the modal body scroll internally. |
486
+ | `size` | `'sm' \| 'lg' \| 'xl' \| string` | — | Controls the width of the modal dialog. |
487
+ | `windowClass` | `string` | — | Extra class added to the `hub-modal` host element. |
488
+ | `modalDialogClass` | `string` | — | Extra class added to the `hub-modal__dialog` element. |
489
+ | `backdropClass` | `string` | — | Extra class added to the `hub-modal__backdrop` element. |
490
+ | `headerSelector` | `string` | — | CSS selector for nodes to project into the header slot. |
491
+ | `footerSelector` | `string` | — | CSS selector for nodes to project into the footer slot. |
492
+ | `dismissSelector` | `string` | `[data-dismiss="modal"]` | Selector for elements that auto-dismiss the modal on click. |
493
+ | `closeSelector` | `string` | `[data-close="modal"]` | Selector for elements that auto-close the modal on click. |
494
+ | `data` | `any` | — | Arbitrary data bound to the content component instance. |
495
+
496
+ ---
497
+
498
+ ### HubModalUpdatableOptions
499
+
500
+ A subset of `HubModalOptions` that can be updated on an already-open modal via `HubModalRef.update()`.
501
+
502
+ `ariaLabelledBy`, `ariaDescribedBy`, `centered`, `placement`, `fullscreen`, `backdropClass`, `size`, `windowClass`, `modalDialogClass`.
503
+
504
+ ---
505
+
506
+ ### HubModalPlacement
274
507
 
275
508
  ```typescript
276
- import { Component } from '@angular/core';
277
- import { ModalService } from 'ng-hub-ui-modal';
509
+ import { HubModalPlacement } from 'ng-hub-ui-modal';
510
+ ```
278
511
 
279
- @Component({
280
- selector: 'app-example',
281
- template: `
282
- <button (click)="openModal()">Open Modal</button>
283
-
284
- <ng-template modalHeader>
285
- <h4 class="modal-title">Modal with Content Projection</h4>
286
- </ng-template>
287
-
288
- <ng-template modalBody>
289
- <p>This is the modal body content.</p>
290
- </ng-template>
291
-
292
- <ng-template modalFooter>
293
- <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
294
- <button type="button" class="btn btn-primary">Save</button>
295
- </ng-template>
296
- `
297
- })
298
- export class ExampleComponent {
299
- constructor(private modalService: ModalService) {}
300
-
301
- openModal() {
302
- const modalRef = this.modalService.open(null, {
303
- headerSelector: '[modalHeader]',
304
- bodySelector: '[modalBody]',
305
- footerSelector: '[modalFooter]'
306
- });
307
- }
308
- }
512
+ | Value | CSS class applied | Description |
513
+ | -------------------------- | ----------------------------- | ---------------------------------------- |
514
+ | `HubModalPlacement.Center` | _(none)_ | Modal centred in the viewport (default). |
515
+ | `HubModalPlacement.Start` | `hub-modal--placement-start` | Left edge anchor. |
516
+ | `HubModalPlacement.End` | `hub-modal--placement-end` | Right edge anchor. |
517
+ | `HubModalPlacement.Top` | `hub-modal--placement-top` | Top edge anchor. |
518
+ | `HubModalPlacement.Bottom` | `hub-modal--placement-bottom` | Bottom edge anchor. |
519
+
520
+ ---
521
+
522
+ ### ModalDismissReasons
523
+
524
+ Built-in dismiss reason constants.
525
+
526
+ ```typescript
527
+ import { ModalDismissReasons } from 'ng-hub-ui-modal';
528
+
529
+ modalRef.dismissed.subscribe((reason) => {
530
+ if (reason === ModalDismissReasons.ESC) {
531
+ /* ESC key */
532
+ }
533
+ if (reason === ModalDismissReasons.BACKDROP_CLICK) {
534
+ /* backdrop */
535
+ }
536
+ });
309
537
  ```
310
538
 
311
- ### Modal with Dismiss Trigger
539
+ ---
540
+
541
+ ### HubModalConfig
542
+
543
+ Inject `HubModalConfig` to provide application-wide default options.
312
544
 
313
545
  ```typescript
314
- import { Component } from '@angular/core';
315
- import { ModalService } from 'ng-hub-ui-modal';
546
+ import { HubModalConfig } from 'ng-hub-ui-modal';
547
+
548
+ @Injectable({ providedIn: 'root' })
549
+ export class AppModalDefaults {
550
+ constructor(config: HubModalConfig) {
551
+ config.animation = true;
552
+ config.keyboard = false;
553
+ config.backdrop = 'static';
554
+ }
555
+ }
556
+ ```
316
557
 
317
- @Component({
318
- selector: 'app-example',
319
- template: `
320
- <button (click)="openModal()">Open Modal</button>
321
-
322
- <ng-template modalContent>
323
- <div class="modal-header">
324
- <h4 class="modal-title">Modal with Dismiss Trigger</h4>
325
- </div>
326
- <div class="modal-body">
327
- <p>Click the button below to dismiss the modal.</p>
328
- <button type="button" class="btn btn-primary" myDismissTrigger>Dismiss</button>
329
- </div>
330
- </ng-template>
331
- `
332
- })
333
- export class ExampleComponent {
334
- constructor(private modalService: ModalService) {}
335
-
336
- openModal() {
337
- const modalRef = this.modalService.open(null, {
338
- bodySelector: '[modalContent]',
339
- dismissSelector: '[myDismissTrigger]'
340
- });
341
- }
558
+ ---
559
+
560
+ ## Styling
561
+
562
+ The library publishes a self-contained stylesheet. Import it once in your application:
563
+
564
+ ```scss
565
+ @import 'ng-hub-ui-modal/src/lib/modal.scss';
566
+ ```
567
+
568
+ ### CSS Variables
569
+
570
+ All visual aspects are controlled via `--hub-modal-*` tokens.
571
+ Full reference: [docs/css-variables-reference.md](./docs/css-variables-reference.md)
572
+
573
+ **Quick reference (most common tokens):**
574
+
575
+ | Variable | Default | Description |
576
+ | ------------------------------ | ------------------ | ------------------------- |
577
+ | `--hub-modal-max-width` | `500px` | Max dialog width |
578
+ | `--hub-modal-border-radius` | `0.5rem` | Dialog corner radius |
579
+ | `--hub-modal-bg` | system surface | Background color |
580
+ | `--hub-modal-color` | system text | Text color |
581
+ | `--hub-modal-header-padding-x` | `1rem` | Header horizontal padding |
582
+ | `--hub-modal-body-padding-x` | `1rem` | Body horizontal padding |
583
+ | `--hub-modal-backdrop-opacity` | `0.5` | Backdrop opacity |
584
+ | `--hub-modal-transition` | `0.2s ease-in-out` | Animation speed |
585
+
586
+ ### Customization Example
587
+
588
+ ```scss
589
+ /* Override at the host element level */
590
+ hub-modal-window {
591
+ --hub-modal-max-width: 720px;
592
+ --hub-modal-border-radius: 1rem;
593
+ --hub-modal-backdrop-opacity: 0.7;
342
594
  }
343
- ```
595
+ ```
596
+
597
+ ### Bootstrap Integration (optional)
598
+
599
+ ```scss
600
+ hub-modal-window {
601
+ --hub-modal-bg: var(--bs-body-bg);
602
+ --hub-modal-color: var(--bs-body-color);
603
+ --hub-modal-border-color: var(--bs-border-color);
604
+ }
605
+ ```
606
+
607
+ ### BEM Class Reference
608
+
609
+ | Class | Element |
610
+ | -------------------------------- | --------------------- |
611
+ | `.hub-modal` | Modal window host |
612
+ | `.hub-modal__backdrop` | Backdrop overlay |
613
+ | `.hub-modal__dialog` | Dialog container |
614
+ | `.hub-modal__content` | Content wrapper |
615
+ | `.hub-modal__header` | Header region |
616
+ | `.hub-modal__body` | Body region |
617
+ | `.hub-modal__footer` | Footer region |
618
+ | `.hub-modal__close` | Built-in close button |
619
+ | `.hub-modal--placement-{value}` | Placement modifier |
620
+ | `.hub-modal__dialog--centered` | Vertical centering |
621
+ | `.hub-modal__dialog--scrollable` | Scrollable body |
622
+ | `.hub-modal__dialog--fullscreen` | Fullscreen modifier |
623
+
624
+ ---
625
+
626
+ ## Contributing
627
+
628
+ ### Development Setup
629
+
630
+ ```bash
631
+ git clone https://github.com/carlos-morcillo/ng-hub-ui-modal.git
632
+ cd ng-hub-ui-modal
633
+ npm install
634
+ ```
635
+
636
+ Build the library in watch mode:
637
+
638
+ ```bash
639
+ ng build modal --watch
640
+ ```
641
+
642
+ Serve the demo application:
643
+
644
+ ```bash
645
+ ng serve
646
+ ```
647
+
648
+ ### Testing
649
+
650
+ ```bash
651
+ ng test modal
652
+ ```
653
+
654
+ ### Commit Guidelines
655
+
656
+ Commits follow the [Conventional Commits](https://www.conventionalcommits.org/) format:
657
+
658
+ ```
659
+ feat(modal): add new placement option
660
+ fix(modal): correct backdrop z-index
661
+ docs(modal): update CSS variable table
662
+ ```
663
+
664
+ ---
665
+
666
+ ## Support & License
667
+
668
+ If this library saves you time, consider supporting further development:
669
+
670
+ ☕ [Buy me a coffee](https://www.buymeacoffee.com/carlosmorcillo)
671
+
672
+ **MIT License** — © [Carlos Morcillo](https://github.com/carlos-morcillo)