@rivinia/modal 0.5.1 → 0.5.2
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 +235 -171
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,213 +1,277 @@
|
|
|
1
1
|
# @rivinia/modal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
An Angular modal manager with typed results, stack/queue admission, dismissal
|
|
4
|
+
policies, async guards, and optional ready-made UI. The core package owns
|
|
5
|
+
modal state and lifecycle but does not render or style dialogs.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
- Install: `npm install @rivinia/modal`
|
|
8
|
+
- Prepared patch: **0.5.2** (latest published: **0.5.1**)
|
|
9
|
+
- Peer dependencies: `@angular/core ^22.0.0` and `@angular/common ^22.0.0`
|
|
10
|
+
- `@rivinia/modal` is the headless manager.
|
|
11
|
+
- `@rivinia/modal/ui` contains standalone dialog and host components.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- Requires: **Angular `^22.0.0`**, `tslib`
|
|
13
|
+
## Configure the manager
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @rivinia/modal
|
|
18
|
-
ng add @rivinia/modal # wires provideRiviniaModal() into app.config.ts
|
|
19
|
-
```
|
|
15
|
+
Register the provider once in the application's root environment providers.
|
|
16
|
+
The defaults use a stack with up to five visible modals and queue overflow:
|
|
20
17
|
|
|
21
18
|
```ts
|
|
22
19
|
// app.config.ts
|
|
20
|
+
import type { ApplicationConfig } from '@angular/core';
|
|
23
21
|
import { provideRiviniaModal } from '@rivinia/modal';
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
export const appConfig: ApplicationConfig = {
|
|
24
|
+
providers: [
|
|
25
|
+
provideRiviniaModal({
|
|
26
|
+
strategy: 'stack',
|
|
27
|
+
maxVisible: 3,
|
|
28
|
+
closingTimeout: 300,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
};
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
`ng add @rivinia/modal` can insert `provideRiviniaModal()` into a detected
|
|
35
|
+
standalone app configuration. It does not add a rendering host; add one as
|
|
36
|
+
described below. You can also configure the provider manually as above.
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
export class Users {
|
|
32
|
-
private modals = injectRiviniaModal();
|
|
38
|
+
## Open a typed modal
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
data: { name: user.name },
|
|
37
|
-
ref: 'confirm-delete',
|
|
38
|
-
});
|
|
39
|
-
const result = await handle.result;
|
|
40
|
-
if (result.status === 'confirmed' && result.value) {
|
|
41
|
-
this.delete(user);
|
|
42
|
-
}
|
|
43
|
-
// 'cancelled' | 'dismissed' | 'queued-cancelled' | 'owner-destroyed' | 'rejected'
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
```
|
|
40
|
+
This standalone component uses a typed template, mounts one shared modal host,
|
|
41
|
+
and waits for the result before changing application state:
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
```ts
|
|
44
|
+
import { Component, TemplateRef, ViewChild, signal } from '@angular/core';
|
|
45
|
+
import type { ModalHandle, ModalTemplateContext } from '@rivinia/modal';
|
|
46
|
+
import { injectRiviniaModal } from '@rivinia/modal';
|
|
47
|
+
import { RiviniaModalHost } from '@rivinia/modal/ui';
|
|
48
|
+
|
|
49
|
+
interface User {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
}
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
reject the promise instead of becoming variants.
|
|
54
|
+
interface DeleteData {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
@Component({
|
|
60
|
+
standalone: true,
|
|
61
|
+
imports: [RiviniaModalHost],
|
|
62
|
+
template: `
|
|
63
|
+
@for (user of users(); track user.id) {
|
|
64
|
+
<button type="button" (click)="askToDelete(user)">Delete {{ user.name }}</button>
|
|
65
|
+
}
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
<ng-template #deleteConfirmation let-data>
|
|
68
|
+
<p>Delete {{ data.name }}?</p>
|
|
69
|
+
<button type="button" (click)="confirmDelete()">Delete user</button>
|
|
70
|
+
<button type="button" (click)="cancelDelete()">Cancel</button>
|
|
71
|
+
</ng-template>
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
<rv-modal-host [modal]="modals" size="sm" />
|
|
74
|
+
`,
|
|
75
|
+
})
|
|
76
|
+
export class UsersPage {
|
|
77
|
+
private readonly manager = injectRiviniaModal();
|
|
78
|
+
protected readonly modals = this.manager;
|
|
79
|
+
readonly users = signal<User[]>([{ id: '42', name: 'Ada Lovelace' }]);
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```
|
|
81
|
+
@ViewChild('deleteConfirmation', { static: true })
|
|
82
|
+
private deleteTemplate!: TemplateRef<ModalTemplateContext<DeleteData>>;
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
normally (a queued request sharing the dead owner is still torn down by its own
|
|
82
|
-
callback, settling `owner-destroyed`). Opening with an already-destroyed owner
|
|
83
|
-
settles `owner-destroyed` immediately without adding anything.
|
|
84
|
+
private pendingDelete: ModalHandle<boolean> | null = null;
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
modals throws — close everything first.
|
|
86
|
+
async askToDelete(user: User): Promise<void> {
|
|
87
|
+
if (this.pendingDelete) return;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
const handle = this.manager.openModal<DeleteData, boolean>(this.deleteTemplate, {
|
|
90
|
+
data: { id: user.id, name: user.name },
|
|
91
|
+
ref: 'confirm-delete',
|
|
92
|
+
metadata: { title: 'Delete user' },
|
|
93
|
+
dismiss: { backdrop: 'static' },
|
|
94
|
+
});
|
|
95
|
+
this.pendingDelete = handle;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await handle.result;
|
|
99
|
+
if (result.status === 'confirmed' && result.value) {
|
|
100
|
+
this.users.update((users) => users.filter((entry) => entry.id !== user.id));
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (this.pendingDelete === handle) this.pendingDelete = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
confirmDelete(): void {
|
|
108
|
+
// ModalHandle<boolean> checks that the confirmed value is a boolean.
|
|
109
|
+
this.pendingDelete?.close(true);
|
|
110
|
+
}
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
cancelDelete(): void {
|
|
113
|
+
this.pendingDelete?.cancel('user-cancelled');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
98
116
|
```
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
The UI host uses `metadata.title` by default, so the example renders
|
|
119
|
+
“Delete user” without custom title logic. Import `RiviniaModalHost` into the
|
|
120
|
+
standalone component that renders it. For an NgModule-based app, import this
|
|
121
|
+
standalone component in the module's `imports` array, not `declarations`.
|
|
122
|
+
Mount a single host for the manager you pass to it.
|
|
101
123
|
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
`openModal<TData, TResult>()` accepts a `TemplateRef`, a component type, or a
|
|
125
|
+
`ModalContent<TData>` descriptor. Template content receives `data` as
|
|
126
|
+
`let-data` and the per-modal operations as `let-modal="modal"`. For component
|
|
127
|
+
content, pass `inputs` in the options; those keys are `Record<string, unknown>`
|
|
128
|
+
and are not checked against the component's inputs at compile time. The
|
|
129
|
+
optional `RIVINIA_MODAL_CONTEXT` injection token supplies the same per-modal
|
|
130
|
+
context to a dynamically created component.
|
|
107
131
|
|
|
108
|
-
|
|
109
|
-
Nothing here loads, no styles leak.
|
|
132
|
+
The result is a discriminated `ModalResult<TResult>`:
|
|
110
133
|
|
|
111
|
-
|
|
134
|
+
- `confirmed` carries the typed `value`.
|
|
135
|
+
- `cancelled` and `dismissed` may carry a `reason`.
|
|
136
|
+
- `owner-destroyed` and `queued-cancelled` identify lifecycle cleanup.
|
|
137
|
+
- `rejected` identifies a request refused by the configured admission or
|
|
138
|
+
overflow policy.
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
Narrow on `result.status` before reading `result.value`. Results settle once
|
|
141
|
+
when closing starts; callers do not wait for the exit animation. A component
|
|
142
|
+
that owns a handle may use `close(value)`, `cancel(reason)`, or
|
|
143
|
+
`dismiss(reason)`. `ModalHandle.close()` enforces `TResult`; the `close()`
|
|
144
|
+
method on template/component context accepts `unknown`, so prefer the handle
|
|
145
|
+
when the result type matters.
|
|
116
146
|
|
|
117
|
-
|
|
118
|
-
exits play, then the host calls `confirmClosed()` (match `closingTimeout`
|
|
119
|
-
to the animation duration; default 300 ms covers the 150 ms default).
|
|
147
|
+
## Dismissal, guards, and lifecycle
|
|
120
148
|
|
|
121
|
-
|
|
149
|
+
The default dismissal policy allows Escape and backdrop click. Configure
|
|
150
|
+
global defaults, named presets, or per-modal overrides:
|
|
122
151
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
```ts
|
|
153
|
+
provideRiviniaModal({
|
|
154
|
+
dismiss: { closeOnEscape: true, backdrop: 'static' },
|
|
155
|
+
presets: {
|
|
156
|
+
critical: { dismiss: { closeOnEscape: false, backdrop: 'static' } },
|
|
157
|
+
},
|
|
158
|
+
});
|
|
129
159
|
```
|
|
130
160
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
Dismissal policy fields merge independently in this order: built-in defaults,
|
|
162
|
+
global config, named preset, and per-open options. A missing key at a later
|
|
163
|
+
layer does not erase an earlier setting. `backdrop` may be `close`, `static`,
|
|
164
|
+
or `disabled`.
|
|
165
|
+
|
|
166
|
+
`RiviniaModalHost` routes Escape, backdrop, and close-button intents through
|
|
167
|
+
`requestDismiss()`, which runs any async `guards` supplied when opening. A
|
|
168
|
+
guard that returns `false` or throws keeps the modal open. The direct
|
|
169
|
+
`close()`, `dismiss()`, and `cancel()` methods on a handle are guard-free;
|
|
170
|
+
use `requestDismiss(id, origin)` when programmatic code should also be checked.
|
|
171
|
+
Forced cleanup such as owner destruction and `dismissAll()` bypasses guards.
|
|
172
|
+
|
|
173
|
+
The manager supports `strategy: 'stack' | 'queue'` (or a custom
|
|
174
|
+
`ModalStrategy`). Stack allows up to `maxVisible` active modals, with the
|
|
175
|
+
latest on top; queue activates one at a time in FIFO order. A modal in its
|
|
176
|
+
exit animation normally occupies a visible slot until the host confirms
|
|
177
|
+
teardown or the timeout finalizes it; `dismiss-oldest` preempts an active
|
|
178
|
+
instance to make room while its exit runs. When capacity is full, `overflow`
|
|
179
|
+
may queue (default), dismiss the oldest active modal, or reject the new request.
|
|
180
|
+
`maxQueue` bounds queued requests when queue overflow is enabled.
|
|
181
|
+
|
|
182
|
+
An optional `owner: DestroyRef` ties a modal to a component/service lifetime.
|
|
183
|
+
Destroying the owner settles an active request as `owner-destroyed`; a queued
|
|
184
|
+
request settles as `queued-cancelled`. Without an owner, the modal is
|
|
185
|
+
independent of the caller's lifetime. Set `parentId` to associate a child
|
|
186
|
+
modal with a parent; by default, closing a parent closes its descendants.
|
|
187
|
+
`descendants: 'prevent-parent-close'` instead blocks parent closure while a
|
|
188
|
+
descendant remains open.
|
|
189
|
+
|
|
190
|
+
When a modal enters `closing`, its result has already settled but it remains
|
|
191
|
+
visible while the UI exits. The UI host calls `confirmClosed()` after its exit
|
|
192
|
+
animation. The default `closingTimeout` is 300 ms; set it to cover the
|
|
193
|
+
renderer’s exit duration, or set it to `0` when a custom renderer will always
|
|
194
|
+
acknowledge closure manually. Changing strategy with active, queued, or
|
|
195
|
+
closing modals throws; close them first.
|
|
196
|
+
|
|
197
|
+
## Ready-made UI
|
|
198
|
+
|
|
199
|
+
`@rivinia/modal/ui` is optional. It provides a native `<dialog>` component
|
|
200
|
+
(`RiviniaModalDialog`) for UI-only use and `RiviniaModalHost` to render
|
|
201
|
+
manager-owned modals. The host passes dismissal intents to the manager before
|
|
202
|
+
closing, so guards and promise results remain coordinated.
|
|
203
|
+
|
|
204
|
+
For a UI-only dialog, import the standalone component and bind its `open`
|
|
205
|
+
model. This mode has no manager, guards, or `ModalHandle` promise:
|
|
156
206
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```html
|
|
177
|
-
<rv-modal-dialog [(open)]="open" [title]="'Edit user'">
|
|
178
|
-
<!-- paints above this dialog; positions are still viewport-anchored -->
|
|
179
|
-
<rv-toast-list [manager]="injectRiviniaToast()" [position]="'top-end'" />
|
|
180
|
-
<form>…</form>
|
|
181
|
-
</rv-modal-dialog>
|
|
207
|
+
```ts
|
|
208
|
+
import { Component } from '@angular/core';
|
|
209
|
+
import { RiviniaModalDialog } from '@rivinia/modal/ui';
|
|
210
|
+
|
|
211
|
+
@Component({
|
|
212
|
+
standalone: true,
|
|
213
|
+
imports: [RiviniaModalDialog],
|
|
214
|
+
template: `
|
|
215
|
+
<button type="button" (click)="open = true">Preview</button>
|
|
216
|
+
<rv-modal-dialog #dialog [(open)]="open" title="Preview">
|
|
217
|
+
<p>Dialog content.</p>
|
|
218
|
+
<button type="button" (click)="dialog.confirm(true)">Continue</button>
|
|
219
|
+
<button type="button" (click)="dialog.cancel('cancelled')">Cancel</button>
|
|
220
|
+
</rv-modal-dialog>
|
|
221
|
+
`,
|
|
222
|
+
})
|
|
223
|
+
export class PreviewDialog {
|
|
224
|
+
open = false;
|
|
225
|
+
}
|
|
182
226
|
```
|
|
183
227
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`
|
|
228
|
+
`RiviniaModalDialog` accepts `open`, `title`, `description`, `ariaLabel`,
|
|
229
|
+
`size`, `position`, `animation`, `closeOnEscape`, `backdrop`, and
|
|
230
|
+
`showCloseButton`; its `open` model supports `[(open)]`. In UI-only mode it
|
|
231
|
+
emits `confirmed`, `dismissed`, and `cancelled`. In managed use, the host
|
|
232
|
+
accepts the required `modal` manager input plus `size`, `showCloseButton`,
|
|
233
|
+
and `titleOf` overrides; observe the manager's `ModalHandle.result` instead
|
|
234
|
+
of the dialog outputs.
|
|
235
|
+
`RvModalHeader` and `RvModalFooter` are marker directives for custom header
|
|
236
|
+
and footer projection; import the marker used by the template and apply it
|
|
237
|
+
alongside `rv-modal-header` or `rv-modal-footer`. A custom header replaces the
|
|
238
|
+
default title and close button. Without a footer slot, no footer region is
|
|
239
|
+
rendered.
|
|
240
|
+
|
|
241
|
+
The native dialog provides focus containment and restores focus to its
|
|
242
|
+
invoker. The UI includes scroll locking, reduced-motion support, RTL-aware
|
|
243
|
+
logical positions, and responsive bottom-sheet styling below 640px for
|
|
244
|
+
center/top/bottom placements. The UI exposes size, position, animation,
|
|
245
|
+
backdrop, Escape, and close-button inputs. During server rendering it remains
|
|
246
|
+
closed and opens on the client after hydration. Theme it with CSS custom
|
|
247
|
+
properties such as `--rv-modal-bg`, `--rv-modal-fg`, `--rv-modal-border`,
|
|
248
|
+
`--rv-modal-radius`, and `--rv-modal-width-md`.
|
|
249
|
+
|
|
250
|
+
## Public core exports
|
|
251
|
+
|
|
252
|
+
The core entry point exports:
|
|
253
|
+
|
|
254
|
+
- `provideRiviniaModal()` and `injectRiviniaModal()` for configuration and
|
|
255
|
+
injection, plus `RiviniaModal` for direct manager APIs.
|
|
256
|
+
- `StackStrategy`, `QueueStrategy`, and the `ModalStrategy` contract.
|
|
257
|
+
- `RiviniaModalConfig`, `ModalPreset`, `ModalHandle`, `ModalResult`,
|
|
258
|
+
`ModalContent`, `ModalOpenOptions`, `ModalUpdate`, `ModalInstance`,
|
|
259
|
+
`ModalTemplateContext`, `ModalContext`, `ModalGuard`,
|
|
260
|
+
`ModalDismissPolicy`, `ModalDismissOrigin`, `ModalOverflowPolicy`,
|
|
261
|
+
`ModalStatus`, `ModalDiagnostic`, and `DescendantPolicy`.
|
|
262
|
+
- `RIVINIA_MODAL_CONTEXT` for dynamic modal components.
|
|
263
|
+
|
|
264
|
+
`RiviniaModal` exposes readonly `active`, `queued`, `closing`, `visible`, and
|
|
265
|
+
`top` signals, along with `openModal`, `close`, `dismiss`, `cancel`,
|
|
266
|
+
`requestDismiss`, `update`, `confirmClosed`, `dismissAll`, `findByRef`,
|
|
267
|
+
`useStrategy`, and diagnostics helpers. Inspect the signal values by calling
|
|
268
|
+
them, for example `manager.top()`.
|
|
269
|
+
|
|
270
|
+
The `@rivinia/modal/ui` entry point exports `RiviniaModalDialog`,
|
|
271
|
+
`RiviniaModalHost`, `RiviniaModalHostManager`, `RiviniaModalScrollLock`,
|
|
272
|
+
`RvModalHeader`, and `RvModalFooter`, plus the `RiviniaModalAnimation`,
|
|
273
|
+
`RiviniaModalBackdrop`, `RiviniaModalPosition`, `RiviniaModalSize`, and
|
|
274
|
+
`RiviniaModalUiOptions` types.
|
|
211
275
|
|
|
212
276
|
## License
|
|
213
277
|
|