@rivinia/modal 0.5.2 → 0.5.3

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.
Files changed (2) hide show
  1. package/README.md +220 -27
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ policies, async guards, and optional ready-made UI. The core package owns
5
5
  modal state and lifecycle but does not render or style dialogs.
6
6
 
7
7
  - Install: `npm install @rivinia/modal`
8
- - Prepared patch: **0.5.2** (latest published: **0.5.1**)
8
+ - Package version: **0.5.3**
9
9
  - Peer dependencies: `@angular/core ^22.0.0` and `@angular/common ^22.0.0`
10
10
  - `@rivinia/modal` is the headless manager.
11
11
  - `@rivinia/modal/ui` contains standalone dialog and host components.
@@ -37,8 +37,9 @@ described below. You can also configure the provider manually as above.
37
37
 
38
38
  ## Open a typed modal
39
39
 
40
- This standalone component uses a typed template, mounts one shared modal host,
41
- and waits for the result before changing application state:
40
+ The examples use standalone components and separate `.ts`/`.html` files. Mount
41
+ one shared `RiviniaModalHost` for the manager. This first version uses the
42
+ classic `@ViewChild` query, which is still supported in Angular 22:
42
43
 
43
44
  ```ts
44
45
  import { Component, TemplateRef, ViewChild, signal } from '@angular/core';
@@ -59,19 +60,7 @@ interface DeleteData {
59
60
  @Component({
60
61
  standalone: true,
61
62
  imports: [RiviniaModalHost],
62
- template: `
63
- @for (user of users(); track user.id) {
64
- <button type="button" (click)="askToDelete(user)">Delete {{ user.name }}</button>
65
- }
66
-
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>
72
-
73
- <rv-modal-host [modal]="modals" size="sm" />
74
- `,
63
+ templateUrl: './users-page.component.html',
75
64
  })
76
65
  export class UsersPage {
77
66
  private readonly manager = injectRiviniaModal();
@@ -115,11 +104,90 @@ export class UsersPage {
115
104
  }
116
105
  ```
117
106
 
107
+ ```html
108
+ <!-- src/app/users-page.component.html -->
109
+ @for (user of users(); track user.id) {
110
+ <button type="button" (click)="askToDelete(user)">Delete {{ user.name }}</button>
111
+ }
112
+
113
+ <ng-template #deleteConfirmation let-data>
114
+ <p>Delete {{ data.name }}?</p>
115
+ <button type="button" (click)="confirmDelete()">Delete user</button>
116
+ <button type="button" (click)="cancelDelete()">Cancel</button>
117
+ </ng-template>
118
+
119
+ <rv-modal-host [modal]="modals" size="sm" />
120
+ ```
121
+
122
+ ### Signal-based view query
123
+
124
+ For new code, Angular 22 also supports the `viewChild` signal query. Keep the
125
+ same HTML; call the query signal to get its `TemplateRef`:
126
+
127
+ ```ts
128
+ // src/app/users-page.component.ts
129
+ import { Component, TemplateRef, signal, viewChild } from '@angular/core';
130
+ import type { ModalHandle, ModalTemplateContext } from '@rivinia/modal';
131
+ import { injectRiviniaModal } from '@rivinia/modal';
132
+ import { RiviniaModalHost } from '@rivinia/modal/ui';
133
+
134
+ interface User {
135
+ readonly id: string;
136
+ readonly name: string;
137
+ }
138
+
139
+ interface DeleteData {
140
+ readonly id: string;
141
+ readonly name: string;
142
+ }
143
+
144
+ @Component({
145
+ standalone: true,
146
+ imports: [RiviniaModalHost],
147
+ templateUrl: './users-page.component.html',
148
+ })
149
+ export class UsersPage {
150
+ private readonly manager = injectRiviniaModal();
151
+ protected readonly modals = this.manager;
152
+ readonly users = signal<User[]>([{ id: '42', name: 'Ada Lovelace' }]);
153
+ private readonly deleteTemplate =
154
+ viewChild.required<TemplateRef<ModalTemplateContext<DeleteData>>>('deleteConfirmation');
155
+ private pendingDelete: ModalHandle<boolean> | null = null;
156
+
157
+ async askToDelete(user: User): Promise<void> {
158
+ if (this.pendingDelete) return;
159
+
160
+ const handle = this.manager.openModal<DeleteData, boolean>(this.deleteTemplate(), {
161
+ data: { id: user.id, name: user.name },
162
+ ref: 'confirm-delete',
163
+ metadata: { title: 'Delete user' },
164
+ dismiss: { backdrop: 'static' },
165
+ });
166
+ this.pendingDelete = handle;
167
+ try {
168
+ const result = await handle.result;
169
+ if (result.status === 'confirmed' && result.value) {
170
+ this.users.update((users) => users.filter((entry) => entry.id !== user.id));
171
+ }
172
+ } finally {
173
+ if (this.pendingDelete === handle) this.pendingDelete = null;
174
+ }
175
+ }
176
+
177
+ confirmDelete(): void {
178
+ this.pendingDelete?.close(true);
179
+ }
180
+
181
+ cancelDelete(): void {
182
+ this.pendingDelete?.cancel('user-cancelled');
183
+ }
184
+ }
185
+ ```
186
+
118
187
  The UI host uses `metadata.title` by default, so the example renders
