@rivinia/toast 0.4.0 → 0.4.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 +216 -90
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,136 +3,262 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rivinia/toast)
|
|
4
4
|
[](https://www.npmjs.com/package/@rivinia/toast)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
imports UI.
|
|
6
|
+
An Angular signal-based toast queue with timers, actions, deduplication, and
|
|
7
|
+
overflow handling. Core is headless; an optional standalone UI is available at
|
|
8
|
+
`@rivinia/toast/ui`.
|
|
10
9
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
10
|
+
- Package version: **0.4.2**
|
|
11
|
+
- Peer dependency: **`@angular/core` `^22.0.0`**
|
|
12
|
+
- No `tslib`, router, or HTTP peer dependency.
|
|
13
13
|
|
|
14
|
-
## Install
|
|
14
|
+
## Install and configure
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install @rivinia/toast
|
|
18
|
-
ng add @rivinia/toast
|
|
18
|
+
ng add @rivinia/toast
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
`ng add` adds `provideRiviniaToast()` to a standalone app's `app.config.ts`; it
|
|
22
|
+
does not add the optional UI component. You can also configure the service
|
|
23
|
+
manually:
|
|
24
|
+
|
|
21
25
|
```ts
|
|
22
|
-
// app.config.ts
|
|
26
|
+
// src/app/app.config.ts
|
|
27
|
+
import type { ApplicationConfig } from '@angular/core';
|
|
23
28
|
import { provideRiviniaToast } from '@rivinia/toast';
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
export const appConfig: ApplicationConfig = {
|
|
31
|
+
providers: [
|
|
32
|
+
provideRiviniaToast({
|
|
33
|
+
duration: 5000,
|
|
34
|
+
durations: { error: 8000 },
|
|
35
|
+
maxVisible: 5,
|
|
36
|
+
overflow: 'dismiss-oldest',
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
};
|
|
26
40
|
```
|
|
27
41
|
|
|
28
|
-
|
|
42
|
+
All provider fields are optional. Defaults are 5000 ms, five active toasts,
|
|
43
|
+
an unbounded queue, and `dismiss-oldest` overflow. `ng add` also accepts
|
|
44
|
+
`--duration` and `--max-visible`.
|
|
45
|
+
|
|
46
|
+
## Use with the optional UI
|
|
47
|
+
|
|
48
|
+
The UI is a separate secondary entry point; import its component explicitly.
|
|
49
|
+
This standalone example keeps the injected manager in a class field (injection
|
|
50
|
+
must not be called from the template):
|
|
29
51
|
|
|
30
52
|
```ts
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
// src/app/notifications.component.ts
|
|
54
|
+
import { Component } from '@angular/core';
|
|
55
|
+
import { injectRiviniaToast } from '@rivinia/toast';
|
|
56
|
+
import { RiviniaToastList } from '@rivinia/toast/ui';
|
|
57
|
+
|
|
58
|
+
@Component({
|
|
59
|
+
selector: 'app-notifications',
|
|
60
|
+
standalone: true,
|
|
61
|
+
imports: [RiviniaToastList],
|
|
62
|
+
templateUrl: './notifications.component.html',
|
|
63
|
+
})
|
|
64
|
+
export class NotificationsComponent {
|
|
65
|
+
readonly toast = injectRiviniaToast();
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
save(): void {
|
|
68
|
+
this.toast.show({
|
|
69
|
+
message: 'Changes saved',
|
|
70
|
+
kind: 'success',
|
|
71
|
+
dedupeKey: 'save',
|
|
72
|
+
});
|
|
37
73
|
}
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
75
|
+
saveWithRetry(retry: () => void): void {
|
|
76
|
+
this.toast.show({
|
|
77
|
+
message: 'Could not save',
|
|
78
|
+
kind: 'error',
|
|
79
|
+
duration: 0,
|
|
80
|
+
action: { label: 'Retry', onPress: retry },
|
|
81
|
+
});
|
|
43
82
|
}
|
|
44
83
|
}
|
|
45
84
|
```
|
|
46
85
|
|
|
47
86
|
```html
|
|
48
|
-
<!--
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
}
|
|
87
|
+
<!-- src/app/notifications.component.html -->
|
|
88
|
+
<button type="button" (click)="save()">Save</button>
|
|
89
|
+
<rv-toast-list [manager]="toast" position="bottom-end" />
|
|
52
90
|
```
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`show()` returns the stable id, or `null` under `'reject'` (or a full finite
|
|
66
|
-
queue). `dismiss/update/pause/resume/runAction` return `false` for unknown ids
|
|
67
|
-
(and repeated dismissal) instead of throwing. `duration: 0` = persistent.
|
|
68
|
-
`pause()` suspends only running timers (persistent toasts can never pause, so
|
|
69
|
-
hover-leave cannot dismiss them); `resume()` on anything unpaused is a no-op;
|
|
70
|
-
each toast exposes its `paused` flag for progress UI.
|
|
71
|
-
Timers start at queue **promotion**, never while queued. Per-toast options
|
|
72
|
-
always beat globals; a kind change re-resolves the per-kind default unless the
|
|
73
|
-
toast carries an explicit duration. Overflow eviction prefers timed,
|
|
74
|
-
actionless toasts — persistent errors with actions are last resort.
|
|
75
|
-
|
|
76
|
-
## Recipes (app-owned, deliberately not in the library)
|
|
92
|
+
The standalone list renders the manager's `active()` signal, pauses timed
|
|
93
|
+
notifications on hover and keyboard focus, runs actions, and provides a
|
|
94
|
+
dismiss button by default. Its defaults are `position="bottom-end"`,
|
|
95
|
+
`animation="slide"`, `richColors=false`, and `showCloseButton=true`.
|
|
96
|
+
`position` supports `top`/`bottom` × `start`/`center`/`end` (logical start/end
|
|
97
|
+
work in RTL); `animation` supports `slide`, `fade`, and `none`. Errors use an
|
|
98
|
+
assertive live region; other kinds use polite announcements.
|
|
99
|
+
|
|
100
|
+
For snapshot-driven UIs, provide the snapshots and handle the emitted IDs
|
|
101
|
+
yourself:
|
|
77
102
|
|
|
78
103
|
```ts
|
|
79
|
-
|
|
80
|
-
import {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
toast.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
// src/app/toast-snapshots.component.ts
|
|
105
|
+
import { Component } from '@angular/core';
|
|
106
|
+
import type { Toast } from '@rivinia/toast';
|
|
107
|
+
import { RiviniaToastList } from '@rivinia/toast/ui';
|
|
108
|
+
|
|
109
|
+
@Component({
|
|
110
|
+
selector: 'app-toast-snapshots',
|
|
111
|
+
standalone: true,
|
|
112
|
+
imports: [RiviniaToastList],
|
|
113
|
+
templateUrl: './toast-snapshots.component.html',
|
|
114
|
+
})
|
|
115
|
+
export class ToastSnapshotsComponent {
|
|
116
|
+
items: readonly Toast[] = [
|
|
117
|
+
{
|
|
118
|
+
id: 1,
|
|
119
|
+
kind: 'info',
|
|
120
|
+
message: 'A background task is ready',
|
|
121
|
+
duration: 0,
|
|
122
|
+
remaining: 0,
|
|
123
|
+
paused: false,
|
|
124
|
+
action: { label: 'Open', onPress: () => console.log('Open task') },
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
remove(id: number): void {
|
|
129
|
+
this.items = this.items.filter((item) => item.id !== id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
run(id: number): void {
|
|
133
|
+
const item = this.items.find((candidate) => candidate.id === id);
|
|
134
|
+
if (!item?.action) return;
|
|
135
|
+
item.action.onPress();
|
|
136
|
+
if (item.action.dismissAfterPress ?? true) this.remove(id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<!-- src/app/toast-snapshots.component.html -->
|
|
143
|
+
<rv-toast-list [toasts]="items" (dismissed)="remove($event)" (actionPressed)="run($event)" />
|
|
96
144
|
```
|
|
97
145
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
146
|
+
In this presentational mode your app owns timers, pause behavior, and action
|
|
147
|
+
handling. If both `manager` and `toasts` are supplied, the manager takes
|
|
148
|
+
precedence.
|
|
101
149
|
|
|
102
|
-
##
|
|
150
|
+
## Use the headless service
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
markers).
|
|
152
|
+
Core can be used without the UI. `active()` and `queue()` are readonly signals;
|
|
153
|
+
the active list is oldest-first. Timers begin only when a toast becomes active.
|
|
107
154
|
|
|
108
155
|
```ts
|
|
109
|
-
|
|
156
|
+
// src/app/save-status.component.ts
|
|
157
|
+
import { Component, inject } from '@angular/core';
|
|
158
|
+
import { resolveToastText, RiviniaToast } from '@rivinia/toast';
|
|
159
|
+
|
|
160
|
+
@Component({
|
|
161
|
+
selector: 'app-save-status',
|
|
162
|
+
standalone: true,
|
|
163
|
+
templateUrl: './save-status.component.html',
|
|
164
|
+
})
|
|
165
|
+
export class SaveStatusComponent {
|
|
166
|
+
readonly toast = inject(RiviniaToast);
|
|
167
|
+
readonly resolveText = resolveToastText;
|
|
168
|
+
|
|
169
|
+
save(): void {
|
|
170
|
+
const id: number | null = this.toast.show({ message: 'Saving…' });
|
|
171
|
+
if (id === null) return; // possible with overflow: 'reject'
|
|
172
|
+
|
|
173
|
+
// A matching key updates the existing active or queued item.
|
|
174
|
+
this.toast.show({
|
|
175
|
+
message: 'Saved',
|
|
176
|
+
kind: 'success',
|
|
177
|
+
dedupeKey: 'save',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
110
181
|
```
|
|
111
182
|
|
|
112
183
|
```html
|
|
113
|
-
<!--
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
184
|
+
<!-- src/app/save-status.component.html -->
|
|
185
|
+
<button type="button" (click)="save()">Save</button>
|
|
186
|
+
@for (item of toast.active(); track item.id) {
|
|
187
|
+
<p [attr.data-kind]="item.kind">{{ resolveText(item.message) }}</p>
|
|
188
|
+
}
|
|
118
189
|
```
|
|
119
190
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
(never swallowed). Dismissed rows play a 150 ms exit from their last
|
|
126
|
-
snapshot on the browser (SSR drops them — renders never hang). When both
|
|
127
|
-
`manager` and `toasts` are given, the manager wins: the UI never races Core.
|
|
191
|
+
`message` and action `label` accept either a string or a reactive string
|
|
192
|
+
signal, so they can reflect language changes. For a custom outlet, use the exported
|
|
193
|
+
`resolveToastText(message)` utility to read either shape.
|
|
194
|
+
|
|
195
|
+
## API and behavior
|
|
128
196
|
|
|
129
|
-
|
|
197
|
+
| Export | Kind | Purpose |
|
|
198
|
+
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `provideRiviniaToast(config?)` | Provider | Configure defaults; see `RiviniaToastConfig`. |
|
|
200
|
+
| `RiviniaToast` | Service | `show`, `dismiss`, `dismissAll`, `update`, `pause`, `resume`, `runAction`; `active` and `queue` signals. |
|
|
201
|
+
| `injectRiviniaToast()` | Function | Shorthand for `inject(RiviniaToast)`. |
|
|
202
|
+
| UI secondary entry point | `@rivinia/toast/ui` | Optional managed or presentational standalone list and its UI types. |
|
|
203
|
+
| `Toast`, `ToastKind`, `ToastMessage`, `ToastAction`, `ToastOptions`, `ToastChanges`, `ToastOverflowPolicy`, `RiviniaToastConfig` | Types | Public service, configuration, and UI input types. |
|
|
204
|
+
| `resolveToastText`, `clampDuration` | Utilities | Resolve signal/string text; normalize a duration. |
|
|
205
|
+
|
|
206
|
+
Important details:
|
|
207
|
+
|
|
208
|
+
- `show()` returns a stable `number` ID, or `null` when rejected by the
|
|
209
|
+
`reject` policy or when a finite queue is full. `dismiss()` and `update()`
|
|
210
|
+
return `false` for unknown or dismissed IDs; `pause()`/`resume()` also return
|
|
211
|
+
`false` when the timer cannot be changed.
|
|
212
|
+
- A repeated `dedupeKey` reuses the active or queued toast ID and updates it
|
|
213
|
+
rather than stacking another toast. A matching `show()` without an `action`
|
|
214
|
+
clears the previous action; include it again if it should remain.
|
|
215
|
+
- A toast-specific `duration` overrides per-kind and global defaults.
|
|
216
|
+
`duration: 0`, negative values, not-a-number, and infinities mean persistent (no
|
|
217
|
+
timer). Per-kind defaults are set with `durations`, for example
|
|
218
|
+
`{ error: 8000 }`.
|
|
219
|
+
- `pause()` preserves the remaining time on an active timed toast. Persistent
|
|
220
|
+
or queued toasts cannot be paused. `resume()` restarts the saved time; if it
|
|
221
|
+
has already elapsed, the toast is dismissed.
|
|
222
|
+
- With `overflow: 'queue'`, new toasts wait without timers and are promoted
|
|
223
|
+
when a visible slot opens. Set `maxQueue` to bound that queue. With
|
|
224
|
+
`overflow: 'reject'`, `show()` returns `null`. `dismiss-oldest` replaces an
|
|
225
|
+
active toast, preferring timed, actionless toasts for eviction.
|
|
226
|
+
- `update(id, changes)` restarts an active timer; a queued item's timer starts
|
|
227
|
+
only on promotion. Changing `kind` re-applies that kind's default duration
|
|
228
|
+
unless the toast was given an explicit duration. `action: null` removes an
|
|
229
|
+
action.
|
|
230
|
+
- `runAction(id)` runs the callback, then dismisses by default.
|
|
231
|
+
`dismissAfterPress: false` keeps the toast visible. Callback errors propagate
|
|
232
|
+
to the caller (and, in managed UI use, Angular's `ErrorHandler`).
|
|
233
|
+
- No routing policy is built in. To clear timed notifications on navigation,
|
|
234
|
+
subscribe to your router in app code and call `dismiss(id)` as appropriate.
|
|
235
|
+
|
|
236
|
+
## Testing app code
|
|
237
|
+
|
|
238
|
+
Use fake timers to make timer-dependent tests deterministic:
|
|
130
239
|
|
|
131
240
|
```ts
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
241
|
+
import { TestBed } from '@angular/core/testing';
|
|
242
|
+
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
243
|
+
import { provideRiviniaToast, RiviniaToast } from '@rivinia/toast';
|
|
244
|
+
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
TestBed.configureTestingModule({ providers: [provideRiviniaToast()] });
|
|
247
|
+
vi.useFakeTimers();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
TestBed.resetTestingModule();
|
|
252
|
+
vi.useRealTimers();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('auto-dismisses after the configured duration', () => {
|
|
256
|
+
const toast = TestBed.inject(RiviniaToast);
|
|
257
|
+
const id = toast.show({ message: 'Saved', duration: 1000 });
|
|
258
|
+
|
|
259
|
+
vi.advanceTimersByTime(1000);
|
|
260
|
+
expect(toast.active().some((item) => item.id === id)).toBe(false);
|
|
261
|
+
});
|
|
136
262
|
```
|
|
137
263
|
|
|
138
264
|
## License
|