@scania-nl/tegel-angular-extensions 0.0.1-0
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 +217 -0
- package/eslint.config.mjs +48 -0
- package/jest.config.ts +21 -0
- package/ng-package.json +7 -0
- package/package.json +20 -0
- package/project.json +36 -0
- package/src/index.ts +4 -0
- package/src/lib/toast/models/toast-state.enum.ts +19 -0
- package/src/lib/toast/models/toast-type.ts +9 -0
- package/src/lib/toast/models/toast.model.ts +87 -0
- package/src/lib/toast/provide-toast.ts +29 -0
- package/src/lib/toast/toast.component.html +41 -0
- package/src/lib/toast/toast.component.scss +118 -0
- package/src/lib/toast/toast.component.spec.ts +28 -0
- package/src/lib/toast/toast.component.ts +37 -0
- package/src/lib/toast/toast.config.ts +50 -0
- package/src/lib/toast/toast.service.ts +275 -0
- package/src/lib/utils/bootstrap-global-component.ts +73 -0
- package/src/test-setup.ts +6 -0
- package/tsconfig.json +28 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @scania-nl/tegel-angular-extensions
|
|
2
|
+
|
|
3
|
+
Angular services for working with the [Tegel Angular 17](https://www.npmjs.com/package/@scania/tegel-angular-17) component library.
|
|
4
|
+
Provides simple wrappers for toast and modal (TBC) functionality using Angular 19+ **standalone components** and **dependency injection configuration**.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
- ✅ Drop-in `ToastService` for displaying toasts
|
|
11
|
+
- ✅ Zero boilerplate — no Angular modules required
|
|
12
|
+
- ✅ Fully typed and configurable via DI
|
|
13
|
+
- ✅ Built for Angular 19+ standalone architecture
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @scania-nl/tegel-angular-extensions @scania/tegel-angular-17
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> Note: `@scania/tegel-angular-17` is a peer dependency and must be installed separately.
|
|
24
|
+
|
|
25
|
+
The following peer dependencies should be included automatically when creating an Angular 19+ project:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"@angular/common": "^19.0.0",
|
|
30
|
+
"@angular/core": "^19.0.0",
|
|
31
|
+
"@angular/router": "^19.0.0",
|
|
32
|
+
"rxjs": "~7.8.0"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
### 1. Add Providers
|
|
41
|
+
In your `app.config.ts`, specifiy the provider with `provideToast()`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { provideToast } from '@scania-nl/tegel-angular-extensions';
|
|
45
|
+
|
|
46
|
+
export const appConfig: ApplicationConfig = {
|
|
47
|
+
providers: [
|
|
48
|
+
provideToast({
|
|
49
|
+
type: 'information', // Default toast type
|
|
50
|
+
title: 'Notification', // Default title
|
|
51
|
+
description: '', // Default description
|
|
52
|
+
duration: 7500, // Auto-dismiss delay (ms)
|
|
53
|
+
closeDuration: 300, // Fade-out animation duration (ms)
|
|
54
|
+
closable: true, // Show a close button
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
> Note: The configuration is optional, all values shown above are the default settings.
|
|
60
|
+
|
|
61
|
+
### 2. Use in components
|
|
62
|
+
In any standalone component:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
@Component({
|
|
66
|
+
standalone: true,
|
|
67
|
+
selector: 'my-toast-demo',
|
|
68
|
+
template: `<button (click)="showToast()">Show Toast</button>`,
|
|
69
|
+
})
|
|
70
|
+
export class MyToastDemoComponent {
|
|
71
|
+
private readonly toastService = inject(ToastService);
|
|
72
|
+
|
|
73
|
+
showToast() {
|
|
74
|
+
this.toastService.create({
|
|
75
|
+
type: 'success',
|
|
76
|
+
title: 'Hello Toast',
|
|
77
|
+
description: 'Toast created successfully!'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ⚙️ Configuration Options
|
|
86
|
+
|
|
87
|
+
You can configure the default appearance and behavior of toasts by passing a `ToastConfig` object to `provideToast()` in your `app.config.ts`.
|
|
88
|
+
|
|
89
|
+
All options are optional. Defaults will be applied if values are not provided.
|
|
90
|
+
|
|
91
|
+
| Property | Type | Default | Description |
|
|
92
|
+
| --------------- | ---------------------------------------------------- | ---------------- | -------------------------------------------------------------------------- |
|
|
93
|
+
| `type` | `'information' \| 'success' \| 'warning' \| 'error'` | `'information'` | Default toast type for `create()` calls |
|
|
94
|
+
| `title` | `string` | `'Notification'` | Default title text for toasts |
|
|
95
|
+
| `description` | `string` | `''` | Default description text |
|
|
96
|
+
| `duration` | `number` | `7500` | Duration (ms) before a toast auto-closes (0 = stays until manually closed) |
|
|
97
|
+
| `closeDuration` | `number` | `300` | Duration (ms) for fade-out animation (0 = remove instantly) |
|
|
98
|
+
| `closable` | `boolean` | `true` | Whether a close button is shown |
|
|
99
|
+
|
|
100
|
+
> Note: You can override these defaults per toast when using `create()` or convenience methods like `success()`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 🧩 ToastService API
|
|
105
|
+
|
|
106
|
+
The `ToastService` provides a signal-based API to create, manage, and dismiss toast notifications in Angular standalone apps. It is automatically available after registering `provideToast()` in your `app.config.ts`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 📦 Properties
|
|
111
|
+
|
|
112
|
+
| Property | Type | Description |
|
|
113
|
+
| -------------- | ----------------- | ----------------------------------------------------- |
|
|
114
|
+
| `toasts` | `Signal<Toast[]>` | Read-only list of all toasts (including closed) |
|
|
115
|
+
| `activeToasts` | `Signal<Toast[]>` | List of currently active toasts (`Open` or `Closing`) |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### 🔧 Methods
|
|
120
|
+
|
|
121
|
+
#### `create(toastOptions: Partial<ToastOptions>): number`
|
|
122
|
+
|
|
123
|
+
Creates a custom toast with full control over appearance and behavior.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
toastService.create({
|
|
129
|
+
type: 'success',
|
|
130
|
+
title: 'Saved!',
|
|
131
|
+
description: 'Your changes have been saved.',
|
|
132
|
+
duration: 5000,
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Returns the unique toast ID.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
#### Convenience Methods
|
|
141
|
+
|
|
142
|
+
Creates a toast of a specific type:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
toastService.success({ title: 'All good!' });
|
|
146
|
+
toastService.error({ title: 'Oops!', description: 'Something went wrong.' });
|
|
147
|
+
toastService.warning({ title: 'Heads up!' });
|
|
148
|
+
toastService.info({ title: 'FYI' });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
#### `getToast(id: number): Toast | undefined`
|
|
154
|
+
|
|
155
|
+
Gets a toast by its ID.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
#### `createRandomToast(props?: Partial<ToastOptions>): number`
|
|
160
|
+
|
|
161
|
+
Creates a random toast with random type and title. Useful for testing. Returns the toast's unique ID.
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
toastService.createRandomToast();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
#### `close(id: number): void`
|
|
170
|
+
|
|
171
|
+
Triggers the fade-out animation and schedules removal.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
#### `closeAll(): void`
|
|
176
|
+
|
|
177
|
+
Closes all currently open toasts.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
#### `remove(id: number): void`
|
|
182
|
+
|
|
183
|
+
Immediately removes a toast (no animation).
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
#### `removeAll(): void`
|
|
188
|
+
|
|
189
|
+
Force-removes all toasts instantly (no animations).
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### 🔁 Toast Lifecycle Hooks
|
|
194
|
+
|
|
195
|
+
Each toast supports optional lifecycle callbacks:
|
|
196
|
+
|
|
197
|
+
| Callback | Description |
|
|
198
|
+
| ------------------ | -------------------------------------------- |
|
|
199
|
+
| `onCreated(toast)` | Called immediately after toast is created |
|
|
200
|
+
| `onClose(toast)` | Called when toast is closed (before removal) |
|
|
201
|
+
| `onRemoved(toast)` | Called when toast is fully removed |
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
toastService.success({
|
|
207
|
+
title: 'Logged out',
|
|
208
|
+
duration: 5000,
|
|
209
|
+
onRemoved: (toast) => console.log(`Toast ${toast.id} removed`)
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 📄 License
|
|
216
|
+
|
|
217
|
+
All CSS, HTML and JS code are available under the MIT license. The Scania brand identity, logos and photographs found in this repository are copyrighted Scania CV AB and are not available on an open source basis or to be used as examples or in any other way, if not specifically ordered by Scania CV AB.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import nx from '@nx/eslint-plugin';
|
|
2
|
+
import baseConfig from '../../eslint.base.config.mjs';
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
...baseConfig,
|
|
6
|
+
{
|
|
7
|
+
files: ['**/*.json'],
|
|
8
|
+
rules: {
|
|
9
|
+
'@nx/dependency-checks': [
|
|
10
|
+
'error',
|
|
11
|
+
{
|
|
12
|
+
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser: await import('jsonc-eslint-parser'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
...nx.configs['flat/angular'],
|
|
21
|
+
...nx.configs['flat/angular-template'],
|
|
22
|
+
{
|
|
23
|
+
files: ['**/*.ts'],
|
|
24
|
+
rules: {
|
|
25
|
+
'@angular-eslint/directive-selector': [
|
|
26
|
+
'error',
|
|
27
|
+
{
|
|
28
|
+
type: 'attribute',
|
|
29
|
+
prefix: 'lib',
|
|
30
|
+
style: 'camelCase',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
'@angular-eslint/component-selector': [
|
|
34
|
+
'error',
|
|
35
|
+
{
|
|
36
|
+
type: 'element',
|
|
37
|
+
prefix: 'tds-ext',
|
|
38
|
+
style: 'kebab-case',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
files: ['**/*.html'],
|
|
45
|
+
// Override or add rules here
|
|
46
|
+
rules: {},
|
|
47
|
+
},
|
|
48
|
+
];
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'tegel-angular-extensions',
|
|
3
|
+
preset: '../../jest.preset.js',
|
|
4
|
+
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
|
5
|
+
coverageDirectory: '../../coverage/libs/tegel-angular-extensions',
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.(ts|mjs|js|html)$': [
|
|
8
|
+
'jest-preset-angular',
|
|
9
|
+
{
|
|
10
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
11
|
+
stringifyContentPathRegex: '\\.(html|svg)$',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
|
16
|
+
snapshotSerializers: [
|
|
17
|
+
'jest-preset-angular/build/serializers/no-ng-attributes',
|
|
18
|
+
'jest-preset-angular/build/serializers/ng-snapshot',
|
|
19
|
+
'jest-preset-angular/build/serializers/html-comment',
|
|
20
|
+
],
|
|
21
|
+
};
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scania-nl/tegel-angular-extensions",
|
|
3
|
+
"version": "0.0.1-0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@angular/common": "^19.0.0",
|
|
10
|
+
"@angular/core": "^19.0.0",
|
|
11
|
+
"@angular/router": "^19.0.0",
|
|
12
|
+
"@scania/tegel-angular-17": "^1.0.0",
|
|
13
|
+
"rxjs": "~7.8.0"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Patrick Groot Koerkamp",
|
|
18
|
+
"email": "patrick.groot.koerkamp@scania.com"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tegel-angular-extensions",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/tegel-angular-extensions/src",
|
|
5
|
+
"prefix": "lib",
|
|
6
|
+
"projectType": "library",
|
|
7
|
+
"tags": [],
|
|
8
|
+
"targets": {
|
|
9
|
+
"build": {
|
|
10
|
+
"executor": "@nx/angular:ng-packagr-lite",
|
|
11
|
+
"outputs": ["{workspaceRoot}/dist/{projectRoot}"],
|
|
12
|
+
"options": {
|
|
13
|
+
"project": "libs/tegel-angular-extensions/ng-package.json"
|
|
14
|
+
},
|
|
15
|
+
"configurations": {
|
|
16
|
+
"production": {
|
|
17
|
+
"tsConfig": "libs/tegel-angular-extensions/tsconfig.lib.prod.json"
|
|
18
|
+
},
|
|
19
|
+
"development": {
|
|
20
|
+
"tsConfig": "libs/tegel-angular-extensions/tsconfig.lib.json"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"defaultConfiguration": "production"
|
|
24
|
+
},
|
|
25
|
+
"test": {
|
|
26
|
+
"executor": "@nx/jest:jest",
|
|
27
|
+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
28
|
+
"options": {
|
|
29
|
+
"jestConfig": "libs/tegel-angular-extensions/jest.config.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"lint": {
|
|
33
|
+
"executor": "@nx/eslint:lint"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the current lifecycle state of a toast.
|
|
3
|
+
*/
|
|
4
|
+
export enum ToastState {
|
|
5
|
+
/**
|
|
6
|
+
* The toast is fully visible and active.
|
|
7
|
+
*/
|
|
8
|
+
Open = 'open',
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The toast is transitioning out (e.g., fading out).
|
|
12
|
+
*/
|
|
13
|
+
Closing = 'closing',
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The toast is fully removed or dismissed.
|
|
17
|
+
*/
|
|
18
|
+
Closed = 'closed',
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List of available toast types (inherited from Tegel)
|
|
3
|
+
*/
|
|
4
|
+
export const TOAST_TYPES = ['success', 'information', 'warning', 'error'] as const;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type representing valid toast type values.
|
|
8
|
+
*/
|
|
9
|
+
export type ToastType = (typeof TOAST_TYPES)[number];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ToastState } from './toast-state.enum';
|
|
2
|
+
import { ToastType } from './toast-type';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines the base structure for a toast message.
|
|
6
|
+
*/
|
|
7
|
+
export interface ToastOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The visual style of the toast (e.g., success, error).
|
|
10
|
+
*/
|
|
11
|
+
type: ToastType;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The main title text displayed in the toast.
|
|
15
|
+
*/
|
|
16
|
+
title: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Optional description text providing additional context.
|
|
20
|
+
*/
|
|
21
|
+
description?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Duration in milliseconds before the toast auto-dismisses.
|
|
25
|
+
* Use `0` for persistent toasts.
|
|
26
|
+
*/
|
|
27
|
+
duration: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Duration in milliseconds for the fade-out transition.
|
|
31
|
+
*/
|
|
32
|
+
closeDuration: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether the toast can be manually closed by the user.
|
|
36
|
+
*/
|
|
37
|
+
closable?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Optional router link to navigate to when the toast is clicked.
|
|
41
|
+
*/
|
|
42
|
+
link?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Custom link text shown if a link is provided.
|
|
46
|
+
*/
|
|
47
|
+
linkText?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional callback triggered when the linkText is clicked.
|
|
51
|
+
* Used as an alternative to `link` for invoking custom behavior.
|
|
52
|
+
*/
|
|
53
|
+
action?: () => void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional callback triggered when the toast is created.
|
|
57
|
+
* @param toast The created toast instance.
|
|
58
|
+
*/
|
|
59
|
+
onCreated?: (toast: Toast) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional callback triggered when the toast begins to close.
|
|
63
|
+
* @param toast The toast that is closing.
|
|
64
|
+
*/
|
|
65
|
+
onClose?: (toast: Toast) => void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Optional callback triggered when the toast has been fully removed.
|
|
69
|
+
* @param toast The removed toast instance.
|
|
70
|
+
*/
|
|
71
|
+
onRemoved?: (toast: Toast) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Represents a fully instantiated toast, including unique ID and current state.
|
|
76
|
+
*/
|
|
77
|
+
export interface Toast extends ToastOptions {
|
|
78
|
+
/**
|
|
79
|
+
* Unique identifier for the toast instance.
|
|
80
|
+
*/
|
|
81
|
+
id: number;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The current state of the toast (open, closing, or closed).
|
|
85
|
+
*/
|
|
86
|
+
state: ToastState;
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EnvironmentProviders,
|
|
3
|
+
makeEnvironmentProviders,
|
|
4
|
+
provideAppInitializer,
|
|
5
|
+
} from '@angular/core';
|
|
6
|
+
import { bootstrapGlobalComponent } from '../utils/bootstrap-global-component';
|
|
7
|
+
import { ToastComponent } from './toast.component';
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_TOAST_CONFIG,
|
|
10
|
+
TOAST_CONFIG,
|
|
11
|
+
ToastConfig,
|
|
12
|
+
} from './toast.config';
|
|
13
|
+
|
|
14
|
+
export function provideToast(
|
|
15
|
+
config: Partial<ToastConfig> = {}
|
|
16
|
+
): EnvironmentProviders {
|
|
17
|
+
return makeEnvironmentProviders([
|
|
18
|
+
{
|
|
19
|
+
provide: TOAST_CONFIG,
|
|
20
|
+
useFactory: () => ({
|
|
21
|
+
...DEFAULT_TOAST_CONFIG,
|
|
22
|
+
...(config ?? {}),
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
provideAppInitializer(() => {
|
|
26
|
+
bootstrapGlobalComponent(ToastComponent, { reuseIfExists: true });
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<ul class="toast-list" aria-live="polite" aria-atomic="true">
|
|
2
|
+
@for (toast of toastsSignal(); track toast.id) {
|
|
3
|
+
<li
|
|
4
|
+
class="toast-item"
|
|
5
|
+
[class]="'toast-' + toast.type"
|
|
6
|
+
[class.toast-closing]="toast.state === 'closing'"
|
|
7
|
+
[style.--duration.ms]="toast.duration"
|
|
8
|
+
[style.--close-duration.ms]="toast.closeDuration"
|
|
9
|
+
role="status"
|
|
10
|
+
>
|
|
11
|
+
<tds-toast
|
|
12
|
+
[variant]="toast.type"
|
|
13
|
+
[header]="toast.title"
|
|
14
|
+
[subheader]="toast.description"
|
|
15
|
+
[closable]="toast.closable"
|
|
16
|
+
tds-close-aria-label="Toast close button"
|
|
17
|
+
>
|
|
18
|
+
@if (toast.link) {
|
|
19
|
+
<tds-link slot="actions">
|
|
20
|
+
<a [routerLink]="toast.link">
|
|
21
|
+
{{ toast.linkText ?? 'Click here' }}
|
|
22
|
+
</a>
|
|
23
|
+
</tds-link>
|
|
24
|
+
} @else if (toast.action) {
|
|
25
|
+
<tds-link slot="actions">
|
|
26
|
+
<a href="#" (click)="toast.action()">
|
|
27
|
+
{{ toast.linkText ?? 'Click here' }}
|
|
28
|
+
</a>
|
|
29
|
+
</tds-link>
|
|
30
|
+
}
|
|
31
|
+
</tds-toast>
|
|
32
|
+
@if (toast.closable) {
|
|
33
|
+
<button
|
|
34
|
+
class="toast-close"
|
|
35
|
+
(click)="closeToast(toast)"
|
|
36
|
+
aria-label="Close toast"
|
|
37
|
+
></button>
|
|
38
|
+
}
|
|
39
|
+
</li>
|
|
40
|
+
}
|
|
41
|
+
</ul>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
$default-close-duration: 300ms;
|
|
2
|
+
$default-duration: 7000ms;
|
|
3
|
+
$item-gap: 6px;
|
|
4
|
+
|
|
5
|
+
:host {
|
|
6
|
+
position: fixed;
|
|
7
|
+
right: 0;
|
|
8
|
+
bottom: 0;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
z-index: 9999;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.toast-list {
|
|
14
|
+
display: grid;
|
|
15
|
+
grid-template-columns: 1fr;
|
|
16
|
+
gap: $item-gap;
|
|
17
|
+
/* List resets */
|
|
18
|
+
list-style: none;
|
|
19
|
+
padding: $item-gap;
|
|
20
|
+
margin: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.toast-item {
|
|
24
|
+
position: relative;
|
|
25
|
+
animation: fadeIn $default-close-duration ease-in forwards;
|
|
26
|
+
|
|
27
|
+
&.toast-closing {
|
|
28
|
+
animation-name: fadeOut;
|
|
29
|
+
animation-duration: var(--close-duration, $default-close-duration);
|
|
30
|
+
animation-fill-mode: forwards;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&::before {
|
|
35
|
+
content: '';
|
|
36
|
+
position: absolute;
|
|
37
|
+
height: 3px;
|
|
38
|
+
width: 100%;
|
|
39
|
+
bottom: 0;
|
|
40
|
+
left: 4px; /* Toast border itself */
|
|
41
|
+
right: 0;
|
|
42
|
+
|
|
43
|
+
animation: progress var(--duration, $default-duration)
|
|
44
|
+
linear forwards;
|
|
45
|
+
|
|
46
|
+
border-top-right-radius: 4px;
|
|
47
|
+
border-bottom-right-radius: 4px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&.toast-information::before {
|
|
51
|
+
background: var(--tds-information);
|
|
52
|
+
}
|
|
53
|
+
&.toast-success::before {
|
|
54
|
+
background: var(--tds-positive);
|
|
55
|
+
}
|
|
56
|
+
&.toast-warning::before {
|
|
57
|
+
background: var(--tds-warning);
|
|
58
|
+
}
|
|
59
|
+
&.toast-error::before {
|
|
60
|
+
background: var(--tds-negative);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
Custom Close element for TDS, which is just a transparent block covering the close-button of the TdsToast element
|
|
65
|
+
|
|
66
|
+
Height, width, and positioning values are equal to TdsToast's button.close element for a perfect overlap
|
|
67
|
+
*/
|
|
68
|
+
.toast-close {
|
|
69
|
+
height: 20px;
|
|
70
|
+
width: 20px;
|
|
71
|
+
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
position: absolute;
|
|
75
|
+
top: 14px;
|
|
76
|
+
right: 14px;
|
|
77
|
+
|
|
78
|
+
border: 0;
|
|
79
|
+
background: transparent;
|
|
80
|
+
|
|
81
|
+
&:active {
|
|
82
|
+
border: 2px solid var(--tds-blue-400);
|
|
83
|
+
outline-offset: -2px;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@keyframes fadeIn {
|
|
89
|
+
0% {
|
|
90
|
+
transform: translateY(40%);
|
|
91
|
+
opacity: 0;
|
|
92
|
+
margin-bottom: -25%;
|
|
93
|
+
}
|
|
94
|
+
100% {
|
|
95
|
+
transform: translateY(0);
|
|
96
|
+
opacity: 1;
|
|
97
|
+
margin-bottom: 0%;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@keyframes fadeOut {
|
|
102
|
+
0% {
|
|
103
|
+
transform: translateY(0);
|
|
104
|
+
opacity: 1;
|
|
105
|
+
margin-top: 0;
|
|
106
|
+
}
|
|
107
|
+
100% {
|
|
108
|
+
opacity: 0;
|
|
109
|
+
transform: translateY(75%);
|
|
110
|
+
margin-top: -25%;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@keyframes progress {
|
|
115
|
+
100% {
|
|
116
|
+
width: 0%;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideToast } from './provide-toast';
|
|
3
|
+
import { ToastComponent } from './toast.component';
|
|
4
|
+
|
|
5
|
+
describe('ToastComponent', () => {
|
|
6
|
+
let component: ToastComponent;
|
|
7
|
+
let fixture: ComponentFixture<ToastComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
declarations: [],
|
|
12
|
+
imports: [ToastComponent],
|
|
13
|
+
providers: [
|
|
14
|
+
provideToast()
|
|
15
|
+
]
|
|
16
|
+
}).compileComponents();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
fixture = TestBed.createComponent(ToastComponent);
|
|
21
|
+
component = fixture.componentInstance;
|
|
22
|
+
fixture.detectChanges();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should create', () => {
|
|
26
|
+
expect(component).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
3
|
+
import { RouterLink } from '@angular/router';
|
|
4
|
+
import { TegelModule } from '@scania/tegel-angular-17';
|
|
5
|
+
|
|
6
|
+
import { Toast } from './models/toast.model';
|
|
7
|
+
import { ToastService } from './toast.service';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Displays toast notifications provided by the ToastService.
|
|
11
|
+
*
|
|
12
|
+
* Toasts are non-blocking messages that automatically disappear after a set duration
|
|
13
|
+
* or can be dismissed manually by the user.
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'tds-ext-toast',
|
|
17
|
+
templateUrl: './toast.component.html',
|
|
18
|
+
styleUrls: ['./toast.component.scss'],
|
|
19
|
+
imports: [CommonModule, TegelModule, RouterLink],
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class ToastComponent {
|
|
23
|
+
private readonly toastService = inject(ToastService);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A reactive signal of all currently active toasts (open or closing).
|
|
27
|
+
*/
|
|
28
|
+
readonly toastsSignal = this.toastService.activeToasts;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initiates the closing process for the given toast.
|
|
32
|
+
*
|
|
33
|
+
* @param toast The toast to be closed.
|
|
34
|
+
* @returns void
|
|
35
|
+
*/
|
|
36
|
+
closeToast = (toast: Toast) => this.toastService.close(toast.id);
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import { ToastType } from './models/toast-type';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global default configuration for toast behavior.
|
|
6
|
+
* Used when individual toast properties are not explicitly set.
|
|
7
|
+
*/
|
|
8
|
+
export interface ToastConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Default toast type (e.g., 'success', 'information', etc.).
|
|
11
|
+
*/
|
|
12
|
+
type: ToastType;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default title to use when none is provided.
|
|
16
|
+
*/
|
|
17
|
+
title: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default description to use when none is provided.
|
|
21
|
+
*/
|
|
22
|
+
description: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default duration (ms) before auto-dismiss.
|
|
26
|
+
* Use `0` for persistent toasts.
|
|
27
|
+
*/
|
|
28
|
+
duration: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default duration (ms) for the fade-out animation.
|
|
32
|
+
*/
|
|
33
|
+
closeDuration: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether toasts are closable by default.
|
|
37
|
+
*/
|
|
38
|
+
closable: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const TOAST_CONFIG = new InjectionToken<ToastConfig>('ToastConfig');
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_TOAST_CONFIG: Required<ToastConfig> = {
|
|
44
|
+
type: 'information',
|
|
45
|
+
title: 'Notification',
|
|
46
|
+
description: '',
|
|
47
|
+
duration: 7500,
|
|
48
|
+
closeDuration: 300,
|
|
49
|
+
closable: true,
|
|
50
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { computed, inject, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { delay, filter, mergeMap, of, Subject, tap } from 'rxjs';
|
|
4
|
+
import { ToastState } from './models/toast-state.enum';
|
|
5
|
+
import { TOAST_TYPES } from './models/toast-type';
|
|
6
|
+
import { Toast, ToastOptions } from './models/toast.model';
|
|
7
|
+
import { TOAST_CONFIG, ToastConfig } from './toast.config';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Service for creating, managing, and removing toast notifications.
|
|
11
|
+
* Supports automatic dismissal, manual control, and lifecycle hooks.
|
|
12
|
+
*/
|
|
13
|
+
@Injectable({
|
|
14
|
+
providedIn: 'root',
|
|
15
|
+
})
|
|
16
|
+
export class ToastService {
|
|
17
|
+
private readonly config = inject<ToastConfig>(TOAST_CONFIG);
|
|
18
|
+
|
|
19
|
+
//*------------------------------------------------------------
|
|
20
|
+
//* Section: Internal variables
|
|
21
|
+
//*------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Internal ID tracker for unique toast IDs */
|
|
24
|
+
private id = 0;
|
|
25
|
+
|
|
26
|
+
/** Signal state holding all toasts */
|
|
27
|
+
private readonly _toasts = signal<Toast[]>([]);
|
|
28
|
+
|
|
29
|
+
/** Public signal of all toasts */
|
|
30
|
+
readonly toasts = this._toasts.asReadonly();
|
|
31
|
+
|
|
32
|
+
/** Signal of toasts that are not yet closed (open or closing) */
|
|
33
|
+
readonly activeToasts = computed(() =>
|
|
34
|
+
this._toasts().filter((toast) => toast.state !== ToastState.Closed)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** Internal stream for auto-closing toasts */
|
|
38
|
+
private readonly autoCloseSubject = new Subject<Toast>();
|
|
39
|
+
|
|
40
|
+
/** Internal stream for fade-out/removal of toasts */
|
|
41
|
+
private readonly closeSubject = new Subject<Toast>();
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
// Auto-close after toast.duration
|
|
45
|
+
this.autoCloseSubject
|
|
46
|
+
.pipe(
|
|
47
|
+
takeUntilDestroyed(),
|
|
48
|
+
filter((toast) => toast.duration > 0),
|
|
49
|
+
mergeMap((toast: Toast) =>
|
|
50
|
+
of(toast).pipe(
|
|
51
|
+
delay(toast.duration),
|
|
52
|
+
filter(() => this.shouldAutoClose(toast.id)),
|
|
53
|
+
tap(() => this.close(toast.id))
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
.subscribe();
|
|
58
|
+
|
|
59
|
+
// Remove after toast.closeDuration (fade-out)
|
|
60
|
+
this.closeSubject
|
|
61
|
+
.pipe(
|
|
62
|
+
takeUntilDestroyed(),
|
|
63
|
+
mergeMap((toast: Toast) =>
|
|
64
|
+
of(toast).pipe(
|
|
65
|
+
delay(toast.closeDuration),
|
|
66
|
+
filter(() => this.shouldRemove(toast.id)),
|
|
67
|
+
tap(() => this.remove(toast.id))
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
.subscribe();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//*------------------------------------------------------------
|
|
75
|
+
//* Section: Public methods
|
|
76
|
+
//*------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates and adds a new toast.
|
|
80
|
+
* @param toastOptions Partial toast definition.
|
|
81
|
+
* @returns The toast's unique ID.
|
|
82
|
+
*/
|
|
83
|
+
create(toastOptions: Partial<ToastOptions>): number {
|
|
84
|
+
const id = this.createId();
|
|
85
|
+
const toast: Toast = {
|
|
86
|
+
...this.config,
|
|
87
|
+
...toastOptions,
|
|
88
|
+
id,
|
|
89
|
+
state: ToastState.Open,
|
|
90
|
+
duration: this.normalizeDuration(
|
|
91
|
+
toastOptions.duration,
|
|
92
|
+
this.config.duration
|
|
93
|
+
),
|
|
94
|
+
closeDuration: this.normalizeDuration(
|
|
95
|
+
toastOptions.closeDuration,
|
|
96
|
+
this.config.closeDuration
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.addToast(toast);
|
|
101
|
+
toast.onCreated?.(toast);
|
|
102
|
+
|
|
103
|
+
// Schedule auto-close if duration > 0
|
|
104
|
+
if (toast.duration > 0) {
|
|
105
|
+
this.autoCloseSubject.next(toast);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Initiates the fade-out transition for a toast.
|
|
113
|
+
* @param id The toast ID.
|
|
114
|
+
*/
|
|
115
|
+
close(id: number): void {
|
|
116
|
+
let toast = this.getToast(id);
|
|
117
|
+
if (!toast || toast.state !== ToastState.Open) return;
|
|
118
|
+
|
|
119
|
+
toast = this.updateToastState(toast.id, ToastState.Closing);
|
|
120
|
+
toast?.onClose?.(toast);
|
|
121
|
+
|
|
122
|
+
// Schedule removal after close duration
|
|
123
|
+
if (toast) this.closeSubject.next(toast);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Immediately marks a toast as closed and removes it from display.
|
|
128
|
+
* @param id The toast ID.
|
|
129
|
+
*/
|
|
130
|
+
remove(id: number): void {
|
|
131
|
+
const toast = this.updateToastState(id, ToastState.Closed);
|
|
132
|
+
toast?.onRemoved?.(toast);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Closes and removes all toasts immediately without fade-out.
|
|
137
|
+
*/
|
|
138
|
+
removeAll(): void {
|
|
139
|
+
const closedToasts: Toast[] = [];
|
|
140
|
+
|
|
141
|
+
this._toasts.update((toasts) =>
|
|
142
|
+
toasts.map((toast) => {
|
|
143
|
+
if (toast.state !== ToastState.Closed) {
|
|
144
|
+
const updated = { ...toast, state: ToastState.Closed };
|
|
145
|
+
closedToasts.push(updated);
|
|
146
|
+
return updated;
|
|
147
|
+
}
|
|
148
|
+
return toast;
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
closedToasts.forEach((toast) => toast.onRemoved?.(toast));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Initiates closing process for all open toasts.
|
|
157
|
+
*/
|
|
158
|
+
closeAll(): void {
|
|
159
|
+
const openToasts = this.toasts().filter(
|
|
160
|
+
(toast) => toast.state === ToastState.Open
|
|
161
|
+
);
|
|
162
|
+
openToasts.forEach((toast) => this.close(toast.id));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gets a toast by ID.
|
|
167
|
+
* @param id The toast ID.
|
|
168
|
+
* @returns The toast instance or undefined.
|
|
169
|
+
*/
|
|
170
|
+
getToast(id: number): Toast | undefined {
|
|
171
|
+
return this._toasts().find((toast) => toast.id === id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//*------------------------------------------------------------
|
|
175
|
+
//* Section: Internal methods
|
|
176
|
+
//*------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/** Whether the toast is eligible for auto-closing */
|
|
179
|
+
private shouldAutoClose(id: number): boolean {
|
|
180
|
+
const currentToast = this.getToast(id);
|
|
181
|
+
return currentToast?.state === ToastState.Open;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether the toast is eligible for final removal */
|
|
185
|
+
private shouldRemove(id: number): boolean {
|
|
186
|
+
const currentToast = this.getToast(id);
|
|
187
|
+
return currentToast?.state === ToastState.Closing;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Add toast to signal list */
|
|
191
|
+
private addToast(toast: Toast): void {
|
|
192
|
+
this._toasts.update((prev) => [...prev, toast]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Updates the state of a toast.
|
|
197
|
+
* @param id Toast ID
|
|
198
|
+
* @param state New state
|
|
199
|
+
* @returns The updated toast or undefined
|
|
200
|
+
*/
|
|
201
|
+
private updateToastState(id: number, state: ToastState): Toast | undefined {
|
|
202
|
+
let updatedToast: Toast | undefined;
|
|
203
|
+
|
|
204
|
+
this._toasts.update((prev) =>
|
|
205
|
+
prev.map((toast) => {
|
|
206
|
+
if (toast.id === id) {
|
|
207
|
+
updatedToast = { ...toast, state };
|
|
208
|
+
return updatedToast;
|
|
209
|
+
}
|
|
210
|
+
return toast;
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return updatedToast;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Creates a unique id
|
|
219
|
+
* @returns New id
|
|
220
|
+
*/
|
|
221
|
+
private createId(): number {
|
|
222
|
+
return ++this.id;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//*------------------------------------------------------------
|
|
226
|
+
//* Section: Public convenience methods
|
|
227
|
+
//*------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Creates a success toast.
|
|
231
|
+
* @param props Toast props without type.
|
|
232
|
+
*/
|
|
233
|
+
readonly success = (props: Partial<Omit<ToastOptions, 'type'>> = {}) =>
|
|
234
|
+
this.create({ ...props, type: 'success' });
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates an error toast.
|
|
238
|
+
* @param props Toast props without type.
|
|
239
|
+
*/
|
|
240
|
+
readonly error = (props: Partial<Omit<ToastOptions, 'type'>> = {}) =>
|
|
241
|
+
this.create({ ...props, type: 'error' });
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Creates a warning toast.
|
|
245
|
+
* @param props Toast props without type.
|
|
246
|
+
*/
|
|
247
|
+
readonly warning = (props: Partial<Omit<ToastOptions, 'type'>> = {}) =>
|
|
248
|
+
this.create({ ...props, type: 'warning' });
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Creates an informational toast.
|
|
252
|
+
* @param props Toast props without type.
|
|
253
|
+
*/
|
|
254
|
+
readonly info = (props: Partial<Omit<ToastOptions, 'type'>> = {}) =>
|
|
255
|
+
this.create({ ...props, type: 'information' });
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates a random toast for testing/demo purposes.
|
|
259
|
+
* @param props Optional overrides
|
|
260
|
+
*/
|
|
261
|
+
readonly createRandomToast = (props: Partial<ToastOptions> = {}) =>
|
|
262
|
+
this.create({
|
|
263
|
+
type: TOAST_TYPES[~~(Math.random() * 4)],
|
|
264
|
+
title: `Random Toast ${Math.random().toString(36).substring(7)}`,
|
|
265
|
+
...props,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
//*------------------------------------------------------------
|
|
269
|
+
//* Section: Helper methods
|
|
270
|
+
//*------------------------------------------------------------
|
|
271
|
+
private normalizeDuration(value: unknown, fallback: number): number {
|
|
272
|
+
const num = typeof value === 'number' ? value : fallback;
|
|
273
|
+
return !Number.isFinite(num) || num < 0 ? 0 : num;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplicationRef,
|
|
3
|
+
ComponentRef,
|
|
4
|
+
EmbeddedViewRef,
|
|
5
|
+
EnvironmentInjector,
|
|
6
|
+
Type,
|
|
7
|
+
createComponent,
|
|
8
|
+
inject,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal map to track which global components have been bootstrapped.
|
|
13
|
+
*/
|
|
14
|
+
const bootstrappedComponents = new WeakMap<
|
|
15
|
+
Type<unknown>,
|
|
16
|
+
ComponentRef<unknown>
|
|
17
|
+
>();
|
|
18
|
+
|
|
19
|
+
interface BootstrapGlobalComponentOptions {
|
|
20
|
+
/**
|
|
21
|
+
* If true, avoids re-creating the component if it's already mounted.
|
|
22
|
+
*/
|
|
23
|
+
reuseIfExists?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bootstraps a global Angular component directly into the <body> element.
|
|
28
|
+
* Useful for toasts, modals, and other global overlays.
|
|
29
|
+
*
|
|
30
|
+
* @param component - The component class to bootstrap.
|
|
31
|
+
* @param options - Optional settings like preventing duplicates.
|
|
32
|
+
* @returns The created ComponentRef.
|
|
33
|
+
*/
|
|
34
|
+
export function bootstrapGlobalComponent<T>(
|
|
35
|
+
component: Type<T>,
|
|
36
|
+
options?: BootstrapGlobalComponentOptions
|
|
37
|
+
): ComponentRef<T> {
|
|
38
|
+
const appRef = inject(ApplicationRef);
|
|
39
|
+
const injector = inject(EnvironmentInjector);
|
|
40
|
+
|
|
41
|
+
const existing = bootstrappedComponents.get(component);
|
|
42
|
+
if (existing && options?.reuseIfExists) {
|
|
43
|
+
return existing as ComponentRef<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cmpRef = createComponent(component, {
|
|
47
|
+
environmentInjector: injector,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
appRef.attachView(cmpRef.hostView);
|
|
51
|
+
|
|
52
|
+
const viewRef = cmpRef.hostView as EmbeddedViewRef<unknown>;
|
|
53
|
+
const element = viewRef.rootNodes[0] as HTMLElement;
|
|
54
|
+
|
|
55
|
+
document.body.appendChild(element);
|
|
56
|
+
|
|
57
|
+
bootstrappedComponents.set(component, cmpRef);
|
|
58
|
+
|
|
59
|
+
return cmpRef;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Destroys a previously bootstrapped global component.
|
|
64
|
+
*
|
|
65
|
+
* @param component - The component class to remove from DOM.
|
|
66
|
+
*/
|
|
67
|
+
export function destroyGlobalComponent<T>(component: Type<T>): void {
|
|
68
|
+
const cmpRef = bootstrappedComponents.get(component);
|
|
69
|
+
if (cmpRef) {
|
|
70
|
+
cmpRef.destroy();
|
|
71
|
+
bootstrappedComponents.delete(component);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"forceConsistentCasingInFileNames": true,
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noImplicitOverride": true,
|
|
7
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
8
|
+
"noImplicitReturns": true,
|
|
9
|
+
"noFallthroughCasesInSwitch": true
|
|
10
|
+
},
|
|
11
|
+
"files": [],
|
|
12
|
+
"include": [],
|
|
13
|
+
"references": [
|
|
14
|
+
{
|
|
15
|
+
"path": "./tsconfig.lib.json"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "./tsconfig.spec.json"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"extends": "../../tsconfig.base.json",
|
|
22
|
+
"angularCompilerOptions": {
|
|
23
|
+
"enableI18nLegacyMessageIdFormat": false,
|
|
24
|
+
"strictInjectionParameters": true,
|
|
25
|
+
"strictInputAccessModifiers": true,
|
|
26
|
+
"strictTemplates": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"inlineSources": true,
|
|
8
|
+
"types": []
|
|
9
|
+
},
|
|
10
|
+
"exclude": [
|
|
11
|
+
"src/**/*.spec.ts",
|
|
12
|
+
"src/test-setup.ts",
|
|
13
|
+
"jest.config.ts",
|
|
14
|
+
"src/**/*.test.ts"
|
|
15
|
+
],
|
|
16
|
+
"include": ["src/**/*.ts"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"target": "es2016",
|
|
7
|
+
"types": ["jest", "node"]
|
|
8
|
+
},
|
|
9
|
+
"files": ["src/test-setup.ts"],
|
|
10
|
+
"include": [
|
|
11
|
+
"jest.config.ts",
|
|
12
|
+
"src/**/*.test.ts",
|
|
13
|
+
"src/**/*.spec.ts",
|
|
14
|
+
"src/**/*.d.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|