119
188
  “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.
189
+ standalone component that renders it. All examples in this guide target
190
+ standalone Angular 22 applications.
123
191
 
124
192
  `openModal<TData, TResult>()` accepts a `TemplateRef`, a component type, or a
125
193
  `ModalContent<TData>` descriptor. Template content receives `data` as
@@ -129,6 +197,128 @@ and are not checked against the component's inputs at compile time. The
129
197
  optional `RIVINIA_MODAL_CONTEXT` injection token supplies the same per-modal
130
198
  context to a dynamically created component.
131
199
 
200
+ ### Component content, modal context, and `NgComponentOutlet`
201
+
202
+ When the content is a component type, the host renders it with Angular's
203
+ `NgComponentOutlet` and creates an injector scoped to that modal instance.
204
+ Inject `RIVINIA_MODAL_CONTEXT` for that instance's `id`, `data`, `close()`, and
205
+ `dismiss()`; ordinary app services still resolve from the surrounding
206
+ injectors. The host supplies this context, so do not build a separate
207
+ `Injector` just to pass modal data or lifecycle operations.
208
+
209
+ ```ts
210
+ // src/app/delete-confirmation.component.ts
211
+ import { Component, inject } from '@angular/core';
212
+ import { RIVINIA_MODAL_CONTEXT } from '@rivinia/modal';
213
+
214
+ @Component({
215
+ standalone: true,
216
+ templateUrl: './delete-confirmation.component.html',
217
+ })
218
+ export class DeleteConfirmationComponent {
219
+ readonly modal = inject(RIVINIA_MODAL_CONTEXT);
220
+
221
+ confirm(): void {
222
+ this.modal.close(true);
223
+ }
224
+
225
+ cancel(): void {
226
+ this.modal.dismiss('user-cancelled');
227
+ }
228
+ }
229
+ ```
230
+
231
+ ```html
232
+ <!-- src/app/delete-confirmation.component.html -->
233
+ <h2>Delete this user?</h2>
234
+ <button type="button" (click)="confirm()">Delete</button>
235
+ <button type="button" (click)="cancel()">Cancel</button>
236
+ ```
237
+
238
+ Open the component type through the same manager and host used for template
239
+ content:
240
+
241
+ ```ts
242
+ const handle = this.manager.openModal<DeleteConfirmationComponent, DeleteData, boolean>(
243
+ DeleteConfirmationComponent,
244
+ {
245
+ data: { id: user.id, name: user.name },
246
+ metadata: { title: 'Delete user' },
247
+ },
248
+ );
249
+ const result = await handle.result;
250
+ if (result.status === 'confirmed' && result.value) {
251
+ this.removeUser(user.id);
252
+ }
253
+ ```
254
+
255
+ The injection token is typed as `ModalContext<unknown>` because content may
256
+ come from different callers. Narrow or validate `modal.data` before reading
257
+ application-specific fields. The opener's `ModalHandle<boolean>` remains
258
+ typed, so `handle.close()` checks the result type. Component `inputs` can also
259
+ be supplied in `openModal` options, but their keys are not type-checked.
260
+
261
+ ### Showing a toast inside an open modal
262
+
263
+ The toast list is fixed-position UI, not a popover. A native `<dialog>` is in
264
+ the browser's top layer, so a toast list rendered outside it can appear behind
265
+ the dialog regardless of its `z-index`. When a notification belongs to the
266
+ open modal, put its `RiviniaToastList` inside the modal template so it is
267
+ rendered inside the dialog:
268
+
269
+ ```ts
270
+ // src/app/payment-page.component.ts
271
+ import { Component, TemplateRef, inject, viewChild } from '@angular/core';
272
+ import type { ModalTemplateContext } from '@rivinia/modal';
273
+ import { RiviniaModal } from '@rivinia/modal';
274
+ import { RiviniaModalHost } from '@rivinia/modal/ui';
275
+ import { RiviniaToast } from '@rivinia/toast';
276
+ import { RiviniaToastList } from '@rivinia/toast/ui';
277
+
278
+ @Component({
279
+ standalone: true,
280
+ imports: [RiviniaModalHost, RiviniaToastList],
281
+ templateUrl: './payment-page.component.html',
282
+ })
283
+ export class PaymentPageComponent {
284
+ readonly modal = inject(RiviniaModal);
285
+ readonly toast = inject(RiviniaToast);
286
+ private readonly paymentTemplate =
287
+ viewChild.required<TemplateRef<ModalTemplateContext<void>>>('paymentConfirmation');
288
+
289
+ openPayment(): void {
290
+ this.modal.openModal<void, void>(this.paymentTemplate(), {
291
+ data: undefined,
292
+ metadata: { title: 'Confirm payment' },
293
+ });
294
+ }
295
+
296
+ showPaymentSaved(): void {
297
+ this.toast.show({ message: 'Payment saved', kind: 'success' });
298
+ }
299
+ }
300
+ ```
301
+
302
+ ```html
303
+ <!-- src/app/payment-page.component.html -->
304
+ <button type="button" (click)="openPayment()">Pay invoice</button>
305
+
306
+ <ng-template #paymentConfirmation let-modal="modal">
307
+ <p>Submit payment for invoice INV-2048?</p>
308
+ <button type="button" (click)="showPaymentSaved()">Show saved notice</button>
309
+ <button type="button" (click)="modal.close()">Done</button>
310
+ <rv-toast-list [manager]="toast" position="top-end" />
311
+ </ng-template>
312
+
313
+ <rv-modal-host [modal]="modal" size="sm" />
314
+ ```
315
+
316
+ Register `provideRiviniaToast()` as well as `provideRiviniaModal()` in the
317
+ application's root providers. Use one `RiviniaToastList` per toast manager at a
318
+ time: if the app already renders that same manager globally, move or gate that
319
+ list while this modal is open rather than mounting a second list and showing
320
+ every notification twice.
321
+
132
322
  The result is a discriminated `ModalResult<TResult>`:
