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