133
323
 
134
324
  - `confirmed` carries the typed `value`.
@@ -211,20 +401,23 @@ import { RiviniaModalDialog } from '@rivinia/modal/ui';
211
401
  @Component({
212
402
  standalone: true,
213
403
  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
- `,
404
+ templateUrl: './preview-dialog.component.html',
222
405
  })
223
406
  export class PreviewDialog {
224
407
  open = false;
225
408
  }
226
409
  ```
227
410
 
411
+ ```html
412
+ <!-- src/app/preview-dialog.component.html -->
413
+ <button type="button" (click)="open = true">Preview</button>
414
+ <rv-modal-dialog #dialog [(open)]="open" title="Preview">
415
+ <p>Dialog content.</p>
416
+ <button type="button" (click)="dialog.confirm(true)">Continue</button>
417
+ <button type="button" (click)="dialog.cancel('cancelled')">Cancel</button>
418
+ </rv-modal-dialog>
419
+ ```
420
+
228
421
  `RiviniaModalDialog` accepts `open`, `title`, `description`, `ariaLabel`,
229
422
  `size`, `position`, `animation`, `closeOnEscape`, `backdrop`, and
230
423
  `showCloseButton`; its `open` model supports `[(open)]`. In UI-only mode it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/modal",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Headless modal manager for Angular: stack/queue strategies, typed results, guards, ownership.",
5
5
  "keywords": [
6
6
  "angular",