├── .editorconfig
├── .eslintrc
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── MIGRATION-GUIDE.md
├── README.md
├── angular.json
├── codecov.yml
├── docs
└── angular-notifier-preview.gif
├── package-lock.json
├── package.json
├── projects
├── angular-notifier-demo
│ ├── src
│ │ ├── app
│ │ │ ├── app.component.html
│ │ │ ├── app.component.ts
│ │ │ └── app.module.ts
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── polyfills.ts
│ │ └── styles.scss
│ └── tsconfig.app.json
└── angular-notifier
│ ├── jest.config.json
│ ├── jest.setup.ts
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── animation-presets
│ │ │ ├── fade.animation-preset.spec.ts
│ │ │ ├── fade.animation-preset.ts
│ │ │ ├── slide.animation-preset.spec.ts
│ │ │ └── slide.animation-preset.ts
│ │ ├── components
│ │ │ ├── notifier-container.component.html
│ │ │ ├── notifier-container.component.spec.ts
│ │ │ ├── notifier-container.component.ts
│ │ │ ├── notifier-notification.component.html
│ │ │ ├── notifier-notification.component.spec.ts
│ │ │ └── notifier-notification.component.ts
│ │ ├── models
│ │ │ ├── notifier-action.model.ts
│ │ │ ├── notifier-animation.model.ts
│ │ │ ├── notifier-config.model.spec.ts
│ │ │ ├── notifier-config.model.ts
│ │ │ ├── notifier-notification.model.spec.ts
│ │ │ └── notifier-notification.model.ts
│ │ ├── notifier.module.spec.ts
│ │ ├── notifier.module.ts
│ │ ├── notifier.tokens.ts
│ │ └── services
│ │ │ ├── notifier-animation.service.spec.ts
│ │ │ ├── notifier-animation.service.ts
│ │ │ ├── notifier-queue.service.spec.ts
│ │ │ ├── notifier-queue.service.ts
│ │ │ ├── notifier-timer.service.spec.ts
│ │ │ ├── notifier-timer.service.ts
│ │ │ ├── notifier.service.spec.ts
│ │ │ └── notifier.service.ts
│ ├── styles.scss
│ └── styles
│ │ ├── core.scss
│ │ ├── themes
│ │ └── theme-material.scss
│ │ └── types
│ │ ├── type-default.scss
│ │ ├── type-error.scss
│ │ ├── type-info.scss
│ │ ├── type-success.scss
│ │ └── type-warning.scss
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
├── tools
└── update-package.js
├── tsconfig.json
└── tsconfig.spec.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = crlf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = 140
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | max_line_length = off
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "tsconfigRootDir": ".",
5 | "project": ["./tsconfig.json"]
6 | },
7 | "plugins": ["@typescript-eslint", "prettier", "simple-import-sort"],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:prettier/recommended"
13 | ],
14 | "rules": {
15 | "@typescript-eslint/no-explicit-any": "off",
16 | "import/order": "off",
17 | "simple-import-sort/exports": "error",
18 | "simple-import-sort/imports": "error",
19 | "sort-imports": "off"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [develop]
6 | pull_request:
7 | branches: [develop]
8 |
9 | jobs:
10 | build:
11 | name: Build
12 | runs-on: ubuntu-20.04
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v2
16 | - name: Setup NodeJS
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.20.x
20 | - name: Install dependencies
21 | run: npm ci
22 | - name: Build library
23 | run: npm run build:library
24 | - name: Build demo
25 | run: npm run build:demo
26 |
27 | lint:
28 | name: Lint
29 | needs: build
30 | runs-on: ubuntu-20.04
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | - name: Setup NodeJS
35 | uses: actions/setup-node@v2
36 | with:
37 | node-version: 16.20.x
38 | - name: Install dependencies
39 | run: npm ci
40 | - name: Lint library
41 | run: npm run lint:library
42 |
43 | test:
44 | name: Test
45 | needs: build
46 | runs-on: ubuntu-20.04
47 | steps:
48 | - name: Checkout repository
49 | uses: actions/checkout@v2
50 | - name: Setup NodeJS
51 | uses: actions/setup-node@v2
52 | with:
53 | node-version: 16.20.x
54 | - name: Install dependencies
55 | run: npm ci
56 | - name: Test library
57 | run: npm run test:library
58 | - name: Upload test coverage
59 | run: npm run test:library:upload-coverage
60 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish-to-npm:
9 | name: Publish to npm
10 | runs-on: ubuntu-20.04
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v2
14 | - name: Setup NodeJS
15 | uses: actions/setup-node@v2
16 | with:
17 | node-version: 16.20.x
18 | registry-url: 'https://registry.npmjs.org'
19 | - name: Install dependencies
20 | run: npm ci
21 | - name: Build library
22 | run: npm run build:library
23 | - name: Publish to npm
24 | run: npm publish ./dist/angular-notifier
25 | env:
26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.angular/cache
2 |
3 | # Created by https://www.toptal.com/developers/gitignore/api/node,vscode
4 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,vscode
5 |
6 | ### Node ###
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # Diagnostic reports (https://nodejs.org/api/report.html)
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage
29 | *.lcov
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (https://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 | jspm_packages/
49 |
50 | # TypeScript v1 declaration files
51 | typings/
52 |
53 | # TypeScript cache
54 | *.tsbuildinfo
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional eslint cache
60 | .eslintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # dotenv environment variables file
78 | .env
79 | .env.test
80 | .env*.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 |
89 | # Nuxt.js build / generate output
90 | .nuxt
91 | dist
92 |
93 | # Gatsby files
94 | .cache/
95 | # Comment in the public line in if your project uses Gatsby and not Next.js
96 | # https://nextjs.org/blog/next-9-1#public-directory-support
97 | # public
98 |
99 | # vuepress build output
100 | .vuepress/dist
101 |
102 | # Serverless directories
103 | .serverless/
104 |
105 | # FuseBox cache
106 | .fusebox/
107 |
108 | # DynamoDB Local files
109 | .dynamodb/
110 |
111 | # TernJS port file
112 | .tern-port
113 |
114 | # Stores VSCode versions used for testing VSCode extensions
115 | .vscode-test
116 |
117 | ### vscode ###
118 | .vscode/*
119 | !.vscode/settings.json
120 | !.vscode/tasks.json
121 | !.vscode/launch.json
122 | !.vscode/extensions.json
123 | *.code-workspace
124 |
125 | # End of https://www.toptal.com/developers/gitignore/api/node,vscode
126 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "endOfLine": "auto",
4 | "printWidth": 140,
5 | "singleQuote": true,
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Also see **[GitHub releases](https://github.com/dominique-mueller/angular-notifier/releases)**.
4 |
5 |
6 |
7 | ## [14.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/14.0.0) (2023-06-05)
8 |
9 | ### Features
10 |
11 | - Upgrade to Angular 16 ([#256](https://github.com/dominique-mueller/angular-notifier/pull/256))
12 |
13 | ### BREAKING CHANGES
14 |
15 | - The upgrade to Angular 16 breaks compatibility with Angular 15.
16 |
17 |
18 |
19 | ## [13.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/13.0.0) (2023-06-05)
20 |
21 | ### Features
22 |
23 | - Upgrade to Angular 15 ([#255](https://github.com/dominique-mueller/angular-notifier/pull/255))
24 |
25 | ### BREAKING CHANGES
26 |
27 | - The upgrade to Angular 15 breaks compatibility with Angular 14.
28 |
29 |
30 |
31 | ## [12.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/12.0.0) (2022-09-13)
32 |
33 | ### Features
34 |
35 | - Upgrade to Angular 14 ([#243](https://github.com/dominique-mueller/angular-notifier/pull/243))
36 |
37 | ### BREAKING CHANGES
38 |
39 | - The upgrade to Angular 14 breaks compatibility with Angular 13.
40 |
41 |
42 |
43 | ## [11.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/11.0.0) (2022-02-20)
44 |
45 | ### Features
46 |
47 | - Upgrade to Angular 13, enable partial-Ivy compilation ([#235](https://github.com/dominique-mueller/angular-notifier/pull/235))
48 |
49 | ### BREAKING CHANGES
50 |
51 | - The upgrade to Angular 13 breaks compatibility with Angular 12. The library is now published as partial-Ivy code.
52 |
53 |
54 |
55 | ## [10.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/10.0.0) (2022-02-20)
56 |
57 | ### Features
58 |
59 | - Upgrade to Angular 12 ([#232](https://github.com/dominique-mueller/angular-notifier/pull/232))
60 |
61 | ### BREAKING CHANGES
62 |
63 | - The upgrade to Angular 12 breaks compatibility with Angular 11.
64 |
65 |
66 |
67 | ## [9.1.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/9.1.0) (2021-07-13)
68 |
69 | ### Features
70 |
71 | - Expose `actionStream` on `NotifierService` ([#214](https://github.com/dominique-mueller/angular-notifier/pull/214))
72 |
73 |
74 |
75 | ## [9.0.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/9.0.1) (2021-03-12)
76 |
77 | ### Styles
78 |
79 | - Remove unnecessary white space around `notifier-container` ([#204](https://github.com/dominique-mueller/angular-notifier/pull/204))
80 |
81 |
82 |
83 | ## [9.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/9.0.0) (2021-03-12)
84 |
85 | ### Features
86 |
87 | - Upgrade to Angular 11 ([#203](https://github.com/dominique-mueller/angular-notifier/pull/203))
88 |
89 | ### BREAKING CHANGES
90 |
91 | - The upgrade to Angular 11 breaks compatibility with Angular 10.
92 |
93 |
94 |
95 | ## [8.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/8.0.0) (2021-03-11)
96 |
97 | ### Features
98 |
99 | - Upgrade to Angular 10 ([#202](https://github.com/dominique-mueller/angular-notifier/pull/202))
100 |
101 | ### BREAKING CHANGES
102 |
103 | - The upgrade to Angular 10 breaks compatibility with Angular 9.
104 |
105 |
106 |
107 | ## [7.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/7.0.0) (2021-03-11)
108 |
109 | ### Features
110 |
111 | - Upgrade to Angular 9 ([#201](https://github.com/dominique-mueller/angular-notifier/pull/201))
112 |
113 | ### BREAKING CHANGES
114 |
115 | - The upgrade to Angular 9 breaks compatibility with Angular 8.
116 |
117 |
118 |
119 | ## [6.0.2](https://github.com/dominique-mueller/angular-notifier/releases/tag/6.0.2) (2021-03-11)
120 |
121 | ### Bug Fixes
122 |
123 | - Fix "sideEffects" property in package.json causing issues when importing styles ([#186](https://github.com/dominique-mueller/angular-notifier/pull/186)), closes [#183](https://github.com/dominique-mueller/angular-notifier/issues/183)
124 |
125 | ### Styles
126 |
127 | - Fix layout falling apart when message becomes multi-line ([#200](https://github.com/dominique-mueller/angular-notifier/pull/200)), closes [#149](https://github.com/dominique-mueller/angular-notifier/issues/149)
128 |
129 |
130 |
131 | ## [6.0.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/6.0.1) (2019-10-20)
132 |
133 | ### Bug Fixes
134 |
135 | - **notifier-container:** Setup notifier-container as early as possible ([#144](https://github.com/dominique-mueller/angular-notifier/issues/144)) ([17b5953](https://github.com/dominique-mueller/angular-notifier/commit/17b5953)), closes [#119](https://github.com/dominique-mueller/angular-notifier/issues/119)
136 |
137 | ### Documentation
138 |
139 | - **README:** Add version information to README ([#143](https://github.com/dominique-mueller/angular-notifier/issues/143)) ([f838719](https://github.com/dominique-mueller/angular-notifier/commit/f838719))
140 |
141 |
142 |
143 | ## [6.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/6.0.0) (2019-10-19)
144 |
145 | ### Features
146 |
147 | - Upgrade to Angular 8 ([#139](https://github.com/dominique-mueller/angular-notifier/issues/139)) ([b355287](https://github.com/dominique-mueller/angular-notifier/commit/b355287))
148 |
149 | ### BREAKING CHANGES
150 |
151 | - The upgrade to Angular 8 breaks compatibility with Angular 7 (and previous versions).
152 |
153 |
154 |
155 | ## [5.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/5.0.0) (2019-10-19)
156 |
157 | ### Features
158 |
159 | - Upgrade to Angular 7 ([#134](https://github.com/dominique-mueller/angular-notifier/issues/134)) ([8f13440](https://github.com/dominique-mueller/angular-notifier/commit/8f13440))
160 |
161 | ### BREAKING CHANGES
162 |
163 | - The upgrade to Angular 7 breaks compatibility with Angular 6 (and previous versions).
164 |
165 |
166 |
167 | ## [4.1.2](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.1.2) (2019-10-18)
168 |
169 | ### Bug Fixes
170 |
171 | - **notifier:** Fix circular dependency issues of injection tokens ([#124](https://github.com/dominique-mueller/angular-notifier/issues/124)) ([139d43c](https://github.com/dominique-mueller/angular-notifier/commit/139d43c))
172 |
173 |
174 |
175 | ## [4.1.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.1.1) (2018-08-09)
176 |
177 | ### Bug Fixes
178 |
179 | - **package:** Fix artifact ([#99](https://github.com/dominique-mueller/angular-notifier/issues/99)) ([7ce901b](https://github.com/dominique-mueller/angular-notifier/commit/7ce901b))
180 |
181 |
182 |
183 | ## [4.1.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.1.0) (2018-08-08)
184 |
185 | ### Features
186 |
187 | - **notification:** Allow templateRef as notification content ([#95](https://github.com/dominique-mueller/angular-notifier/issues/95)) ([d705180](https://github.com/dominique-mueller/angular-notifier/commit/d705180))
188 |
189 | ### Documentation
190 |
191 | - **README:** Update demo to Stackblitz example ([#93](https://github.com/dominique-mueller/angular-notifier/issues/93)) ([1e26507](https://github.com/dominique-mueller/angular-notifier/commit/1e26507))
192 |
193 |
194 |
195 | ## [4.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.0.0) (2018-07-04)
196 |
197 | ### Features
198 |
199 | - Upgrade to Angular 6, fix breaking changes ([#83](https://github.com/dominique-mueller/angular-notifier/issues/83)) ([aae723d](https://github.com/dominique-mueller/angular-notifier/commit/aae723d)), closes [#82](https://github.com/dominique-mueller/angular-notifier/issues/82)
200 |
201 | ### BREAKING CHANGES
202 |
203 | - The upgrade to Angular 6 breaks compatibility with Angular 5.
204 |
205 |
206 |
207 | ## [3.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/3.0.0) (2018-01-18)
208 |
209 | ### Features
210 |
211 | - **angular:** Upgrade to Angular 5 ([#38](https://github.com/dominique-mueller/angular-notifier/issues/38)) ([355785e](https://github.com/dominique-mueller/angular-notifier/commit/355785e))
212 |
213 | ### Documentation
214 |
215 | - **README:** Add Angular compatibility details, cleanup ([#40](https://github.com/dominique-mueller/angular-notifier/issues/40)) ([9286920](https://github.com/dominique-mueller/angular-notifier/commit/9286920))
216 | - **README:** Fix wrong notifier container selector ([#32](https://github.com/dominique-mueller/angular-notifier/issues/32)) ([7b82d35](https://github.com/dominique-mueller/angular-notifier/commit/7b82d35)), closes [#30](https://github.com/dominique-mueller/angular-notifier/issues/30)
217 |
218 | ### BREAKING CHANGES
219 |
220 | - **angular:** The upgrade to Angular 5 breaks compatibility with Angular 4.
221 |
222 |
223 |
224 | ## [2.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/2.0.0) (2017-05-11)
225 |
226 | ### Features
227 |
228 | - **angular:** Upgrade to Angular 4 and its new APIs ([#19](https://github.com/dominique-mueller/angular-notifier/issues/19)) ([0a0be99](https://github.com/dominique-mueller/angular-notifier/commit/0a0be99))
229 |
230 | ### Bug Fixes
231 |
232 | - **notifier-config:** Fix notifier config injection error, refactor notifier module ([#22](https://github.com/dominique-mueller/angular-notifier/issues/22)) ([67f09f5](https://github.com/dominique-mueller/angular-notifier/commit/67f09f5)), closes [#17](https://github.com/dominique-mueller/angular-notifier/issues/17)
233 |
234 | ### Documentation
235 |
236 | - **preview:** Update animated GIF preview showing the new colors ([#18](https://github.com/dominique-mueller/angular-notifier/issues/18)) ([571b098](https://github.com/dominique-mueller/angular-notifier/commit/571b098))
237 | - **README, MIGRATION:** Update README, add MIGRATION-GUIDE ([#28](https://github.com/dominique-mueller/angular-notifier/issues/28)) ([f2c7781](https://github.com/dominique-mueller/angular-notifier/commit/f2c7781))
238 |
239 | ### Refactoring
240 |
241 | - **animations:** Refactor usage of Web Animations API, add typings ([#27](https://github.com/dominique-mueller/angular-notifier/issues/27)) ([d34f9f3](https://github.com/dominique-mueller/angular-notifier/commit/d34f9f3)), closes [#6](https://github.com/dominique-mueller/angular-notifier/issues/6) [#10](https://github.com/dominique-mueller/angular-notifier/issues/10)
242 | - **naming:** Refactor namings to no longer use the "x-" prefix ([#26](https://github.com/dominique-mueller/angular-notifier/issues/26)) ([d2158bd](https://github.com/dominique-mueller/angular-notifier/commit/d2158bd))
243 |
244 | ### BREAKING CHANGES
245 |
246 | - **naming:** Compontent selectors and class name no longer have the "x-" prefix (see MIGRATION GUIDE).
247 | - **notifier-config:** The forRoot() method of the NotifierModule is now called withConfig() (see MIGRATION GUIDE).
248 | - **angular:** The upgrade to Angular 4 and its APIs breaks compatibility with all Angular 2 based applications.
249 |
250 |
251 |
252 | ## [1.0.6](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.6) (2017-04-04)
253 |
254 | ### Styles
255 |
256 | - **type-colors:** Use bootstrap colors for notification types ([18eb1d2](https://github.com/dominique-mueller/angular-notifier/commit/18eb1d2)), closes [#11](https://github.com/dominique-mueller/angular-notifier/issues/11)
257 |
258 |
259 |
260 | ## [1.0.5](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.5) (2017-04-03)
261 |
262 | ### Bug Fixes
263 |
264 | - **notification-container:** Fix wrong ngFor trackby implementation ([f086ae4](https://github.com/dominique-mueller/angular-notifier/commit/f086ae4)), closes [#12](https://github.com/dominique-mueller/angular-notifier/issues/12)
265 |
266 |
267 |
268 | ## [1.0.4](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.4) (2017-03-21)
269 |
270 | ### Bug Fixes
271 |
272 | - **aot:** Fixed Angular AoT compilation issue ([e5ed9bb](https://github.com/dominique-mueller/angular-notifier/commit/e5ed9bb)), closes [#7](https://github.com/dominique-mueller/angular-notifier/issues/7)
273 |
274 |
275 |
276 | ## [1.0.3](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.3) (2017-02-05)
277 |
278 | ### Bug Fixes
279 |
280 | - **aot:** Fixed error occuring when using NotifierModule.forRoot with ([a501f40](https://github.com/dominique-mueller/angular-notifier/commit/a501f40)), closes [#5](https://github.com/dominique-mueller/angular-notifier/issues/5)
281 |
282 |
283 |
284 | ## [1.0.2](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.2) (2016-12-21)
285 |
286 | ### Bug Fixes
287 |
288 | - **config:** Fixed broken configuration merge ([9793773](https://github.com/dominique-mueller/angular-notifier/commit/9793773))
289 |
290 |
291 |
292 | ## [1.0.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.1) (2016-12-05)
293 |
294 | ### Bug Fixes
295 |
296 | - **dependencies:** Fixed wrong type dependencies in definition files ([#2](https://github.com/dominique-mueller/angular-notifier/issues/2)) ([a986e66](https://github.com/dominique-mueller/angular-notifier/commit/a986e66)), closes [#1](https://github.com/dominique-mueller/angular-notifier/issues/1)
297 | - **gulp:** Fixed broken release task ([#3](https://github.com/dominique-mueller/angular-notifier/issues/3)) ([cdee2d8](https://github.com/dominique-mueller/angular-notifier/commit/cdee2d8))
298 |
299 |
300 |
301 | ## [1.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.0) (2016-12-04)
302 |
303 | Initial release!
304 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Dominique Müller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MIGRATION-GUIDE.md:
--------------------------------------------------------------------------------
1 | # Migration Guide
2 |
3 | Strictly following the principle of semantic versioning, breaking changes only occur between major versions. This Migration Guide gives
4 | developers a more detailed insight into the changes introduced with new major releases, in particular the breaking changes and their
5 | consequences, while also suggesting a migration strategy.
6 |
7 | Also see then **[CHANGELOG](./CHANGELOG.md)** and **[GitHub releases](https://github.com/dominique-mueller/angular-notifier/releases)**.
8 |
9 |
10 |
11 | ## Migration from `1.x` to `2.x`
12 |
13 | > The amount of breaking changes from `1.x` to `2.x` is rather small, a migration shouldn't take longer than 5 minutes.
14 |
15 | #### Compatible with Angular 4+ only
16 |
17 | The library is now compatible with Angular 4+, using the all new & improved Angular APIs (such as the new `Renderer2`). Consequently, this
18 | also means that the compatibility with Angualr 2 breaks. If you still want to stick to Angular 2, you can continue using the latest `1.x`
19 | release; however, all new development (inlcuding bug fixes and features) will happen in the new `2.x` releases.
20 |
21 | #### Renaming of component selectors and classes
22 |
23 | For consistency reasons, all component selectors and CSS class names got renamed to no longer use the `x-` prefix. To migrate your
24 | application, simply rename the `x-notifier-container` tag to `notifier-container`. Also, if you did write custom themes or overwrote the
25 | default styling, you should remove the `x-` prefix from all CSS class names. The SASS variables, however, are still named the same.
26 |
27 | #### Renaming of module `forRoot()` method
28 |
29 | The `NotifierModule.forRoot()` method was used for passing custom options to the notifier. While the functionality stays the exact same, the
30 | method is now called `NotifierModule.withConfig()` instead. This seemed to be the more semantic, meaningful name here.
31 |
32 | #### Names & paths of published files
33 |
34 | With Angular 4+, a new recommendation regarding the publishment of Angular libraries has been defined. This includes a different folder
35 | structure, and also different output files. Therefore, the published files now include:
36 |
37 | - `angular-notifier.js` as the "rolled up" ES6 FESM (Flat ECMAScript Module) bundle
38 | - `angular-notifier.es5.js` as the "rolled up" ES5 FESM (Flat ECMAScript Module) bundle, however using ES6 import
39 | - `angular-notifier.umd.js` as the ES5 UMD (Universal Module Definition) bundle, here for compatibility reasons
40 | - Both the original `styles.scss` and compiled `styles.css` file exist, yet are available at the root path; sub-files are now located in the
41 | "styles" folder
42 | - Also, the places of all the sourcemaps and TypeScript definition files changed (which, however, shouldn't affect anyone)
43 |
44 | *The only change affecting developers is probably the path change of the SASS / CSS files. When using SystemJS, a path change of JavaScript
45 | files might also be necessary. Most modern frontend build tools (such as Webpack or Rollup) will recognize and understand this library and
46 | its published files automatically.*
47 |
48 | #### Web Animations API polyfill
49 |
50 | The implementation of animations has been changed slightly, so that now the *default* Web Animations API polyfill should be sufficient to
51 | make this library work in older browsers. This is also the polyfill defined within Angular CLI based projects in the `polyfills.ts` file by
52 | default. While it for sure will save us a few bytes over the network line, it also prevents confusion amongst developers (such as
53 | **[#6](https://github.com/dominique-mueller/angular-notifier/issues/6)**,
54 | **[#10](https://github.com/dominique-mueller/angular-notifier/issues/10)**). In particular:
55 |
56 | ``` typescript
57 | // With 1.x
58 | import 'web-animations-js/web-animations-next.min.js';
59 |
60 | // Now with 2.x
61 | import 'web-animations-js';
62 | // Same as: import 'web-animations-js/web-animations.min.js';
63 | ```
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # angular-notifier
4 |
5 | **A well designed, fully animated, highly customizable, and easy-to-use notification library for your **Angular 2+** application.**
6 |
7 |
8 |
9 |
10 |
11 | ## Demo
12 |
13 | You can play around with this library with **[this Stackblitz right here](https://stackblitz.com/edit/angular-notifier-demo)**.
14 |
15 | 
16 |
17 |
18 |
19 | ## How to install
20 |
21 | You can get **angular-notifier** via **npm** by either adding it as a new _dependency_ to your `package.json` file and running npm install,
22 | or running the following command:
23 |
24 | ```bash
25 | npm install angular-notifier
26 | ```
27 |
28 |
29 |
30 | ### Angular versions
31 |
32 | The following list describes the compatibility with Angular:
33 |
34 | | Angular Notifier | Angular | Compilation |
35 | | ---------------- | ------- | ------------------ |
36 | | `1.x` | `2.x` | View Engine |
37 | | `2.x` | `4.x` | View Engine |
38 | | `3.x` | `5.x` | View Engine |
39 | | `4.x` | `6.x` | View Engine |
40 | | `5.x` | `7.x` | View Engine |
41 | | `6.x` | `8.x` | View Engine |
42 | | `7.x` | `9.x` | View Engine |
43 | | `8.x` | `10.x` | View Engine |
44 | | `9.x` | `11.x` | View Engine |
45 | | `10.x` | `12.x` | View Engine |
46 | | `11.x` | `13.x` | Ivy (partial mode) |
47 | | `12.x` | `14.x` | Ivy (partial mode) |
48 | | `13.x` | `15.x` | Ivy (partial mode) |
49 | | `14.x` | `16.x` | Ivy (partial mode) |
50 |
51 |
52 |
53 | ## How to setup
54 |
55 | Before actually being able to use the **angular-notifier** library within our code, we have to first set it up within Angular, and also
56 | bring the styles into our project.
57 |
58 |
59 |
60 | ### 1. Import the `NotifierModule`
61 |
62 | First of all, make **angular-notifier** globally available to your Angular application by importing (and optionally also configuring) the
63 | `NotifierModule` the your root Angular module. For example:
64 |
65 | ```typescript
66 | import { NotifierModule } from 'angular-notifier';
67 |
68 | @NgModule({
69 | imports: [NotifierModule],
70 | })
71 | export class AppModule {}
72 | ```
73 |
74 | But wait -- your probably might want to customize your notifications' look and behaviour according to your requirements and needs. To do so,
75 | call the `withConfig` method on the `NotifierModule`, and pass in the options. For example:
76 |
77 | ```typescript
78 | import { NotifierModule } from 'angular-notifier';
79 |
80 | @NgModule({
81 | imports: [
82 | NotifierModule.withConfig({
83 | // Custom options in here
84 | }),
85 | ],
86 | })
87 | export class AppModule {}
88 | ```
89 |
90 |
91 |
92 | ### 2. Use the `notifier-container` component
93 |
94 | In addition, you have to place the `notifier-container` component somewhere in your application, best at the last element of your
95 | root (app) component. For example:
96 |
97 | ```typescript
98 | @Component({
99 | selector: 'my-app',
100 | template: `
101 | Hello World
102 |
103 | `,
104 | })
105 | export class AppComponent {}
106 | ```
107 |
108 | > Later on, this component will contain and manage all your applications' notifications.
109 |
110 |
111 |
112 | ### 3. Import the styles
113 |
114 | Of course we also need to import the **angular-notifier** styles into our application. Depending on the architecture of your Angular
115 | application, you want to either import the original SASS files, or the already compiled CSS files instead - or none of them if you wish to
116 | write your own styles from scratch.
117 |
118 | #### The easy way: Import all the styles
119 |
120 | To import all the styles, simple include either the `~/angular-notifier/styles.(scss|css)` file. It contains the core styles as well as all
121 | the themes and notification types.
122 |
123 | #### The advanced way: Only import the styles actually needed
124 |
125 | To keep the size if your styles as small as possible (improving performance for the perfect UX), your might instead decide to only import
126 | the styles actually needed by our application. The **angular-notifier** styles are modular:
127 |
128 | - The `~/angular-notifier/styles/core.(scss|css)` file is always required, it defines the basic styles (such as the layout)
129 | - Themes can be imported from the `~/angular-notifier/styles/theme` folder
130 | - The different notification types, then, can be imported from the `~/angular-notifier/styles/types` folder
131 |
132 |
133 |
134 | ## How to use
135 |
136 | Using **angular-notifier** is as simple as it can get -- simple import and inject the `NotifierService` into every component (directive,
137 | service, ...) you want to use in. For example:
138 |
139 | ```typescript
140 | import { NotifierService } from 'angular-notifier';
141 |
142 | @Component({
143 | // ...
144 | })
145 | export class MyAwesomeComponent {
146 | private readonly notifier: NotifierService;
147 |
148 | constructor(notifierService: NotifierService) {
149 | this.notifier = notifierService;
150 | }
151 | }
152 | ```
153 |
154 |
155 |
156 | ### Show notifications
157 |
158 | Showing a notification is simple - all your need is a type, and a message to be displayed. For example:
159 |
160 | ```typescript
161 | this.notifier.notify('success', 'You are awesome! I mean it!');
162 | ```
163 |
164 | You can further pass in a _notification ID_ as the third (optional) argument. Essentially, such a _notification ID_ is nothing more but a
165 | unique string tha can be used later on to gain access (and thus control) to this specific notification. For example:
166 |
167 | ```typescript
168 | this.notifier.notify('success', 'You are awesome! I mean it!', 'THAT_NOTIFICATION_ID');
169 | ```
170 |
171 | > For example, you might want to define a _notification ID_ if you know that, at some point in the future, you will need to remove _this
172 | > exact_ notification.
173 |
174 | **The syntax above is actually just a shorthand version of the following:**
175 |
176 | ```typescript
177 | this.notifier.show({
178 | type: 'success',
179 | message: 'You are awesome! I mean it!',
180 | id: 'THAT_NOTIFICATION_ID', // Again, this is optional
181 | });
182 | ```
183 |
184 |
185 |
186 | ### Hide notifications
187 |
188 | You can also hide notifications. To hide a specific notification - assuming you've defined a _notification ID_ when creating it, simply
189 | call:
190 |
191 | ```typescript
192 | this.notifier.hide('THAT_NOTIFICATION_ID');
193 | ```
194 |
195 | Furthermore, your can hide the newest notification by calling:
196 |
197 | ```typescript
198 | this.notifier.hideNewest();
199 | ```
200 |
201 | Or, your could hide the oldest notification:
202 |
203 | ```typescript
204 | this.notifier.hideOldest();
205 | ```
206 |
207 | And, of course, it's also possible to hide all visible notifications at once:
208 |
209 | ```typescript
210 | this.notifier.hideAll();
211 | ```
212 |
213 |
214 |
215 | ## How to customize
216 |
217 | From the beginning, the **angular-notifier** library has been written with customizability in mind. The idea is that **angular-notifier**
218 | works the way your want it to, so that you can make it blend perfectly into the rest of your application. Still, the default configuration
219 | should already provide a great User Experience.
220 |
221 | > Keep in mind that **angular-notifier** can be configured only once - which is at the time you import the `NotifierModule` into your root
222 | > (app) module.
223 |
224 |
225 |
226 | ### Position
227 |
228 | With the `position` property you can define where exactly notifications will appear on the screen:
229 |
230 | ```typescript
231 | position: {
232 |
233 | horizontal: {
234 |
235 | /**
236 | * Defines the horizontal position on the screen
237 | * @type {'left' | 'middle' | 'right'}
238 | */
239 | position: 'left',
240 |
241 | /**
242 | * Defines the horizontal distance to the screen edge (in px)
243 | * @type {number}
244 | */
245 | distance: 12
246 |
247 | },
248 |
249 | vertical: {
250 |
251 | /**
252 | * Defines the vertical position on the screen
253 | * @type {'top' | 'bottom'}
254 | */
255 | position: 'bottom',
256 |
257 | /**
258 | * Defines the vertical distance to the screen edge (in px)
259 | * @type {number}
260 | */
261 | distance: 12
262 |
263 | /**
264 | * Defines the vertical gap, existing between multiple notifications (in px)
265 | * @type {number}
266 | */
267 | gap: 10
268 |
269 | }
270 |
271 | }
272 | ```
273 |
274 |
275 |
276 | ### Theme
277 |
278 | With the `theme` property you can change the overall look and feel of your notifications:
279 |
280 | ```typescript
281 | /**
282 | * Defines the notification theme, responsible for the Visual Design of notifications
283 | * @type {string}
284 | */
285 | theme: 'material';
286 | ```
287 |
288 | #### Theming in detail
289 |
290 | Well, how does theming actually work? In the end, the value set for the `theme` property will be part of a class added to each notification
291 | when being created. For example, using `material` as the theme results in all notifications getting a class assigned named `x-notifier__notification--material`.
292 |
293 | > Everyone - yes, I'm looking at you - can use this mechanism to write custom notification themes and apply them via the `theme` property.
294 | > For example on how to create a theme from scratch, just take a look at the themes coming along with this library (as for now only the
295 | > `material` theme).
296 |
297 |
298 |
299 | ### Behaviour
300 |
301 | With the `behaviour` property you can define how notifications will behave in different situations:
302 |
303 | ```typescript
304 | behaviour: {
305 |
306 | /**
307 | * Defines whether each notification will hide itself automatically after a timeout passes
308 | * @type {number | false}
309 | */
310 | autoHide: 5000,
311 |
312 | /**
313 | * Defines what happens when someone clicks on a notification
314 | * @type {'hide' | false}
315 | */
316 | onClick: false,
317 |
318 | /**
319 | * Defines what happens when someone hovers over a notification
320 | * @type {'pauseAutoHide' | 'resetAutoHide' | false}
321 | */
322 | onMouseover: 'pauseAutoHide',
323 |
324 | /**
325 | * Defines whether the dismiss button is visible or not
326 | * @type {boolean}
327 | */
328 | showDismissButton: true,
329 |
330 | /**
331 | * Defines whether multiple notification will be stacked, and how high the stack limit is
332 | * @type {number | false}
333 | */
334 | stacking: 4
335 |
336 | }
337 | ```
338 |
339 |
340 |
341 | ### Custom Templates
342 |
343 | If you need more control over how the inner HTML part of the notification looks like, either because your style-guide requires it, or for being able to add icons etc, then you can **define a custom ``** which you pass to the `NotifierService`.
344 |
345 | You can define a custom `ng-template` as follows:
346 |
347 | ```html
348 |
349 | {{ notificationData.message }}
350 |
351 | ```
352 |
353 | In this case you could wrap your own HTML, even a `` component which you might use in your application. The notification data is passed in as a `notification` object, which you can reference inside the `` using the `let-` syntax.
354 |
355 | Inside your component, you can then reference the `` by its template variable `#customNotification` using Angular's `ViewChild`:
356 |
357 | ```typescript
358 | import { ViewChild } from '@angular/core';
359 |
360 | @Component({
361 | // ...
362 | })
363 | export class SomeComponent {
364 | @ViewChild('customNotification', { static: true }) customNotificationTmpl;
365 |
366 | constructor(private notifierService: NotifierService) {}
367 |
368 | showNotification() {
369 | this.notifier.show({
370 | message: 'Hi there!',
371 | type: 'info',
372 | template: this.customNotificationTmpl,
373 | });
374 | }
375 | }
376 | ```
377 |
378 |
379 |
380 | ### Animations
381 |
382 | With the `animations` property your can define whether and how exactly notification will be animated:
383 |
384 | ```typescript
385 | animations: {
386 |
387 | /**
388 | * Defines whether all (!) animations are enabled or disabled
389 | * @type {boolean}
390 | */
391 | enabled: true,
392 |
393 | show: {
394 |
395 | /**
396 | * Defines the animation preset that will be used to animate a new notification in
397 | * @type {'fade' | 'slide'}
398 | */
399 | preset: 'slide',
400 |
401 | /**
402 | * Defines how long it will take to animate a new notification in (in ms)
403 | * @type {number}
404 | */
405 | speed: 300,
406 |
407 | /**
408 | * Defines which easing method will be used when animating a new notification in
409 | * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'}
410 | */
411 | easing: 'ease'
412 |
413 | },
414 |
415 | hide: {
416 |
417 | /**
418 | * Defines the animation preset that will be used to animate a new notification out
419 | * @type {'fade' | 'slide'}
420 | */
421 | preset: 'fade',
422 |
423 | /**
424 | * Defines how long it will take to animate a new notification out (in ms)
425 | * @type {number}
426 | */
427 | speed: 300,
428 |
429 | /**
430 | * Defines which easing method will be used when animating a new notification out
431 | * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'}
432 | */
433 | easing: 'ease',
434 |
435 | /**
436 | * Defines the animation offset used when hiding multiple notifications at once (in ms)
437 | * @type {number | false}
438 | */
439 | offset: 50
440 |
441 | },
442 |
443 | shift: {
444 |
445 | /**
446 | * Defines how long it will take to shift a notification around (in ms)
447 | * @type {number}
448 | */
449 | speed: 300,
450 |
451 | /**
452 | * Defines which easing method will be used when shifting a notification around
453 | * @type {string}
454 | */
455 | easing: 'ease' // All standard CSS easing methods work
456 |
457 | },
458 |
459 | /**
460 | * Defines the overall animation overlap, allowing for much smoother looking animations (in ms)
461 | * @type {number | false}
462 | */
463 | overlap: 150
464 |
465 | }
466 | ```
467 |
468 |
469 |
470 | ### In short -- the default configuration
471 |
472 | To sum it up, the following is the default configuration _(copy-paste-friendly)_:
473 |
474 | ```typescript
475 | const notifierDefaultOptions: NotifierOptions = {
476 | position: {
477 | horizontal: {
478 | position: 'left',
479 | distance: 12,
480 | },
481 | vertical: {
482 | position: 'bottom',
483 | distance: 12,
484 | gap: 10,
485 | },
486 | },
487 | theme: 'material',
488 | behaviour: {
489 | autoHide: 5000,
490 | onClick: false,
491 | onMouseover: 'pauseAutoHide',
492 | showDismissButton: true,
493 | stacking: 4,
494 | },
495 | animations: {
496 | enabled: true,
497 | show: {
498 | preset: 'slide',
499 | speed: 300,
500 | easing: 'ease',
501 | },
502 | hide: {
503 | preset: 'fade',
504 | speed: 300,
505 | easing: 'ease',
506 | offset: 50,
507 | },
508 | shift: {
509 | speed: 300,
510 | easing: 'ease',
511 | },
512 | overlap: 150,
513 | },
514 | };
515 | ```
516 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "cli": {
4 | "analytics": false
5 | },
6 | "version": 1,
7 | "newProjectRoot": "projects",
8 | "projects": {
9 | "angular-notifier": {
10 | "projectType": "library",
11 | "root": "projects/angular-notifier",
12 | "sourceRoot": "projects/angular-notifier/src",
13 | "architect": {
14 | "build": {
15 | "builder": "@angular-devkit/build-angular:ng-packagr",
16 | "options": {
17 | "tsConfig": "projects/angular-notifier/tsconfig.lib.json",
18 | "project": "projects/angular-notifier/ng-package.json"
19 | },
20 | "configurations": {
21 | "production": {
22 | "tsConfig": "projects/angular-notifier/tsconfig.lib.prod.json"
23 | }
24 | }
25 | },
26 | "test": {
27 | "builder": "@angular-builders/jest:run",
28 | "options": {
29 | "configPath": "jest.config.json"
30 | }
31 | }
32 | }
33 | },
34 | "angular-notifier-demo": {
35 | "projectType": "application",
36 | "root": "projects/angular-notifier-demo/",
37 | "sourceRoot": "projects/angular-notifier-demo/src",
38 | "architect": {
39 | "build": {
40 | "builder": "@angular-devkit/build-angular:browser",
41 | "options": {
42 | "outputPath": "dist/angular-notifier-demo",
43 | "index": "projects/angular-notifier-demo/src/index.html",
44 | "main": "projects/angular-notifier-demo/src/main.ts",
45 | "polyfills": "projects/angular-notifier-demo/src/polyfills.ts",
46 | "tsConfig": "projects/angular-notifier-demo/tsconfig.app.json",
47 | "assets": ["projects/angular-notifier-demo/src/favicon.ico", "projects/angular-notifier-demo/src/assets"],
48 | "styles": ["projects/angular-notifier-demo/src/styles.scss"],
49 | "scripts": [],
50 | "vendorChunk": true,
51 | "extractLicenses": false,
52 | "buildOptimizer": false,
53 | "sourceMap": true,
54 | "optimization": false,
55 | "namedChunks": true
56 | },
57 | "configurations": {
58 | "production": {
59 | "fileReplacements": [
60 | {
61 | "replace": "projects/angular-notifier-demo/src/environments/environment.ts",
62 | "with": "projects/angular-notifier-demo/src/environments/environment.prod.ts"
63 | }
64 | ],
65 | "optimization": true,
66 | "outputHashing": "all",
67 | "sourceMap": false,
68 | "namedChunks": false,
69 | "vendorChunk": false,
70 | "buildOptimizer": true
71 | }
72 | },
73 | "defaultConfiguration": ""
74 | },
75 | "serve": {
76 | "builder": "@angular-devkit/build-angular:dev-server",
77 | "options": {
78 | "browserTarget": "angular-notifier-demo:build"
79 | },
80 | "configurations": {
81 | "production": {
82 | "browserTarget": "angular-notifier-demo:build:production"
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment:
2 | layout: "diff, flags, files" # Do not show the weird graph
3 |
4 | coverage:
5 | status:
6 | project:
7 | default:
8 | threshold: 1 # Allow the coverage to decrease by 1%
9 | patch:
10 | default:
11 | threshold: 1 # Allow the coverage to decrease by 1%
12 |
--------------------------------------------------------------------------------
/docs/angular-notifier-preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dominique-mueller/angular-notifier/517cda4a8fd6d038ef2a97a36d5fac3e89740d24/docs/angular-notifier-preview.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-notifier",
3 | "description": "A well designed, fully animated, highly customizable, and easy-to-use notification library for your Angular application.",
4 | "version": "14.0.0",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/dominique-mueller/angular-notifier"
9 | },
10 | "keywords": [
11 | "angular",
12 | "angular2",
13 | "ng",
14 | "ng2",
15 | "notifier",
16 | "notification",
17 | "notifications",
18 | "toast",
19 | "toasts",
20 | "alert",
21 | "library"
22 | ],
23 | "scripts": {
24 | "build:demo": "ng build angular-notifier-demo --configuration production",
25 | "build:library": "rimraf -r dist && npm run build:library:angular && npm run build:library:sass && npm run build:library:css && npm run build:library:docs && npm run build:library:package",
26 | "build:library:angular": "ng build angular-notifier --configuration production",
27 | "build:library:css": "sass projects/angular-notifier/src:dist/angular-notifier --style=expanded",
28 | "build:library:docs": "copyfiles \"docs/**\" CHANGELOG.md MIGRATION-GUIDE.md LICENSE README.md \"dist/angular-notifier\"",
29 | "build:library:package": "node tools/update-package.js",
30 | "build:library:sass": "copyfiles \"projects/angular-notifier/src/**/*.scss\" \"dist/angular-notifier\" --up 3",
31 | "lint:library": "eslint projects/angular-notifier/src/**/*.ts --max-warnings 0",
32 | "lint:library:fix": "eslint projects/angular-notifier/src/**/*.ts --max-warnings 0 --fix",
33 | "start": "ng serve angular-notifier-demo",
34 | "test:library": "ng test angular-notifier",
35 | "test:library:upload-coverage": "codecov -f coverage/coverage-final.json"
36 | },
37 | "dependencies": {
38 | "tslib": "2.4.x"
39 | },
40 | "peerDependencies": {
41 | "@angular/common": ">= 16.0.0 < 17.0.0",
42 | "@angular/core": ">= 16.0.0 < 17.0.0"
43 | },
44 | "devDependencies": {
45 | "@angular-builders/jest": "16.0.x",
46 | "@angular-devkit/build-angular": "16.0.x",
47 | "@angular/cli": "16.0.x",
48 | "@angular/common": "16.0.x",
49 | "@angular/compiler": "16.0.x",
50 | "@angular/compiler-cli": "16.0.x",
51 | "@angular/core": "16.0.x",
52 | "@angular/platform-browser": "16.0.x",
53 | "@angular/platform-browser-dynamic": "16.0.x",
54 | "@types/jest": "29.5.x",
55 | "@types/node": "16.x",
56 | "@types/web-animations-js": "2.2.x",
57 | "@typescript-eslint/eslint-plugin": "5.59.x",
58 | "@typescript-eslint/parser": "5.59.x",
59 | "codecov": "3.8.x",
60 | "copyfiles": "2.4.x",
61 | "eslint": "8.42.x",
62 | "eslint-config-prettier": "8.5.x",
63 | "eslint-plugin-import": "2.26.x",
64 | "eslint-plugin-prettier": "4.2.x",
65 | "eslint-plugin-simple-import-sort": "8.0.x",
66 | "jest": "29.5.x",
67 | "ng-packagr": "16.0.x",
68 | "prettier": "2.7.x",
69 | "rimraf": "3.0.x",
70 | "rxjs": "7.5.x",
71 | "sass": "1.54.x",
72 | "ts-node": "10.9.x",
73 | "typescript": "4.9.x",
74 | "zone.js": "0.13.x"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/app/app.component.html:
--------------------------------------------------------------------------------
1 | "angular-notifier" demo
2 |
3 | Show notifications
4 | Default me!
5 | Info me!
6 | Success me!
7 |
8 | Warning me!
9 |
10 | Error me!
11 |
12 | Custom notification
13 |
14 |
15 | Hide notifications
16 | Hide all notifications!
17 | Hide oldest notification!
18 | Hide newest notification!
19 |
20 | Show & hide a specific notification
21 |
22 | Show notification with ID named 'ID_TEST'
23 |
24 | Hide notification with ID named 'ID_TEST'
25 |
26 |
27 |
28 |
29 |
30 | {{ notification.type }}: {{ notification.message }}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild } from '@angular/core';
2 | import { NotifierService } from 'angular-notifier';
3 |
4 | /**
5 | * App component
6 | */
7 | @Component({
8 | host: {
9 | class: 'app',
10 | },
11 | selector: 'app',
12 | templateUrl: './app.component.html',
13 | })
14 | export class AppComponent {
15 | @ViewChild('customTemplate', { static: true }) customNotificationTmpl;
16 |
17 | /**
18 | * Notifier service
19 | */
20 | private notifier: NotifierService;
21 |
22 | /**
23 | * Constructor
24 | *
25 | * @param {NotifierService} notifier Notifier service
26 | */
27 | public constructor(notifier: NotifierService) {
28 | this.notifier = notifier;
29 | }
30 |
31 | /**
32 | * Show a notification
33 | *
34 | * @param {string} type Notification type
35 | * @param {string} message Notification message
36 | */
37 | public showNotification(type: string, message: string): void {
38 | this.notifier.notify(type, message);
39 | }
40 |
41 | /**
42 | * Hide oldest notification
43 | */
44 | public hideOldestNotification(): void {
45 | this.notifier.hideOldest();
46 | }
47 |
48 | /**
49 | * Hide newest notification
50 | */
51 | public hideNewestNotification(): void {
52 | this.notifier.hideNewest();
53 | }
54 |
55 | /**
56 | * Hide all notifications at once
57 | */
58 | public hideAllNotifications(): void {
59 | this.notifier.hideAll();
60 | }
61 |
62 | /**
63 | * Show custom notification using template
64 | *
65 | * @param {string} type Notification type
66 | * @param {string} message Notification message
67 | */
68 | public showCustomNotificationTemplate(type: string, message: string): void {
69 | this.notifier.show({
70 | message,
71 | type,
72 | template: this.customNotificationTmpl,
73 | });
74 | }
75 |
76 | /**
77 | * Show a specific notification (with a custom notification ID)
78 | *
79 | * @param {string} type Notification type
80 | * @param {string} message Notification message
81 | * @param {string} id Notification ID
82 | */
83 | public showSpecificNotification(type: string, message: string, id: string): void {
84 | this.notifier.show({
85 | id,
86 | message,
87 | type,
88 | });
89 | }
90 |
91 | /**
92 | * Hide a specific notification (by a given notification ID)
93 | *
94 | * @param {string} id Notification ID
95 | */
96 | public hideSpecificNotification(id: string): void {
97 | this.notifier.hide(id);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { NotifierModule, NotifierOptions } from 'angular-notifier';
4 |
5 | import { AppComponent } from './app.component';
6 |
7 | /**
8 | * Custom angular notifier options
9 | */
10 | const customNotifierOptions: NotifierOptions = {
11 | position: {
12 | horizontal: {
13 | position: 'left',
14 | distance: 12,
15 | },
16 | vertical: {
17 | position: 'bottom',
18 | distance: 12,
19 | gap: 10,
20 | },
21 | },
22 | theme: 'material',
23 | behaviour: {
24 | autoHide: false,
25 | onClick: false,
26 | onMouseover: 'pauseAutoHide',
27 | showDismissButton: true,
28 | stacking: 4,
29 | },
30 | animations: {
31 | enabled: true,
32 | show: {
33 | preset: 'slide',
34 | speed: 300,
35 | easing: 'ease',
36 | },
37 | hide: {
38 | preset: 'fade',
39 | speed: 300,
40 | easing: 'ease',
41 | offset: 50,
42 | },
43 | shift: {
44 | speed: 300,
45 | easing: 'ease',
46 | },
47 | overlap: 150,
48 | },
49 | };
50 |
51 | /**
52 | * App module
53 | */
54 | @NgModule({
55 | bootstrap: [AppComponent],
56 | declarations: [AppComponent],
57 | imports: [BrowserModule, NotifierModule.withConfig(customNotifierOptions)],
58 | })
59 | export class AppModule {}
60 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false,
3 | };
4 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dominique-mueller/angular-notifier/517cda4a8fd6d038ef2a97a36d5fac3e89740d24/projects/angular-notifier-demo/src/favicon.ico
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | "angular-notifier" demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Loading demo for "angular-notifier" ...
16 |
17 |
18 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch((err) => console.error(err));
14 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import '../../angular-notifier/src/styles';
2 |
3 | /* BASIC */
4 |
5 | html {
6 | width: 100%;
7 | height: 100%;
8 | }
9 |
10 | body {
11 | width: 100%;
12 | height: 100%;
13 | margin: 0;
14 | box-sizing: border-box;
15 | font-family: 'Roboto', 'Tahoma', 'Trebuchet MS', 'Arial', 'Helvetica', sans-serif;
16 | font-size: 14px;
17 | }
18 |
19 | * {
20 | box-sizing: inherit;
21 | }
22 |
23 | /* APP */
24 |
25 | .app {
26 | display: block;
27 | padding: 30px;
28 | }
29 |
30 | /* HEADINGS */
31 |
32 | h1 {
33 | font-size: 30px;
34 | padding-bottom: 10px;
35 | }
36 |
37 | h2 {
38 | margin-top: 40px;
39 | font-size: 18px;
40 | }
41 |
42 | /* BUTTONS */
43 |
44 | .button {
45 | display: inline-block;
46 | padding: 11px 16px 10px;
47 | margin-right: 4px;
48 | background: none;
49 | border: none;
50 | border-radius: 2px;
51 | cursor: pointer;
52 | opacity: 1;
53 | -webkit-transition: background-color 0.2s ease;
54 | transition: background-color 0.2s ease;
55 | }
56 |
57 | .button--primary {
58 | background-color: #2b90d9;
59 | color: #fff;
60 | }
61 |
62 | .button--primary:hover,
63 | .button--primary:focus,
64 | .button--primary:active {
65 | background-color: #2273ad;
66 | }
67 |
68 | .button--secondary {
69 | background-color: #ddd;
70 | color: #333;
71 | }
72 |
73 | .button--secondary:hover,
74 | .button--secondary:focus,
75 | .button--secondary:active {
76 | background-color: #cacaca;
77 | }
78 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/app",
5 | "types": []
6 | },
7 | "files": ["src/main.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/projects/angular-notifier/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "collectCoverage": true,
3 | "preset": "jest-preset-angular",
4 | "setupFilesAfterEnv": ["./projects/angular-notifier/jest.setup.ts"],
5 | "verbose": true
6 | }
7 |
--------------------------------------------------------------------------------
/projects/angular-notifier/jest.setup.ts:
--------------------------------------------------------------------------------
1 | Object.defineProperty(window, 'getComputedStyle', {
2 | value: () => {
3 | return {
4 | display: 'none',
5 | appearance: ['-webkit-appearance'],
6 | };
7 | },
8 | });
9 |
10 | /**
11 | * ISSUE: https://github.com/angular/material2/issues/7101
12 | * Workaround for JSDOM missing transform property
13 | */
14 | Object.defineProperty(document.body.style, 'transform', {
15 | value: () => {
16 | return {
17 | enumerable: true,
18 | configurable: true,
19 | };
20 | },
21 | });
22 |
23 | // Very cheap way of making the Web Animations API work in Jest. Every element gets a 'animate' method, making it compatible with the Web
24 | // Animations APi calls. However, we essentially do nothing in the method - which is absolutely fine, as every 'elmeent.animate' call gets
25 | // mocked away by Jest anyway.
26 | (window).Element.prototype.animate = (): any => {
27 | // Nothing to implement
28 | };
29 |
--------------------------------------------------------------------------------
/projects/angular-notifier/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/angular-notifier",
4 | "lib": {
5 | "entryFile": "src/index.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/projects/angular-notifier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-notifier",
3 | "sideEffects": [
4 | "*.scss",
5 | "*.css"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/components/notifier-container.component';
2 | export * from './lib/components/notifier-notification.component';
3 | export * from './lib/models/notifier-config.model';
4 | export * from './lib/notifier.module';
5 | export * from './lib/notifier.tokens';
6 | export * from './lib/services/notifier.service';
7 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/fade.animation-preset.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 | import { NotifierConfig } from '../models/notifier-config.model';
3 | import { fade } from './fade.animation-preset';
4 |
5 | /**
6 | * Fade animation preset - Unit Test
7 | */
8 | describe('Fade Animation Preset', () => {
9 | describe('(show)', () => {
10 | it('should return animation keyframes', () => {
11 | const testNotification: MockNotification = new MockNotification({});
12 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
13 | from: {
14 | opacity: '0',
15 | },
16 | to: {
17 | opacity: '1',
18 | },
19 | };
20 | const keyframes: NotifierAnimationPresetKeyframes = fade.show(testNotification);
21 |
22 | expect(keyframes).toEqual(expectedKeyframes);
23 | });
24 | });
25 |
26 | describe('(hide)', () => {
27 | it('should return animation keyframes', () => {
28 | const testNotification: MockNotification = new MockNotification({});
29 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
30 | from: {
31 | opacity: '1',
32 | },
33 | to: {
34 | opacity: '0',
35 | },
36 | };
37 | const keyframes: NotifierAnimationPresetKeyframes = fade.hide(testNotification);
38 |
39 | expect(keyframes).toEqual(expectedKeyframes);
40 | });
41 | });
42 | });
43 |
44 | /**
45 | * Mock Notification Height
46 | */
47 | const mockNotificationHeight = 40;
48 |
49 | /**
50 | * Mock Notification Shift
51 | */
52 | const mockNotificationShift = 80;
53 |
54 | /**
55 | * Mock Notification Width
56 | */
57 | const mockNotificationWidth = 300;
58 |
59 | /**
60 | * Mock notification, providing static values except the global configuration
61 | */
62 | class MockNotification {
63 | /**
64 | * Configuration
65 | */
66 | public config: NotifierConfig;
67 |
68 | /**
69 | * Notification ID
70 | */
71 | public id = 'ID_FAKE';
72 |
73 | /**
74 | * Notification type
75 | */
76 | public type = 'SUCCESS';
77 |
78 | /**
79 | * Notification message
80 | */
81 | public message = 'Lorem ipsum dolor sit amet.';
82 |
83 | /**
84 | * Notification component
85 | */
86 | public component: any = {
87 | getConfig: () => this.config,
88 | getHeight: () => mockNotificationHeight,
89 | getShift: () => mockNotificationShift,
90 | getWidth: () => mockNotificationWidth,
91 | };
92 |
93 | /**
94 | * Constructor
95 | *
96 | * @param {NotifierConfig} config Configuration
97 | */
98 | public constructor(config: NotifierConfig) {
99 | this.config = config;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/fade.animation-preset.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 |
3 | /**
4 | * Fade animation preset
5 | */
6 | export const fade: NotifierAnimationPreset = {
7 | hide: (): NotifierAnimationPresetKeyframes => {
8 | return {
9 | from: {
10 | opacity: '1',
11 | },
12 | to: {
13 | opacity: '0',
14 | },
15 | };
16 | },
17 | show: (): NotifierAnimationPresetKeyframes => {
18 | return {
19 | from: {
20 | opacity: '0',
21 | },
22 | to: {
23 | opacity: '1',
24 | },
25 | };
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/slide.animation-preset.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 | import { NotifierConfig } from '../models/notifier-config.model';
3 | import { slide } from './slide.animation-preset';
4 |
5 | /**
6 | * Slide Animation Preset - Unit Test
7 | */
8 | describe('Slide Animation Preset', () => {
9 | describe('(show)', () => {
10 | it('should return animation keyframes for top-left position', () => {
11 | const testConfig: NotifierConfig = new NotifierConfig({
12 | position: {
13 | horizontal: {
14 | distance: 50,
15 | position: 'left',
16 | },
17 | vertical: {
18 | distance: 100,
19 | position: 'top',
20 | },
21 | },
22 | });
23 | const testNotification: MockNotification = new MockNotification(testConfig);
24 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
25 | from: {
26 | transform: `translate3d( calc( -100% - ${testConfig.position.horizontal.distance}px - 10px ), 0, 0 )`,
27 | },
28 | to: {
29 | transform: 'translate3d( 0, 0, 0 )',
30 | },
31 | };
32 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
33 |
34 | expect(keyframes).toEqual(expectedKeyframes);
35 | });
36 |
37 | it('should return animation keyframes for top-right position', () => {
38 | const testConfig: NotifierConfig = new NotifierConfig({
39 | position: {
40 | horizontal: {
41 | distance: 50,
42 | position: 'right',
43 | },
44 | vertical: {
45 | distance: 100,
46 | position: 'top',
47 | },
48 | },
49 | });
50 | const testNotification: MockNotification = new MockNotification(testConfig);
51 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
52 | from: {
53 | transform: `translate3d( calc( 100% + ${testConfig.position.horizontal.distance}px + 10px ), 0, 0 )`,
54 | },
55 | to: {
56 | transform: 'translate3d( 0, 0, 0 )',
57 | },
58 | };
59 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
60 |
61 | expect(keyframes).toEqual(expectedKeyframes);
62 | });
63 |
64 | it('should return animation keyframes for top-middle position', () => {
65 | const testConfig: NotifierConfig = new NotifierConfig({
66 | position: {
67 | horizontal: {
68 | distance: 50,
69 | position: 'middle',
70 | },
71 | vertical: {
72 | distance: 100,
73 | position: 'top',
74 | },
75 | },
76 | });
77 | const testNotification: MockNotification = new MockNotification(testConfig);
78 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
79 | from: {
80 | transform: `translate3d( -50%, calc( -100% - ${testConfig.position.horizontal.distance}px - 10px ), 0 )`,
81 | },
82 | to: {
83 | transform: 'translate3d( -50%, 0, 0 )',
84 | },
85 | };
86 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
87 |
88 | expect(keyframes).toEqual(expectedKeyframes);
89 | });
90 |
91 | it('should return animation keyframes for bottom-middle position', () => {
92 | const testConfig: NotifierConfig = new NotifierConfig({
93 | position: {
94 | horizontal: {
95 | distance: 50,
96 | position: 'middle',
97 | },
98 | vertical: {
99 | distance: 100,
100 | position: 'bottom',
101 | },
102 | },
103 | });
104 | const testNotification: MockNotification = new MockNotification(testConfig);
105 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
106 | from: {
107 | transform: `translate3d( -50%, calc( 100% + ${testConfig.position.horizontal.distance}px + 10px ), 0 )`,
108 | },
109 | to: {
110 | transform: 'translate3d( -50%, 0, 0 )',
111 | },
112 | };
113 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
114 |
115 | expect(keyframes).toEqual(expectedKeyframes);
116 | });
117 | });
118 |
119 | describe('(hide)', () => {
120 | it('should return animation keyframes for top-left position', () => {
121 | const testConfig: NotifierConfig = new NotifierConfig({
122 | position: {
123 | horizontal: {
124 | distance: 50,
125 | position: 'left',
126 | },
127 | vertical: {
128 | distance: 100,
129 | position: 'top',
130 | },
131 | },
132 | });
133 | const testNotification: MockNotification = new MockNotification(testConfig);
134 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
135 | from: {
136 | transform: `translate3d( 0, ${testNotification.component.getShift()}px, 0 )`,
137 | },
138 | to: {
139 | transform: `translate3d( calc( -100% - ${
140 | testConfig.position.horizontal.distance
141 | }px - 10px ), ${testNotification.component.getShift()}px, 0 )`,
142 | },
143 | };
144 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
145 |
146 | expect(keyframes).toEqual(expectedKeyframes);
147 | });
148 |
149 | it('should return animation keyframes for top-right position', () => {
150 | const testConfig: NotifierConfig = new NotifierConfig({
151 | position: {
152 | horizontal: {
153 | distance: 50,
154 | position: 'right',
155 | },
156 | vertical: {
157 | distance: 100,
158 | position: 'top',
159 | },
160 | },
161 | });
162 | const testNotification: MockNotification = new MockNotification(testConfig);
163 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
164 | from: {
165 | transform: `translate3d( 0, ${testNotification.component.getShift()}px, 0 )`,
166 | },
167 | to: {
168 | transform: `translate3d( calc( 100% + ${
169 | testConfig.position.horizontal.distance
170 | }px + 10px ), ${testNotification.component.getShift()}px, 0 )`,
171 | },
172 | };
173 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
174 |
175 | expect(keyframes).toEqual(expectedKeyframes);
176 | });
177 |
178 | it('should return animation keyframes for top-middle position', () => {
179 | const testConfig: NotifierConfig = new NotifierConfig({
180 | position: {
181 | horizontal: {
182 | distance: 50,
183 | position: 'middle',
184 | },
185 | vertical: {
186 | distance: 100,
187 | position: 'top',
188 | },
189 | },
190 | });
191 | const testNotification: MockNotification = new MockNotification(testConfig);
192 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
193 | from: {
194 | transform: `translate3d( -50%, ${testNotification.component.getShift()}px, 0 )`,
195 | },
196 | to: {
197 | transform: `translate3d( -50%, calc( -100% - ${testConfig.position.horizontal.distance}px - 10px ), 0 )`,
198 | },
199 | };
200 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
201 |
202 | expect(keyframes).toEqual(expectedKeyframes);
203 | });
204 |
205 | it('should return animation keyframes for bottom-middle position', () => {
206 | const testConfig: NotifierConfig = new NotifierConfig({
207 | position: {
208 | horizontal: {
209 | distance: 50,
210 | position: 'middle',
211 | },
212 | vertical: {
213 | distance: 100,
214 | position: 'bottom',
215 | },
216 | },
217 | });
218 | const testNotification: MockNotification = new MockNotification(testConfig);
219 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
220 | from: {
221 | transform: `translate3d( -50%, ${testNotification.component.getShift()}px, 0 )`,
222 | },
223 | to: {
224 | transform: `translate3d( -50%, calc( 100% + ${testConfig.position.horizontal.distance}px + 10px ), 0 )`,
225 | },
226 | };
227 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
228 |
229 | expect(keyframes).toEqual(expectedKeyframes);
230 | });
231 | });
232 | });
233 |
234 | /**
235 | * Mock Notification Height
236 | */
237 | const mockNotificationHeight = 40;
238 |
239 | /**
240 | * Mock Notification Shift
241 | */
242 | const mockNotificationShift = 80;
243 |
244 | /**
245 | * Mock Notification Width
246 | */
247 | const mockNotificationWidth = 300;
248 |
249 | /**
250 | * Mock notification, providing static values except the global configuration
251 | */
252 | class MockNotification {
253 | /**
254 | * Configuration
255 | */
256 | public config: NotifierConfig;
257 |
258 | /**
259 | * Notification ID
260 | */
261 | public id = 'ID_FAKE';
262 |
263 | /**
264 | * Notification type
265 | */
266 | public type = 'SUCCESS';
267 |
268 | /**
269 | * Notification message
270 | */
271 | public message = 'Lorem ipsum dolor sit amet.';
272 |
273 | /**
274 | * Notification component
275 | */
276 | public component: any = {
277 | getConfig: () => this.config,
278 | getHeight: () => mockNotificationHeight,
279 | getShift: () => mockNotificationShift,
280 | getWidth: () => mockNotificationWidth,
281 | };
282 |
283 | /**
284 | * Constructor
285 | *
286 | * @param {NotifierConfig} config Configuration
287 | */
288 | public constructor(config: NotifierConfig) {
289 | this.config = config;
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/slide.animation-preset.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 | import { NotifierConfig } from '../models/notifier-config.model';
3 | import { NotifierNotification } from '../models/notifier-notification.model';
4 |
5 | /**
6 | * Slide animation preset
7 | */
8 | export const slide: NotifierAnimationPreset = {
9 | hide: (notification: NotifierNotification): NotifierAnimationPresetKeyframes => {
10 | // Prepare variables
11 | const config: NotifierConfig = notification.component.getConfig();
12 | const shift: number = notification.component.getShift();
13 | let from: {
14 | [animatablePropertyName: string]: string;
15 | };
16 | let to: {
17 | [animatablePropertyName: string]: string;
18 | };
19 |
20 | // Configure variables, depending on configuration and component
21 | if (config.position.horizontal.position === 'left') {
22 | from = {
23 | transform: `translate3d( 0, ${shift}px, 0 )`,
24 | };
25 | to = {
26 | transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), ${shift}px, 0 )`,
27 | };
28 | } else if (config.position.horizontal.position === 'right') {
29 | from = {
30 | transform: `translate3d( 0, ${shift}px, 0 )`,
31 | };
32 | to = {
33 | transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), ${shift}px, 0 )`,
34 | };
35 | } else {
36 | let horizontalPosition: string;
37 | if (config.position.vertical.position === 'top') {
38 | horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`;
39 | } else {
40 | horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`;
41 | }
42 | from = {
43 | transform: `translate3d( -50%, ${shift}px, 0 )`,
44 | };
45 | to = {
46 | transform: `translate3d( -50%, ${horizontalPosition}, 0 )`,
47 | };
48 | }
49 |
50 | // Done
51 | return {
52 | from,
53 | to,
54 | };
55 | },
56 | show: (notification: NotifierNotification): NotifierAnimationPresetKeyframes => {
57 | // Prepare variables
58 | const config: NotifierConfig = notification.component.getConfig();
59 | let from: {
60 | [animatablePropertyName: string]: string;
61 | };
62 | let to: {
63 | [animatablePropertyName: string]: string;
64 | };
65 |
66 | // Configure variables, depending on configuration and component
67 | if (config.position.horizontal.position === 'left') {
68 | from = {
69 | transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), 0, 0 )`,
70 | };
71 | to = {
72 | transform: 'translate3d( 0, 0, 0 )',
73 | };
74 | } else if (config.position.horizontal.position === 'right') {
75 | from = {
76 | transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), 0, 0 )`,
77 | };
78 | to = {
79 | transform: 'translate3d( 0, 0, 0 )',
80 | };
81 | } else {
82 | let horizontalPosition: string;
83 | if (config.position.vertical.position === 'top') {
84 | horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`;
85 | } else {
86 | horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`;
87 | }
88 | from = {
89 | transform: `translate3d( -50%, ${horizontalPosition}, 0 )`,
90 | };
91 | to = {
92 | transform: 'translate3d( -50%, 0, 0 )',
93 | };
94 | }
95 |
96 | // Done
97 | return {
98 | from,
99 | to,
100 | };
101 | },
102 | };
103 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-container.component.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-container.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
2 | import { Subscription } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 | import { NotifierConfig } from '../models/notifier-config.model';
6 | import { NotifierNotification } from '../models/notifier-notification.model';
7 | import { NotifierService } from '../services/notifier.service';
8 | import { NotifierQueueService } from '../services/notifier-queue.service';
9 | import { NotifierNotificationComponent } from './notifier-notification.component';
10 |
11 | /**
12 | * Notifier container component
13 | * ----------------------------
14 | * This component acts as a wrapper for all notification components; consequently, it is responsible for creating a new notification
15 | * component and removing an existing notification component. Being more precicely, it also handles side effects of those actions, such as
16 | * shifting or even completely removing other notifications as well. Overall, this components handles actions coming from the queue service
17 | * by subscribing to its action stream.
18 | *
19 | * Technical sidenote:
20 | * This component has to be used somewhere in an application to work; it will not inject and create itself automatically, primarily in order
21 | * to not break the Angular AoT compilation. Moreover, this component (and also the notification components) set their change detection
22 | * strategy onPush, which means that we handle change detection manually in order to get the best performance. (#perfmatters)
23 | */
24 | @Component({
25 | changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters)
26 | host: {
27 | class: 'notifier__container',
28 | },
29 | selector: 'notifier-container',
30 | templateUrl: './notifier-container.component.html',
31 | })
32 | export class NotifierContainerComponent implements OnDestroy {
33 | /**
34 | * List of currently somewhat active notifications
35 | */
36 | public notifications: Array;
37 |
38 | /**
39 | * Change detector
40 | */
41 | private readonly changeDetector: ChangeDetectorRef;
42 |
43 | /**
44 | * Notifier queue service
45 | */
46 | private readonly queueService: NotifierQueueService;
47 |
48 | /**
49 | * Notifier configuration
50 | */
51 | private readonly config: NotifierConfig;
52 |
53 | /**
54 | * Queue service observable subscription (saved for cleanup)
55 | */
56 | private queueServiceSubscription: Subscription;
57 |
58 | /**
59 | * Promise resolve function reference, temporarily used while the notification child component gets created
60 | */
61 | private tempPromiseResolver: () => void;
62 |
63 | /**
64 | * Constructor
65 | *
66 | * @param changeDetector Change detector, used for manually triggering change detection runs
67 | * @param notifierQueueService Notifier queue service
68 | * @param notifierService Notifier service
69 | */
70 | public constructor(changeDetector: ChangeDetectorRef, notifierQueueService: NotifierQueueService, notifierService: NotifierService) {
71 | this.changeDetector = changeDetector;
72 | this.queueService = notifierQueueService;
73 | this.config = notifierService.getConfig();
74 | this.notifications = [];
75 |
76 | // Connects this component up to the action queue, then handle incoming actions
77 | this.queueServiceSubscription = this.queueService.actionStream.subscribe((action: NotifierAction) => {
78 | this.handleAction(action).then(() => {
79 | this.queueService.continue();
80 | });
81 | });
82 | }
83 |
84 | /**
85 | * Component destroyment lifecycle hook, cleans up the observable subsciption
86 | */
87 | public ngOnDestroy(): void {
88 | if (this.queueServiceSubscription) {
89 | this.queueServiceSubscription.unsubscribe();
90 | }
91 | }
92 |
93 | /**
94 | * Notification identifier, used as the ngFor trackby function
95 | *
96 | * @param index Index
97 | * @param notification Notifier notification
98 | * @returns Notification ID as the unique identnfier
99 | */
100 | public identifyNotification(index: number, notification: NotifierNotification): string {
101 | return notification.id;
102 | }
103 |
104 | /**
105 | * Event handler, handles clicks on notification dismiss buttons
106 | *
107 | * @param notificationId ID of the notification to dismiss
108 | */
109 | public onNotificationDismiss(notificationId: string): void {
110 | this.queueService.push({
111 | payload: notificationId,
112 | type: 'HIDE',
113 | });
114 | }
115 |
116 | /**
117 | * Event handler, handles notification ready events
118 | *
119 | * @param notificationComponent Notification component reference
120 | */
121 | public onNotificationReady(notificationComponent: NotifierNotificationComponent): void {
122 | const currentNotification: NotifierNotification = this.notifications[this.notifications.length - 1]; // Get the latest notification
123 | currentNotification.component = notificationComponent; // Save the new omponent reference
124 | this.continueHandleShowAction(currentNotification); // Continue with handling the show action
125 | }
126 |
127 | /**
128 | * Handle incoming actions by mapping action types to methods, and then running them
129 | *
130 | * @param action Action object
131 | * @returns Promise, resolved when done
132 | */
133 | private handleAction(action: NotifierAction): Promise {
134 | switch (
135 | action.type // TODO: Maybe a map (actionType -> class method) is a cleaner solution here?
136 | ) {
137 | case 'SHOW':
138 | return this.handleShowAction(action);
139 | case 'HIDE':
140 | return this.handleHideAction(action);
141 | case 'HIDE_OLDEST':
142 | return this.handleHideOldestAction(action);
143 | case 'HIDE_NEWEST':
144 | return this.handleHideNewestAction(action);
145 | case 'HIDE_ALL':
146 | return this.handleHideAllAction();
147 | default:
148 | return new Promise((resolve: () => void) => {
149 | resolve(); // Ignore unknown action types
150 | });
151 | }
152 | }
153 |
154 | /**
155 | * Show a new notification
156 | *
157 | * We simply add the notification to the list, and then wait until its properly initialized / created / rendered.
158 | *
159 | * @param action Action object
160 | * @returns Promise, resolved when done
161 | */
162 | private handleShowAction(action: NotifierAction): Promise {
163 | return new Promise((resolve: () => void) => {
164 | this.tempPromiseResolver = resolve; // Save the promise resolve function so that it can be called later on by another method
165 | this.addNotificationToList(new NotifierNotification(action.payload));
166 | });
167 | }
168 |
169 | /**
170 | * Continue to show a new notification (after the notification components is initialized / created / rendered).
171 | *
172 | * If this is the first (and thus only) notification, we can simply show it. Otherwhise, if stacking is disabled (or a low value), we
173 | * switch out notifications, in particular we hide the existing one, and then show our new one. Yet, if stacking is enabled, we first
174 | * shift all older notifications, and then show our new notification. In addition, if there are too many notification on the screen,
175 | * we hide the oldest one first. Furthermore, if configured, animation overlapping is applied.
176 | *
177 | * @param notification New notification to show
178 | */
179 | private continueHandleShowAction(notification: NotifierNotification): void {
180 | // First (which means only one) notification in the list?
181 | const numberOfNotifications: number = this.notifications.length;
182 | if (numberOfNotifications === 1) {
183 | notification.component.show().then(this.tempPromiseResolver); // Done
184 | } else {
185 | const implicitStackingLimit = 2;
186 |
187 | // Stacking enabled? (stacking value below 2 means stacking is disabled)
188 | if (this.config.behaviour.stacking === false || this.config.behaviour.stacking < implicitStackingLimit) {
189 | this.notifications[0].component.hide().then(() => {
190 | this.removeNotificationFromList(this.notifications[0]);
191 | notification.component.show().then(this.tempPromiseResolver); // Done
192 | });
193 | } else {
194 | const stepPromises: Array> = [];
195 |
196 | // Are there now too many notifications?
197 | if (numberOfNotifications > this.config.behaviour.stacking) {
198 | const oldNotifications: Array = this.notifications.slice(1, numberOfNotifications - 1);
199 |
200 | // Are animations enabled?
201 | if (this.config.animations.enabled) {
202 | // Is animation overlap enabled?
203 | if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {
204 | stepPromises.push(this.notifications[0].component.hide());
205 | setTimeout(() => {
206 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
207 | }, this.config.animations.hide.speed - this.config.animations.overlap);
208 | setTimeout(() => {
209 | stepPromises.push(notification.component.show());
210 | }, this.config.animations.hide.speed + this.config.animations.shift.speed - this.config.animations.overlap);
211 | } else {
212 | stepPromises.push(
213 | new Promise((resolve: () => void) => {
214 | this.notifications[0].component.hide().then(() => {
215 | this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => {
216 | notification.component.show().then(resolve);
217 | });
218 | });
219 | }),
220 | );
221 | }
222 | } else {
223 | stepPromises.push(this.notifications[0].component.hide());
224 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
225 | stepPromises.push(notification.component.show());
226 | }
227 | } else {
228 | const oldNotifications: Array = this.notifications.slice(0, numberOfNotifications - 1);
229 |
230 | // Are animations enabled?
231 | if (this.config.animations.enabled) {
232 | // Is animation overlap enabled?
233 | if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {
234 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
235 | setTimeout(() => {
236 | stepPromises.push(notification.component.show());
237 | }, this.config.animations.shift.speed - this.config.animations.overlap);
238 | } else {
239 | stepPromises.push(
240 | new Promise((resolve: () => void) => {
241 | this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => {
242 | notification.component.show().then(resolve);
243 | });
244 | }),
245 | );
246 | }
247 | } else {
248 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
249 | stepPromises.push(notification.component.show());
250 | }
251 | }
252 |
253 | Promise.all(stepPromises).then(() => {
254 | if (numberOfNotifications > this.config.behaviour.stacking) {
255 | this.removeNotificationFromList(this.notifications[0]);
256 | }
257 | this.tempPromiseResolver();
258 | }); // Done
259 | }
260 | }
261 | }
262 |
263 | /**
264 | * Hide an existing notification
265 | *
266 | * Fist, we skip everything if there are no notifications at all, or the given notification does not exist. Then, we hide the given
267 | * notification. If there exist older notifications, we then shift them around to fill the gap. Once both hiding the given notification
268 | * and shifting the older notificaitons is done, the given notification gets finally removed (from the DOM).
269 | *
270 | * @param action Action object, payload contains the notification ID
271 | * @returns Promise, resolved when done
272 | */
273 | private handleHideAction(action: NotifierAction): Promise {
274 | return new Promise((resolve: () => void) => {
275 | const stepPromises: Array> = [];
276 |
277 | // Does the notification exist / are there even any notifications? (let's prevent accidential errors)
278 | const notification: NotifierNotification | undefined = this.findNotificationById(action.payload);
279 | if (notification === undefined) {
280 | resolve();
281 | return;
282 | }
283 |
284 | // Get older notifications
285 | const notificationIndex: number | undefined = this.findNotificationIndexById(action.payload);
286 | if (notificationIndex === undefined) {
287 | resolve();
288 | return;
289 | }
290 | const oldNotifications: Array = this.notifications.slice(0, notificationIndex);
291 |
292 | // Do older notifications exist, and thus do we need to shift other notifications as a consequence?
293 | if (oldNotifications.length > 0) {
294 | // Are animations enabled?
295 | if (this.config.animations.enabled && this.config.animations.hide.speed > 0) {
296 | // Is animation overlap enabled?
297 | if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {
298 | stepPromises.push(notification.component.hide());
299 | setTimeout(() => {
300 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));
301 | }, this.config.animations.hide.speed - this.config.animations.overlap);
302 | } else {
303 | notification.component.hide().then(() => {
304 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));
305 | });
306 | }
307 | } else {
308 | stepPromises.push(notification.component.hide());
309 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));
310 | }
311 | } else {
312 | stepPromises.push(notification.component.hide());
313 | }
314 |
315 | // Wait until both hiding and shifting is done, then remove the notification from the list
316 | Promise.all(stepPromises).then(() => {
317 | this.removeNotificationFromList(notification);
318 | resolve(); // Done
319 | });
320 | });
321 | }
322 |
323 | /**
324 | * Hide the oldest notification (bridge to handleHideAction)
325 | *
326 | * @param action Action object
327 | * @returns Promise, resolved when done
328 | */
329 | private handleHideOldestAction(action: NotifierAction): Promise {
330 | // Are there any notifications? (prevent accidential errors)
331 | if (this.notifications.length === 0) {
332 | return new Promise((resolve: () => void) => {
333 | resolve();
334 | }); // Done
335 | } else {
336 | action.payload = this.notifications[0].id;
337 | return this.handleHideAction(action);
338 | }
339 | }
340 |
341 | /**
342 | * Hide the newest notification (bridge to handleHideAction)
343 | *
344 | * @param action Action object
345 | * @returns Promise, resolved when done
346 | */
347 | private handleHideNewestAction(action: NotifierAction): Promise {
348 | // Are there any notifications? (prevent accidential errors)
349 | if (this.notifications.length === 0) {
350 | return new Promise((resolve: () => void) => {
351 | resolve();
352 | }); // Done
353 | } else {
354 | action.payload = this.notifications[this.notifications.length - 1].id;
355 | return this.handleHideAction(action);
356 | }
357 | }
358 |
359 | /**
360 | * Hide all notifications at once
361 | *
362 | * @returns Promise, resolved when done
363 | */
364 | private handleHideAllAction(): Promise {
365 | return new Promise((resolve: () => void) => {
366 | // Are there any notifications? (prevent accidential errors)
367 | const numberOfNotifications: number = this.notifications.length;
368 | if (numberOfNotifications === 0) {
369 | resolve(); // Done
370 | return;
371 | }
372 |
373 | // Are animations enabled?
374 | if (
375 | this.config.animations.enabled &&
376 | this.config.animations.hide.speed > 0 &&
377 | this.config.animations.hide.offset !== false &&
378 | this.config.animations.hide.offset > 0
379 | ) {
380 | for (let i: number = numberOfNotifications - 1; i >= 0; i--) {
381 | const animationOffset: number = this.config.position.vertical.position === 'top' ? numberOfNotifications - 1 : i;
382 | setTimeout(() => {
383 | this.notifications[i].component.hide().then(() => {
384 | // Are we done here, was this the last notification to be hidden?
385 | if (
386 | (this.config.position.vertical.position === 'top' && i === 0) ||
387 | (this.config.position.vertical.position === 'bottom' && i === numberOfNotifications - 1)
388 | ) {
389 | this.removeAllNotificationsFromList();
390 | resolve(); // Done
391 | }
392 | });
393 | }, this.config.animations.hide.offset * animationOffset);
394 | }
395 | } else {
396 | const stepPromises: Array> = [];
397 | for (let i: number = numberOfNotifications - 1; i >= 0; i--) {
398 | stepPromises.push(this.notifications[i].component.hide());
399 | }
400 | Promise.all(stepPromises).then(() => {
401 | this.removeAllNotificationsFromList();
402 | resolve(); // Done
403 | });
404 | }
405 | });
406 | }
407 |
408 | /**
409 | * Shift multiple notifications at once
410 | *
411 | * @param notifications List containing the notifications to be shifted
412 | * @param distance Distance to shift (in px)
413 | * @param toMakePlace Flag, defining in which direciton to shift
414 | * @returns Promise, resolved when done
415 | */
416 | private shiftNotifications(notifications: Array, distance: number, toMakePlace: boolean): Promise {
417 | return new Promise((resolve: () => void) => {
418 | // Are there any notifications to shift?
419 | if (notifications.length === 0) {
420 | resolve();
421 | return;
422 | }
423 |
424 | const notificationPromises: Array> = [];
425 | for (let i: number = notifications.length - 1; i >= 0; i--) {
426 | notificationPromises.push(notifications[i].component.shift(distance, toMakePlace));
427 | }
428 | Promise.all(notificationPromises).then(resolve); // Done
429 | });
430 | }
431 |
432 | /**
433 | * Add a new notification to the list of notifications (triggers change detection)
434 | *
435 | * @param notification Notification to add to the list of notifications
436 | */
437 | private addNotificationToList(notification: NotifierNotification): void {
438 | this.notifications.push(notification);
439 | this.changeDetector.markForCheck(); // Run change detection because the notification list changed
440 | }
441 |
442 | /**
443 | * Remove an existing notification from the list of notifications (triggers change detection)
444 | *
445 | * @param notification Notification to be removed from the list of notifications
446 | */
447 | private removeNotificationFromList(notification: NotifierNotification): void {
448 | this.notifications = this.notifications.filter((item: NotifierNotification) => item.component !== notification.component);
449 | this.changeDetector.markForCheck(); // Run change detection because the notification list changed
450 | }
451 |
452 | /**
453 | * Remove all notifications from the list (triggers change detection)
454 | */
455 | private removeAllNotificationsFromList(): void {
456 | this.notifications = [];
457 | this.changeDetector.markForCheck(); // Run change detection because the notification list changed
458 | }
459 |
460 | /**
461 | * Helper: Find a notification in the notification list by a given notification ID
462 | *
463 | * @param notificationId Notification ID, used for finding notification
464 | * @returns Notification, undefined if not found
465 | */
466 | private findNotificationById(notificationId: string): NotifierNotification | undefined {
467 | return this.notifications.find((currentNotification: NotifierNotification) => currentNotification.id === notificationId);
468 | }
469 |
470 | /**
471 | * Helper: Find a notification's index by a given notification ID
472 | *
473 | * @param notificationId Notification ID, used for finding a notification's index
474 | * @returns Notification index, undefined if not found
475 | */
476 | private findNotificationIndexById(notificationId: string): number | undefined {
477 | const notificationIndex: number = this.notifications.findIndex(
478 | (currentNotification: NotifierNotification) => currentNotification.id === notificationId,
479 | );
480 | return notificationIndex !== -1 ? notificationIndex : undefined;
481 | }
482 | }
483 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-notification.component.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {{ notification.message }}
10 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-notification.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, DebugElement, Injectable, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core';
2 | import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 |
5 | import { NotifierAnimationData } from '../models/notifier-animation.model';
6 | import { NotifierConfig } from '../models/notifier-config.model';
7 | import { NotifierNotification } from '../models/notifier-notification.model';
8 | import { NotifierConfigToken } from '../notifier.tokens';
9 | import { NotifierService } from '../services/notifier.service';
10 | import { NotifierAnimationService } from '../services/notifier-animation.service';
11 | import { NotifierTimerService } from '../services/notifier-timer.service';
12 | import { NotifierNotificationComponent } from './notifier-notification.component';
13 |
14 | /**
15 | * Notifier Notification Component - Unit Test
16 | */
17 | describe('Notifier Notification Component', () => {
18 | const fakeAnimation: any = {
19 | onfinish: () => null, // We only need this property to be actually mocked away
20 | };
21 |
22 | const testNotification: NotifierNotification = new NotifierNotification({
23 | id: 'ID_FAKE',
24 | message: 'Lorem ipsum dolor sit amet.',
25 | type: 'SUCCESS',
26 | });
27 |
28 | let componentFixture: ComponentFixture;
29 | let componentInstance: NotifierNotificationComponent;
30 |
31 | let timerService: MockNotifierTimerService;
32 |
33 | it('should instantiate', () => {
34 | // Setup test module
35 | beforeEachWithConfig(new NotifierConfig());
36 |
37 | expect(componentInstance).toBeDefined();
38 | });
39 |
40 | describe('(render)', () => {
41 | it('should render', () => {
42 | // Setup test module
43 | beforeEachWithConfig(new NotifierConfig());
44 |
45 | componentInstance.notification = testNotification;
46 | componentFixture.detectChanges();
47 |
48 | // Check the calculated values
49 | expect(componentInstance.getConfig()).toEqual(new NotifierConfig());
50 | expect(componentInstance.getHeight()).toBe(componentFixture.nativeElement.offsetHeight);
51 | expect(componentInstance.getWidth()).toBe(componentFixture.nativeElement.offsetWidth);
52 | expect(componentInstance.getShift()).toBe(0);
53 |
54 | // Check the template
55 | const messageElement: DebugElement = componentFixture.debugElement.query(By.css('.notifier__notification-message'));
56 | expect(messageElement.nativeElement.textContent).toContain(componentInstance.notification.message);
57 | const dismissButtonElement: DebugElement = componentFixture.debugElement.query(By.css('.notifier__notification-button'));
58 | expect(dismissButtonElement).not.toBeNull();
59 |
60 | // Check the class names
61 | const classNameType = `notifier__notification--${componentInstance.notification.type}`;
62 | expect(componentFixture.nativeElement.classList.contains(classNameType)).toBeTruthy();
63 | const classNameTheme = `notifier__notification--${componentInstance.getConfig().theme}`;
64 | expect(componentFixture.nativeElement.classList.contains(classNameTheme)).toBeTruthy();
65 | });
66 |
67 | it('should render the custom template if provided by the user', async(() => {
68 | // Setup test module
69 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
70 | position: {
71 | horizontal: {
72 | distance: 10,
73 | position: 'left',
74 | },
75 | vertical: {
76 | distance: 10,
77 | gap: 4,
78 | position: 'top',
79 | },
80 | },
81 | });
82 | beforeEachWithConfig(testNotifierConfig, false);
83 |
84 | const template = `{{notificationData.message}}
`;
85 |
86 | const testcmp = createTestComponent(template);
87 |
88 | // associate the templateref
89 | const myTestNotification = {
90 | ...testNotification,
91 | template: testcmp.componentInstance.currentTplRef,
92 | };
93 | expect(testcmp.componentInstance.currentTplRef).toBeDefined();
94 |
95 | componentFixture = TestBed.createComponent(NotifierNotificationComponent);
96 | componentInstance = componentFixture.componentInstance;
97 |
98 | componentInstance.notification = myTestNotification;
99 | componentFixture.detectChanges();
100 |
101 | // // assert
102 | expect(componentFixture.debugElement.query(By.css('div.custom-notification-body'))).not.toBeNull();
103 | expect(componentFixture.debugElement.query(By.css('div.custom-notification-body')).nativeElement.innerHTML).toBe(
104 | myTestNotification.message,
105 | );
106 | }));
107 |
108 | it('should render on the left', () => {
109 | // Setup test module
110 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
111 | position: {
112 | horizontal: {
113 | distance: 10,
114 | position: 'left',
115 | },
116 | vertical: {
117 | distance: 10,
118 | gap: 4,
119 | position: 'top',
120 | },
121 | },
122 | });
123 | beforeEachWithConfig(testNotifierConfig);
124 |
125 | componentInstance.notification = testNotification;
126 | componentFixture.detectChanges();
127 |
128 | // Check position
129 | expect(componentFixture.debugElement.styles['left']).toBe(`${testNotifierConfig.position.horizontal.distance}px`);
130 | });
131 |
132 | it('should render on the right', () => {
133 | // Setup test module
134 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
135 | position: {
136 | horizontal: {
137 | distance: 10,
138 | position: 'right',
139 | },
140 | vertical: {
141 | distance: 10,
142 | gap: 4,
143 | position: 'top',
144 | },
145 | },
146 | });
147 | beforeEachWithConfig(testNotifierConfig);
148 |
149 | componentInstance.notification = testNotification;
150 | componentFixture.detectChanges();
151 |
152 | // Check position
153 | expect(componentFixture.debugElement.styles['right']).toBe(`${testNotifierConfig.position.horizontal.distance}px`);
154 | });
155 |
156 | it('should render in the middle', () => {
157 | // Setup test module
158 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
159 | position: {
160 | horizontal: {
161 | distance: 10,
162 | position: 'middle',
163 | },
164 | vertical: {
165 | distance: 10,
166 | gap: 4,
167 | position: 'top',
168 | },
169 | },
170 | });
171 | beforeEachWithConfig(testNotifierConfig);
172 |
173 | componentInstance.notification = testNotification;
174 | componentFixture.detectChanges();
175 |
176 | // Check position
177 | expect(componentFixture.debugElement.styles['left']).toBe('50%');
178 | expect(componentFixture.debugElement.styles['transform']).toBe('translate3d( -50%, 0, 0 )');
179 | });
180 |
181 | it('should render on the top', () => {
182 | // Setup test module
183 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
184 | position: {
185 | horizontal: {
186 | distance: 10,
187 | position: 'left',
188 | },
189 | vertical: {
190 | distance: 10,
191 | gap: 4,
192 | position: 'top',
193 | },
194 | },
195 | });
196 | beforeEachWithConfig(testNotifierConfig);
197 |
198 | componentInstance.notification = testNotification;
199 | componentFixture.detectChanges();
200 |
201 | // Check position
202 | expect(componentFixture.debugElement.styles['top']).toBe(`${testNotifierConfig.position.vertical.distance}px`);
203 | });
204 |
205 | it('should render on the bottom', () => {
206 | // Setup test module
207 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
208 | position: {
209 | horizontal: {
210 | distance: 10,
211 | position: 'left',
212 | },
213 | vertical: {
214 | distance: 10,
215 | gap: 4,
216 | position: 'bottom',
217 | },
218 | },
219 | });
220 | beforeEachWithConfig(testNotifierConfig);
221 |
222 | componentInstance.notification = testNotification;
223 | componentFixture.detectChanges();
224 |
225 | // Check position
226 | expect(componentFixture.debugElement.styles['bottom']).toBe(`${testNotifierConfig.position.vertical.distance}px`);
227 | });
228 | });
229 |
230 | describe('(show)', () => {
231 | it('should show', fakeAsync(() => {
232 | // Setup test module
233 | beforeEachWithConfig(
234 | new NotifierConfig({
235 | animations: {
236 | enabled: false,
237 | },
238 | behaviour: {
239 | autoHide: false,
240 | },
241 | }),
242 | );
243 |
244 | componentInstance.notification = testNotification;
245 | componentFixture.detectChanges();
246 |
247 | const showCallback = jest.fn();
248 | componentInstance.show().then(showCallback);
249 | tick();
250 |
251 | expect(componentFixture.debugElement.styles['visibility']).toBe('visible');
252 | expect(showCallback).toHaveBeenCalled();
253 | }));
254 |
255 | it('should show (with animations)', fakeAsync(() => {
256 | // Setup test module
257 | beforeEachWithConfig(
258 | new NotifierConfig({
259 | behaviour: {
260 | autoHide: false,
261 | },
262 | }),
263 | );
264 |
265 | componentInstance.notification = testNotification;
266 | componentFixture.detectChanges();
267 |
268 | // Mock away the Web Animations API
269 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
270 | componentFixture.debugElement.styles['opacity'] = '1'; // Fake animation result
271 | return fakeAnimation;
272 | });
273 |
274 | const showCallback = jest.fn();
275 | componentInstance.show().then(showCallback);
276 | fakeAnimation.onfinish();
277 | tick();
278 |
279 | expect(componentFixture.debugElement.styles['visibility']).toBe('visible');
280 | expect(componentFixture.debugElement.styles['opacity']).toBe('1');
281 | expect(showCallback).toHaveBeenCalled();
282 | }));
283 | });
284 |
285 | describe('(hide)', () => {
286 | it('should hide', fakeAsync(() => {
287 | // Setup test module
288 | beforeEachWithConfig(
289 | new NotifierConfig({
290 | animations: {
291 | enabled: false,
292 | },
293 | behaviour: {
294 | autoHide: false,
295 | },
296 | }),
297 | );
298 |
299 | componentInstance.notification = testNotification;
300 | componentFixture.detectChanges();
301 |
302 | const hideCallback = jest.fn();
303 | componentInstance.hide().then(hideCallback);
304 | tick();
305 |
306 | expect(hideCallback).toHaveBeenCalled();
307 | }));
308 |
309 | it('should hide (with animations)', fakeAsync(() => {
310 | // Setup test module
311 | beforeEachWithConfig(
312 | new NotifierConfig({
313 | behaviour: {
314 | autoHide: false,
315 | },
316 | }),
317 | );
318 |
319 | componentInstance.notification = testNotification;
320 | componentFixture.detectChanges();
321 |
322 | // Mock away the Web Animations API
323 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
324 | componentFixture.debugElement.styles['opacity'] = '0'; // Fake animation result
325 | return fakeAnimation;
326 | });
327 |
328 | const hideCallback = jest.fn();
329 | componentInstance.hide().then(hideCallback);
330 | fakeAnimation.onfinish();
331 | tick();
332 |
333 | expect(componentFixture.debugElement.styles['opacity']).toBe('0');
334 | expect(hideCallback).toHaveBeenCalled();
335 | }));
336 | });
337 |
338 | describe('(shift)', () => {
339 | it('should shift to make place on top', fakeAsync(() => {
340 | // Setup test module
341 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
342 | animations: {
343 | enabled: false,
344 | },
345 | behaviour: {
346 | autoHide: false,
347 | },
348 | position: {
349 | horizontal: {
350 | distance: 12,
351 | position: 'left',
352 | },
353 | vertical: {
354 | distance: 12,
355 | gap: 10,
356 | position: 'top',
357 | },
358 | },
359 | });
360 | beforeEachWithConfig(testNotifierConfig);
361 |
362 | componentInstance.notification = testNotification;
363 | componentFixture.detectChanges();
364 |
365 | const shiftCallback = jest.fn();
366 | const shiftDistance = 100;
367 | componentInstance.shift(shiftDistance, true).then(shiftCallback);
368 | tick();
369 |
370 | expect(componentFixture.debugElement.styles['transform']).toBe(
371 | `translate3d( 0, ${shiftDistance + testNotifierConfig.position.vertical.gap}px, 0 )`,
372 | );
373 | expect(shiftCallback).toHaveBeenCalled();
374 | }));
375 |
376 | it('should shift to make place on top (with animations)', fakeAsync(() => {
377 | // Setup test module
378 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
379 | behaviour: {
380 | autoHide: false,
381 | },
382 | position: {
383 | horizontal: {
384 | distance: 12,
385 | position: 'left',
386 | },
387 | vertical: {
388 | distance: 12,
389 | gap: 10,
390 | position: 'top',
391 | },
392 | },
393 | });
394 | beforeEachWithConfig(testNotifierConfig);
395 |
396 | componentInstance.notification = testNotification;
397 | componentFixture.detectChanges();
398 |
399 | const shiftDistance = 100;
400 |
401 | // Mock away the Web Animations API
402 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
403 | componentFixture.debugElement.styles['transform'] = `translate3d( 0, ${
404 | shiftDistance + testNotifierConfig.position.vertical.gap
405 | }px, 0 )`; // Fake animation result
406 | return fakeAnimation;
407 | });
408 |
409 | const shiftCallback = jest.fn();
410 | componentInstance.shift(shiftDistance, true).then(shiftCallback);
411 | fakeAnimation.onfinish();
412 | tick();
413 |
414 | expect(componentFixture.debugElement.styles['transform']).toBe(
415 | `translate3d( 0, ${shiftDistance + testNotifierConfig.position.vertical.gap}px, 0 )`,
416 | );
417 | expect(shiftCallback).toHaveBeenCalled();
418 | }));
419 |
420 | it('should shift to make place on bottom', fakeAsync(() => {
421 | // Setup test module
422 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
423 | animations: {
424 | enabled: false,
425 | },
426 | behaviour: {
427 | autoHide: false,
428 | },
429 | position: {
430 | horizontal: {
431 | distance: 12,
432 | position: 'left',
433 | },
434 | vertical: {
435 | distance: 12,
436 | gap: 10,
437 | position: 'bottom',
438 | },
439 | },
440 | });
441 | beforeEachWithConfig(testNotifierConfig);
442 |
443 | componentInstance.notification = testNotification;
444 | componentFixture.detectChanges();
445 |
446 | const shiftCallback = jest.fn();
447 | const shiftDistance = 100;
448 | componentInstance.shift(shiftDistance, true).then(shiftCallback);
449 | tick();
450 |
451 | expect(componentFixture.debugElement.styles['transform']).toBe(
452 | `translate3d( 0, ${-shiftDistance - testNotifierConfig.position.vertical.gap}px, 0 )`,
453 | );
454 | expect(shiftCallback).toHaveBeenCalled();
455 | }));
456 |
457 | it('should shift to make place on bottom (with animations)', fakeAsync(() => {
458 | // Setup test module
459 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
460 | behaviour: {
461 | autoHide: false,
462 | },
463 | position: {
464 | horizontal: {
465 | distance: 12,
466 | position: 'left',
467 | },
468 | vertical: {
469 | distance: 12,
470 | gap: 10,
471 | position: 'bottom',
472 | },
473 | },
474 | });
475 | beforeEachWithConfig(testNotifierConfig);
476 |
477 | componentInstance.notification = testNotification;
478 | componentFixture.detectChanges();
479 |
480 | // Mock away the Web Animations API
481 | const shiftDistance = 100;
482 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
483 | componentFixture.debugElement.styles['transform'] = `translate3d( 0, ${
484 | -shiftDistance - testNotifierConfig.position.vertical.gap
485 | }px, 0 )`; // Fake animation result
486 | return fakeAnimation;
487 | });
488 |
489 | const shiftCallback = jest.fn();
490 | componentInstance.shift(shiftDistance, true).then(shiftCallback);
491 | fakeAnimation.onfinish();
492 | tick();
493 |
494 | expect(componentFixture.debugElement.styles['transform']).toBe(
495 | `translate3d( 0, ${-shiftDistance - testNotifierConfig.position.vertical.gap}px, 0 )`,
496 | );
497 | expect(shiftCallback).toHaveBeenCalled();
498 | }));
499 |
500 | it('should shift to fill place on top', fakeAsync(() => {
501 | // Setup test module
502 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
503 | animations: {
504 | enabled: false,
505 | },
506 | behaviour: {
507 | autoHide: false,
508 | },
509 | position: {
510 | horizontal: {
511 | distance: 12,
512 | position: 'left',
513 | },
514 | vertical: {
515 | distance: 12,
516 | gap: 10,
517 | position: 'top',
518 | },
519 | },
520 | });
521 | beforeEachWithConfig(testNotifierConfig);
522 |
523 | componentInstance.notification = testNotification;
524 | componentFixture.detectChanges();
525 |
526 | const shiftCallback = jest.fn();
527 | const shiftDistance = 100;
528 | componentInstance.shift(shiftDistance, false).then(shiftCallback);
529 | tick();
530 |
531 | expect(componentFixture.debugElement.styles['transform']).toBe(
532 | `translate3d( 0, ${-shiftDistance - testNotifierConfig.position.vertical.gap}px, 0 )`,
533 | );
534 | expect(shiftCallback).toHaveBeenCalled();
535 | }));
536 |
537 | it('should shift to fill place on top (with animations)', fakeAsync(() => {
538 | // Setup test module
539 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
540 | behaviour: {
541 | autoHide: false,
542 | },
543 | position: {
544 | horizontal: {
545 | distance: 12,
546 | position: 'left',
547 | },
548 | vertical: {
549 | distance: 12,
550 | gap: 10,
551 | position: 'top',
552 | },
553 | },
554 | });
555 | beforeEachWithConfig(testNotifierConfig);
556 |
557 | componentInstance.notification = testNotification;
558 | componentFixture.detectChanges();
559 |
560 | // Mock away the Web Animations API
561 | const shiftDistance = 100;
562 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
563 | componentFixture.debugElement.styles['transform'] = `translate3d( 0, ${
564 | -shiftDistance - testNotifierConfig.position.vertical.gap
565 | }px, 0 )`; // Fake animation result
566 | return fakeAnimation;
567 | });
568 |
569 | const shiftCallback = jest.fn();
570 | componentInstance.shift(shiftDistance, false).then(shiftCallback);
571 | fakeAnimation.onfinish();
572 | tick();
573 |
574 | expect(componentFixture.debugElement.styles['transform']).toBe(
575 | `translate3d( 0, ${0 - shiftDistance - testNotifierConfig.position.vertical.gap}px, 0 )`,
576 | );
577 | expect(shiftCallback).toHaveBeenCalled();
578 | }));
579 |
580 | it('should shift to fill place on bottom', fakeAsync(() => {
581 | // Setup test module
582 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
583 | animations: {
584 | enabled: false,
585 | },
586 | behaviour: {
587 | autoHide: false,
588 | },
589 | position: {
590 | horizontal: {
591 | distance: 12,
592 | position: 'left',
593 | },
594 | vertical: {
595 | distance: 12,
596 | gap: 10,
597 | position: 'bottom',
598 | },
599 | },
600 | });
601 | beforeEachWithConfig(testNotifierConfig);
602 |
603 | componentInstance.notification = testNotification;
604 | componentFixture.detectChanges();
605 |
606 | const shiftCallback = jest.fn();
607 | const shiftDistance = 100;
608 | componentInstance.shift(shiftDistance, false).then(shiftCallback);
609 | tick();
610 |
611 | expect(componentFixture.debugElement.styles['transform']).toBe(
612 | `translate3d( 0, ${shiftDistance + testNotifierConfig.position.vertical.gap}px, 0 )`,
613 | );
614 | expect(shiftCallback).toHaveBeenCalled();
615 | }));
616 |
617 | it('should shift to fill place on bottom (with animations)', fakeAsync(() => {
618 | // Setup test module
619 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
620 | behaviour: {
621 | autoHide: false,
622 | },
623 | position: {
624 | horizontal: {
625 | distance: 12,
626 | position: 'left',
627 | },
628 | vertical: {
629 | distance: 12,
630 | gap: 10,
631 | position: 'bottom',
632 | },
633 | },
634 | });
635 | beforeEachWithConfig(testNotifierConfig);
636 |
637 | componentInstance.notification = testNotification;
638 | componentFixture.detectChanges();
639 |
640 | // Mock away the Web Animations API
641 | const shiftDistance = 100;
642 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
643 | componentFixture.debugElement.styles['transform'] = `translate3d( 0, ${
644 | shiftDistance + testNotifierConfig.position.vertical.gap
645 | }px, 0 )`; // Fake animation result
646 | return fakeAnimation;
647 | });
648 |
649 | const shiftCallback = jest.fn();
650 | componentInstance.shift(shiftDistance, false).then(shiftCallback);
651 | fakeAnimation.onfinish();
652 | tick();
653 |
654 | expect(componentFixture.debugElement.styles['transform']).toBe(
655 | `translate3d( 0, ${shiftDistance + testNotifierConfig.position.vertical.gap}px, 0 )`,
656 | );
657 | expect(shiftCallback).toHaveBeenCalled();
658 | }));
659 |
660 | it('should shift to make place in the middle', fakeAsync(() => {
661 | // Setup test module
662 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
663 | animations: {
664 | enabled: false,
665 | },
666 | behaviour: {
667 | autoHide: false,
668 | },
669 | position: {
670 | horizontal: {
671 | distance: 12,
672 | position: 'middle',
673 | },
674 | vertical: {
675 | distance: 12,
676 | gap: 10,
677 | position: 'top',
678 | },
679 | },
680 | });
681 | beforeEachWithConfig(testNotifierConfig);
682 |
683 | componentInstance.notification = testNotification;
684 | componentFixture.detectChanges();
685 |
686 | const shiftCallback = jest.fn();
687 | const shiftDistance = 100;
688 | componentInstance.shift(shiftDistance, true).then(shiftCallback);
689 | tick();
690 |
691 | expect(componentFixture.debugElement.styles['transform']).toBe(
692 | `translate3d( -50%, ${shiftDistance + testNotifierConfig.position.vertical.gap}px, 0 )`,
693 | );
694 | expect(shiftCallback).toHaveBeenCalled();
695 | }));
696 |
697 | it('should shift to make place in the middle (with animations)', fakeAsync(() => {
698 | // Setup test module
699 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
700 | animations: {
701 | enabled: false,
702 | },
703 | behaviour: {
704 | autoHide: false,
705 | },
706 | position: {
707 | horizontal: {
708 | distance: 12,
709 | position: 'middle',
710 | },
711 | vertical: {
712 | distance: 12,
713 | gap: 10,
714 | position: 'top',
715 | },
716 | },
717 | });
718 | beforeEachWithConfig(testNotifierConfig);
719 |
720 | componentInstance.notification = testNotification;
721 | componentFixture.detectChanges();
722 |
723 | // Mock away the Web Animations API
724 | const shiftDistance = 100;
725 | jest.spyOn(componentFixture.nativeElement, 'animate').mockImplementation(() => {
726 | componentFixture.debugElement.styles['transform'] = `translate3d( -50%, ${
727 | shiftDistance + testNotifierConfig.position.vertical.gap
728 | }px, 0 )`; // Fake animation result
729 | return fakeAnimation;
730 | });
731 |
732 | const shiftCallback = jest.fn();
733 | componentInstance.shift(shiftDistance, true).then(shiftCallback);
734 | fakeAnimation.onfinish();
735 | tick();
736 |
737 | expect(componentFixture.debugElement.styles['transform']).toBe(
738 | `translate3d( -50%, ${shiftDistance + testNotifierConfig.position.vertical.gap}px, 0 )`,
739 | );
740 | expect(shiftCallback).toHaveBeenCalled();
741 | }));
742 | });
743 |
744 | describe('(behaviour)', () => {
745 | it('should hide automatically after timeout', fakeAsync(() => {
746 | // Setup test module
747 | beforeEachWithConfig(
748 | new NotifierConfig({
749 | animations: {
750 | enabled: false,
751 | },
752 | behaviour: {
753 | autoHide: 5000,
754 | },
755 | }),
756 | );
757 |
758 | componentInstance.notification = testNotification;
759 | componentFixture.detectChanges();
760 |
761 | componentInstance.show();
762 | jest.spyOn(componentInstance, 'onClickDismiss');
763 | tick();
764 |
765 | timerService.finishManually();
766 | tick();
767 |
768 | expect(componentInstance.onClickDismiss).toHaveBeenCalled();
769 | }));
770 |
771 | it('should hide after clicking the dismiss button', fakeAsync(() => {
772 | // Setup test module
773 | beforeEachWithConfig(
774 | new NotifierConfig({
775 | animations: {
776 | enabled: false,
777 | },
778 | behaviour: {
779 | autoHide: false,
780 | showDismissButton: true,
781 | },
782 | }),
783 | );
784 |
785 | componentInstance.notification = testNotification;
786 | componentFixture.detectChanges();
787 |
788 | componentInstance.show();
789 | jest.spyOn(componentInstance, 'onClickDismiss');
790 |
791 | const dismissButtonElement: DebugElement = componentFixture.debugElement.query(By.css('.notifier__notification-button'));
792 | dismissButtonElement.nativeElement.click(); // Emulate click event
793 | componentFixture.detectChanges();
794 |
795 | expect(componentInstance.onClickDismiss).toHaveBeenCalled();
796 | }));
797 |
798 | it('should hide after clicking on the notification', fakeAsync(() => {
799 | // Setup test module
800 | beforeEachWithConfig(
801 | new NotifierConfig({
802 | animations: {
803 | enabled: false,
804 | },
805 | behaviour: {
806 | autoHide: false,
807 | onClick: 'hide',
808 | },
809 | }),
810 | );
811 |
812 | componentInstance.notification = testNotification;
813 | componentFixture.detectChanges();
814 |
815 | componentInstance.show();
816 | jest.spyOn(componentInstance, 'onClickDismiss');
817 |
818 | componentFixture.nativeElement.click(); // Emulate click event
819 | componentFixture.detectChanges();
820 |
821 | expect(componentInstance.onClickDismiss).toHaveBeenCalled();
822 | }));
823 |
824 | it('should not hide after clicking on the notification', fakeAsync(() => {
825 | // Setup test module
826 | beforeEachWithConfig(
827 | new NotifierConfig({
828 | animations: {
829 | enabled: false,
830 | },
831 | behaviour: {
832 | autoHide: false,
833 | onClick: false,
834 | },
835 | }),
836 | );
837 |
838 | componentInstance.notification = testNotification;
839 | componentFixture.detectChanges();
840 |
841 | componentInstance.show();
842 | jest.spyOn(componentInstance, 'onClickDismiss');
843 |
844 | componentFixture.nativeElement.click(); // Emulate click event
845 | componentFixture.detectChanges();
846 |
847 | expect(componentInstance.onClickDismiss).not.toHaveBeenCalled();
848 | }));
849 |
850 | it('should pause the autoHide timer on mouseover, and resume again on mouseout', fakeAsync(() => {
851 | // Setup test module
852 | beforeEachWithConfig(
853 | new NotifierConfig({
854 | animations: {
855 | enabled: false,
856 | },
857 | behaviour: {
858 | autoHide: 5000,
859 | onMouseover: 'pauseAutoHide',
860 | },
861 | }),
862 | );
863 |
864 | componentInstance.notification = testNotification;
865 | componentFixture.detectChanges();
866 |
867 | componentInstance.show();
868 | jest.spyOn(componentInstance, 'onClickDismiss');
869 | jest.spyOn(timerService, 'pause');
870 | jest.spyOn(timerService, 'continue');
871 |
872 | componentInstance.onNotificationMouseover();
873 |
874 | expect(timerService.pause).toHaveBeenCalled();
875 |
876 | componentInstance.onNotificationMouseout();
877 |
878 | expect(timerService.continue).toHaveBeenCalled();
879 |
880 | timerService.finishManually();
881 | tick();
882 |
883 | expect(componentInstance.onClickDismiss).toHaveBeenCalled();
884 | }));
885 |
886 | it('should restart the autoHide timer on mouseover', fakeAsync(() => {
887 | // Setup test module
888 | beforeEachWithConfig(
889 | new NotifierConfig({
890 | animations: {
891 | enabled: false,
892 | },
893 | behaviour: {
894 | autoHide: 5000,
895 | onMouseover: 'resetAutoHide',
896 | },
897 | }),
898 | );
899 |
900 | componentInstance.notification = testNotification;
901 | componentFixture.detectChanges();
902 |
903 | componentInstance.show();
904 | jest.spyOn(componentInstance, 'onClickDismiss');
905 | jest.spyOn(timerService, 'stop');
906 | jest.spyOn(timerService, 'start');
907 |
908 | componentInstance.onNotificationMouseover();
909 |
910 | expect(timerService.stop).toHaveBeenCalled();
911 |
912 | componentInstance.onNotificationMouseout();
913 |
914 | expect(timerService.start).toHaveBeenCalled();
915 |
916 | timerService.finishManually();
917 | tick();
918 |
919 | expect(componentInstance.onClickDismiss).toHaveBeenCalled();
920 | }));
921 | });
922 |
923 | /**
924 | * Helper for upfront configuration
925 | */
926 | function beforeEachWithConfig(testNotifierConfig: NotifierConfig, extractServices = true): void {
927 | TestBed.configureTestingModule({
928 | declarations: [NotifierNotificationComponent, TestComponent],
929 | providers: [
930 | {
931 | provide: NotifierService,
932 | useValue: {
933 | getConfig: () => testNotifierConfig,
934 | },
935 | },
936 | {
937 | // No idea why this is *actually* necessary -- it shouldn't be ...
938 | provide: NotifierConfigToken,
939 | useValue: {},
940 | },
941 | {
942 | provide: NotifierAnimationService,
943 | useClass: MockNotifierAnimationService,
944 | },
945 | ],
946 | }).overrideComponent(NotifierNotificationComponent, {
947 | set: {
948 | providers: [
949 | // Override component-specific providers
950 | {
951 | provide: NotifierTimerService,
952 | useClass: MockNotifierTimerService,
953 | },
954 | ],
955 | },
956 | });
957 |
958 | if (extractServices) {
959 | componentFixture = TestBed.createComponent(NotifierNotificationComponent);
960 | componentInstance = componentFixture.componentInstance;
961 |
962 | // Get the service from the component's local injector
963 | timerService = componentFixture.debugElement.injector.get(NotifierTimerService);
964 | }
965 | }
966 | });
967 |
968 | /**
969 | * Mock notifier animation service, always returning the animation
970 | */
971 | @Injectable()
972 | class MockNotifierAnimationService extends NotifierAnimationService {
973 | /**
974 | * Get animation data
975 | *
976 | * @param {'show' | 'hide'} direction Animation direction, either in or out
977 | * @returns {NotifierAnimationData} Animation information
978 | *
979 | * @override
980 | */
981 | public getAnimationData(direction: 'show' | 'hide'): NotifierAnimationData {
982 | if (direction === 'show') {
983 | return {
984 | keyframes: [
985 | {
986 | opacity: '0',
987 | },
988 | {
989 | opacity: '1',
990 | },
991 | ],
992 | options: {
993 | duration: 300,
994 | easing: 'ease',
995 | fill: 'forwards',
996 | },
997 | };
998 | } else {
999 | return {
1000 | keyframes: [
1001 | {
1002 | opacity: '1',
1003 | },
1004 | {
1005 | opacity: '0',
1006 | },
1007 | ],
1008 | options: {
1009 | duration: 300,
1010 | easing: 'ease',
1011 | fill: 'forwards',
1012 | },
1013 | };
1014 | }
1015 | }
1016 | }
1017 |
1018 | /**
1019 | * Mock Notifier Timer Service
1020 | */
1021 | @Injectable()
1022 | class MockNotifierTimerService extends NotifierTimerService {
1023 | /**
1024 | * Temp resolve function
1025 | *
1026 | * @override
1027 | */
1028 | private resolveFunction: () => void;
1029 |
1030 | /**
1031 | * Start (or resume) the timer - doing nothing here
1032 | *
1033 | * @param {number} duration Timer duration, in ms
1034 | * @returns {Promise} Promise, resolved once the timer finishes
1035 | *
1036 | * @override
1037 | */
1038 | public start(): Promise {
1039 | return new Promise((resolve: () => void) => {
1040 | this.resolveFunction = resolve;
1041 | });
1042 | }
1043 |
1044 | /**
1045 | * Pause the timer - doing nothing here
1046 | */
1047 | public pause(): void {
1048 | // Do nothing
1049 | }
1050 |
1051 | /**
1052 | * Continue the timer - doing nothing here
1053 | */
1054 | public continue(): void {
1055 | // Do nothing
1056 | }
1057 |
1058 | /**
1059 | * Stop the timer - doing nothing here
1060 | */
1061 | public stop(): void {
1062 | // Do nothing
1063 | }
1064 |
1065 | /**
1066 | * Finish the timer manually, from outside
1067 | */
1068 | public finishManually(): void {
1069 | this.resolveFunction();
1070 | }
1071 | }
1072 |
1073 | @Component({ selector: 'test-cmp', template: '' })
1074 | class TestComponent {
1075 | @ViewChild('tpl', { static: true })
1076 | currentTplRef: TemplateRef;
1077 | }
1078 |
1079 | function createTestComponent(template: string): ComponentFixture {
1080 | return TestBed.overrideComponent(TestComponent, { set: { template: template } })
1081 | .configureTestingModule({ schemas: [NO_ERRORS_SCHEMA] })
1082 | .createComponent(TestComponent);
1083 | }
1084 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-notification.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core';
2 |
3 | import { NotifierAnimationData } from '../models/notifier-animation.model';
4 | import { NotifierConfig } from '../models/notifier-config.model';
5 | import { NotifierNotification } from '../models/notifier-notification.model';
6 | import { NotifierService } from '../services/notifier.service';
7 | import { NotifierAnimationService } from '../services/notifier-animation.service';
8 | import { NotifierTimerService } from '../services/notifier-timer.service';
9 |
10 | /**
11 | * Notifier notification component
12 | * -------------------------------
13 | * This component is responsible for actually displaying the notification on screen. In addition, it's able to show and hide this
14 | * notification, in particular to animate this notification in and out, as well as shift (move) this notification vertically around.
15 | * Furthermore, the notification component handles all interactions the user has with this notification / component, such as clicks and
16 | * mouse movements.
17 | */
18 | @Component({
19 | changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters)
20 | host: {
21 | '(click)': 'onNotificationClick()',
22 | '(mouseout)': 'onNotificationMouseout()',
23 | '(mouseover)': 'onNotificationMouseover()',
24 | class: 'notifier__notification',
25 | },
26 | providers: [
27 | // We provide the timer to the component's local injector, so that every notification components gets its own
28 | // instance of the timer service, thus running their timers independently from each other
29 | NotifierTimerService,
30 | ],
31 | selector: 'notifier-notification',
32 | templateUrl: './notifier-notification.component.html',
33 | })
34 | export class NotifierNotificationComponent implements AfterViewInit {
35 | /**
36 | * Input: Notification object, contains all details necessary to construct the notification
37 | */
38 | @Input()
39 | public notification: NotifierNotification;
40 |
41 | /**
42 | * Output: Ready event, handles the initialization success by emitting a reference to this notification component
43 | */
44 | @Output()
45 | public ready: EventEmitter;
46 |
47 | /**
48 | * Output: Dismiss event, handles the click on the dismiss button by emitting the notification ID of this notification component
49 | */
50 | @Output()
51 | public dismiss: EventEmitter;
52 |
53 | /**
54 | * Notifier configuration
55 | */
56 | public readonly config: NotifierConfig;
57 |
58 | /**
59 | * Notifier timer service
60 | */
61 | private readonly timerService: NotifierTimerService;
62 |
63 | /**
64 | * Notifier animation service
65 | */
66 | private readonly animationService: NotifierAnimationService;
67 |
68 | /**
69 | * Angular renderer, used to preserve the overall DOM abstraction & independence
70 | */
71 | private readonly renderer: Renderer2;
72 |
73 | /**
74 | * Native element reference, used for manipulating DOM properties
75 | */
76 | private readonly element: HTMLElement;
77 |
78 | /**
79 | * Current notification height, calculated and cached here (#perfmatters)
80 | */
81 | private elementHeight: number;
82 |
83 | /**
84 | * Current notification width, calculated and cached here (#perfmatters)
85 | */
86 | private elementWidth: number;
87 |
88 | /**
89 | * Current notification shift, calculated and cached here (#perfmatters)
90 | */
91 | private elementShift: number;
92 |
93 | /**
94 | * Constructor
95 | *
96 | * @param elementRef Reference to the component's element
97 | * @param renderer Angular renderer
98 | * @param notifierService Notifier service
99 | * @param notifierTimerService Notifier timer service
100 | * @param notifierAnimationService Notifier animation service
101 | */
102 | public constructor(
103 | elementRef: ElementRef,
104 | renderer: Renderer2,
105 | notifierService: NotifierService,
106 | notifierTimerService: NotifierTimerService,
107 | notifierAnimationService: NotifierAnimationService,
108 | ) {
109 | this.config = notifierService.getConfig();
110 | this.ready = new EventEmitter();
111 | this.dismiss = new EventEmitter();
112 | this.timerService = notifierTimerService;
113 | this.animationService = notifierAnimationService;
114 | this.renderer = renderer;
115 | this.element = elementRef.nativeElement;
116 | this.elementShift = 0;
117 | }
118 |
119 | /**
120 | * Component after view init lifecycle hook, setts up the component and then emits the ready event
121 | */
122 | public ngAfterViewInit(): void {
123 | this.setup();
124 | this.elementHeight = this.element.offsetHeight;
125 | this.elementWidth = this.element.offsetWidth;
126 | this.ready.emit(this);
127 | }
128 |
129 | /**
130 | * Get the notifier config
131 | *
132 | * @returns Notifier configuration
133 | */
134 | public getConfig(): NotifierConfig {
135 | return this.config;
136 | }
137 |
138 | /**
139 | * Get notification element height (in px)
140 | *
141 | * @returns Notification element height (in px)
142 | */
143 | public getHeight(): number {
144 | return this.elementHeight;
145 | }
146 |
147 | /**
148 | * Get notification element width (in px)
149 | *
150 | * @returns Notification element height (in px)
151 | */
152 | public getWidth(): number {
153 | return this.elementWidth;
154 | }
155 |
156 | /**
157 | * Get notification shift offset (in px)
158 | *
159 | * @returns Notification element shift offset (in px)
160 | */
161 | public getShift(): number {
162 | return this.elementShift;
163 | }
164 |
165 | /**
166 | * Show (animate in) this notification
167 | *
168 | * @returns Promise, resolved when done
169 | */
170 | public show(): Promise {
171 | return new Promise((resolve: () => void) => {
172 | // Are animations enabled?
173 | if (this.config.animations.enabled && this.config.animations.show.speed > 0) {
174 | // Get animation data
175 | const animationData: NotifierAnimationData = this.animationService.getAnimationData('show', this.notification);
176 |
177 | // Set initial styles (styles before animation), prevents quick flicker when animation starts
178 | const animatedProperties: Array = Object.keys(animationData.keyframes[0]);
179 | for (let i: number = animatedProperties.length - 1; i >= 0; i--) {
180 | this.renderer.setStyle(this.element, animatedProperties[i], animationData.keyframes[0][animatedProperties[i]]);
181 | }
182 |
183 | // Animate notification in
184 | this.renderer.setStyle(this.element, 'visibility', 'visible');
185 | const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);
186 | animation.onfinish = () => {
187 | this.startAutoHideTimer();
188 | resolve(); // Done
189 | };
190 | } else {
191 | // Show notification
192 | this.renderer.setStyle(this.element, 'visibility', 'visible');
193 | this.startAutoHideTimer();
194 | resolve(); // Done
195 | }
196 | });
197 | }
198 |
199 | /**
200 | * Hide (animate out) this notification
201 | *
202 | * @returns Promise, resolved when done
203 | */
204 | public hide(): Promise {
205 | return new Promise((resolve: () => void) => {
206 | this.stopAutoHideTimer();
207 |
208 | // Are animations enabled?
209 | if (this.config.animations.enabled && this.config.animations.hide.speed > 0) {
210 | const animationData: NotifierAnimationData = this.animationService.getAnimationData('hide', this.notification);
211 | const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);
212 | animation.onfinish = () => {
213 | resolve(); // Done
214 | };
215 | } else {
216 | resolve(); // Done
217 | }
218 | });
219 | }
220 |
221 | /**
222 | * Shift (move) this notification
223 | *
224 | * @param distance Distance to shift (in px)
225 | * @param shiftToMakePlace Flag, defining in which direction to shift
226 | * @returns Promise, resolved when done
227 | */
228 | public shift(distance: number, shiftToMakePlace: boolean): Promise {
229 | return new Promise((resolve: () => void) => {
230 | // Calculate new position (position after the shift)
231 | let newElementShift: number;
232 | if (
233 | (this.config.position.vertical.position === 'top' && shiftToMakePlace) ||
234 | (this.config.position.vertical.position === 'bottom' && !shiftToMakePlace)
235 | ) {
236 | newElementShift = this.elementShift + distance + this.config.position.vertical.gap;
237 | } else {
238 | newElementShift = this.elementShift - distance - this.config.position.vertical.gap;
239 | }
240 | const horizontalPosition: string = this.config.position.horizontal.position === 'middle' ? '-50%' : '0';
241 |
242 | // Are animations enabled?
243 | if (this.config.animations.enabled && this.config.animations.shift.speed > 0) {
244 | const animationData: NotifierAnimationData = {
245 | // TODO: Extract into animation service
246 | keyframes: [
247 | {
248 | transform: `translate3d( ${horizontalPosition}, ${this.elementShift}px, 0 )`,
249 | },
250 | {
251 | transform: `translate3d( ${horizontalPosition}, ${newElementShift}px, 0 )`,
252 | },
253 | ],
254 | options: {
255 | duration: this.config.animations.shift.speed,
256 | easing: this.config.animations.shift.easing,
257 | fill: 'forwards',
258 | },
259 | };
260 | this.elementShift = newElementShift;
261 | const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);
262 | animation.onfinish = () => {
263 | resolve(); // Done
264 | };
265 | } else {
266 | this.renderer.setStyle(this.element, 'transform', `translate3d( ${horizontalPosition}, ${newElementShift}px, 0 )`);
267 | this.elementShift = newElementShift;
268 | resolve(); // Done
269 | }
270 | });
271 | }
272 |
273 | /**
274 | * Handle click on dismiss button
275 | */
276 | public onClickDismiss(): void {
277 | this.dismiss.emit(this.notification.id);
278 | }
279 |
280 | /**
281 | * Handle mouseover over notification area
282 | */
283 | public onNotificationMouseover(): void {
284 | if (this.config.behaviour.onMouseover === 'pauseAutoHide') {
285 | this.pauseAutoHideTimer();
286 | } else if (this.config.behaviour.onMouseover === 'resetAutoHide') {
287 | this.stopAutoHideTimer();
288 | }
289 | }
290 |
291 | /**
292 | * Handle mouseout from notification area
293 | */
294 | public onNotificationMouseout(): void {
295 | if (this.config.behaviour.onMouseover === 'pauseAutoHide') {
296 | this.continueAutoHideTimer();
297 | } else if (this.config.behaviour.onMouseover === 'resetAutoHide') {
298 | this.startAutoHideTimer();
299 | }
300 | }
301 |
302 | /**
303 | * Handle click on notification area
304 | */
305 | public onNotificationClick(): void {
306 | if (this.config.behaviour.onClick === 'hide') {
307 | this.onClickDismiss();
308 | }
309 | }
310 |
311 | /**
312 | * Start the auto hide timer (if enabled)
313 | */
314 | private startAutoHideTimer(): void {
315 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
316 | this.timerService.start(this.config.behaviour.autoHide).then(() => {
317 | this.onClickDismiss();
318 | });
319 | }
320 | }
321 |
322 | /**
323 | * Pause the auto hide timer (if enabled)
324 | */
325 | private pauseAutoHideTimer(): void {
326 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
327 | this.timerService.pause();
328 | }
329 | }
330 |
331 | /**
332 | * Continue the auto hide timer (if enabled)
333 | */
334 | private continueAutoHideTimer(): void {
335 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
336 | this.timerService.continue();
337 | }
338 | }
339 |
340 | /**
341 | * Stop the auto hide timer (if enabled)
342 | */
343 | private stopAutoHideTimer(): void {
344 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
345 | this.timerService.stop();
346 | }
347 | }
348 |
349 | /**
350 | * Initial notification setup
351 | */
352 | private setup(): void {
353 | // Set start position (initially the exact same for every new notification)
354 | if (this.config.position.horizontal.position === 'left') {
355 | this.renderer.setStyle(this.element, 'left', `${this.config.position.horizontal.distance}px`);
356 | } else if (this.config.position.horizontal.position === 'right') {
357 | this.renderer.setStyle(this.element, 'right', `${this.config.position.horizontal.distance}px`);
358 | } else {
359 | this.renderer.setStyle(this.element, 'left', '50%');
360 | // Let's get the GPU handle some work as well (#perfmatters)
361 | this.renderer.setStyle(this.element, 'transform', 'translate3d( -50%, 0, 0 )');
362 | }
363 | if (this.config.position.vertical.position === 'top') {
364 | this.renderer.setStyle(this.element, 'top', `${this.config.position.vertical.distance}px`);
365 | } else {
366 | this.renderer.setStyle(this.element, 'bottom', `${this.config.position.vertical.distance}px`);
367 | }
368 |
369 | // Add classes (responsible for visual design)
370 | this.renderer.addClass(this.element, `notifier__notification--${this.notification.type}`);
371 | this.renderer.addClass(this.element, `notifier__notification--${this.config.theme}`);
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-action.model.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Notifier action
3 | *
4 | * In general, API calls don't get processed right away. Instead, we have to queue them up in order to prevent simultanious API calls
5 | * interfering with each other. This, at least in theory, is possible at any time, primarily due to overlapping animations.
6 | *
7 | * Technical sidenote:
8 | * An action looks pretty similar to the ones within the Flux / Redux pattern.
9 | */
10 | export interface NotifierAction {
11 | /**
12 | * Action payload containing all information necessary to process the action (optional)
13 | */
14 | payload?: any;
15 |
16 | /**
17 | * Action type
18 | */
19 | type: 'SHOW' | 'HIDE' | 'HIDE_ALL' | 'HIDE_NEWEST' | 'HIDE_OLDEST';
20 | }
21 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-animation.model.ts:
--------------------------------------------------------------------------------
1 | import { NotifierNotification } from './notifier-notification.model';
2 |
3 | /**
4 | * Notifier animation data
5 | *
6 | * This interface describes an object containing all information necessary to run an animation, in particular to run an animation with the
7 | * all new (shiny) Web Animations API. When other components or services request data for an animation they have to run, this is the object
8 | * they get back from the animation service.
9 | *
10 | * Technical sidenote:
11 | * Nope, it's not a coincidence - the structure looks similar to the Web Animation API syntax.
12 | */
13 | export interface NotifierAnimationData {
14 | /**
15 | * Animation keyframes; the first index ctonaining changes for animate-in, the second index those for animate-out
16 | */
17 | keyframes: Array<{
18 | [animatablePropertyName: string]: string;
19 | }>;
20 |
21 | /**
22 | * Futher animation options
23 | */
24 | options: {
25 | /**
26 | * Animation duration, in ms
27 | */
28 | duration: number;
29 |
30 | /**
31 | * Animation easing function (comp. CSS easing functions)
32 | */
33 | easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | string;
34 |
35 | /**
36 | * Animation fill mode
37 | */
38 | fill: 'none' | 'forwards' | 'backwards';
39 | };
40 | }
41 |
42 | /**
43 | * Notifier animation preset
44 | *
45 | * This interface describes the structure of an animation preset, defining the keyframes for both animating-in and animating-out. Animation
46 | * presets are always defined outside the animation service, and therefore one day may become part of some new API.
47 | */
48 | export interface NotifierAnimationPreset {
49 | /**
50 | * Function generating the keyframes for animating-out
51 | */
52 | hide: (notification: NotifierNotification) => NotifierAnimationPresetKeyframes;
53 |
54 | /**
55 | * Function generating the keyframes for animating-in
56 | */
57 | show: (notification: NotifierNotification) => NotifierAnimationPresetKeyframes;
58 | }
59 |
60 | /**
61 | * Notifier animation keyframes
62 | *
63 | * This interface describes the data, in particular all the keyframes animation presets return.
64 | */
65 | export interface NotifierAnimationPresetKeyframes {
66 | /**
67 | * CSS attributes before the animation starts
68 | */
69 | from: {
70 | [animatablePropertyName: string]: string;
71 | };
72 |
73 | /**
74 | * CSS attributes after the animation ends
75 | */
76 | to: {
77 | [animatablePropertyName: string]: string;
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-config.model.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierConfig } from './notifier-config.model';
2 |
3 | /**
4 | * Notifier Configuration - Unit Test
5 | */
6 | describe('Notifier Configuration', () => {
7 | it('should initialize with the default configuration', () => {
8 | const testNotifierConfig: NotifierConfig = new NotifierConfig();
9 | const expectedNotifierConfig: NotifierConfig = new NotifierConfig({
10 | animations: {
11 | enabled: true,
12 | hide: {
13 | easing: 'ease',
14 | offset: 50,
15 | preset: 'fade',
16 | speed: 300,
17 | },
18 | overlap: 150,
19 | shift: {
20 | easing: 'ease',
21 | speed: 300,
22 | },
23 | show: {
24 | easing: 'ease',
25 | preset: 'slide',
26 | speed: 300,
27 | },
28 | },
29 | behaviour: {
30 | autoHide: 7000,
31 | onClick: false,
32 | onMouseover: 'pauseAutoHide',
33 | showDismissButton: true,
34 | stacking: 4,
35 | },
36 | position: {
37 | horizontal: {
38 | distance: 12,
39 | position: 'left',
40 | },
41 | vertical: {
42 | distance: 12,
43 | gap: 10,
44 | position: 'bottom',
45 | },
46 | },
47 | theme: 'material',
48 | });
49 |
50 | expect(testNotifierConfig).toEqual(expectedNotifierConfig);
51 | });
52 |
53 | it('should override custom bits of the configuration', () => {
54 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
55 | animations: {
56 | hide: {
57 | easing: 'ease-in-out',
58 | },
59 | overlap: 100,
60 | shift: {
61 | speed: 200,
62 | },
63 | },
64 | behaviour: {
65 | autoHide: 5000,
66 | stacking: 7,
67 | },
68 | position: {
69 | horizontal: {
70 | distance: 20,
71 | },
72 | },
73 | theme: 'my-custom-theme',
74 | });
75 | const expectedNotifierConfig: NotifierConfig = new NotifierConfig({
76 | animations: {
77 | enabled: true,
78 | hide: {
79 | easing: 'ease-in-out',
80 | offset: 50,
81 | preset: 'fade',
82 | speed: 300,
83 | },
84 | overlap: 100,
85 | shift: {
86 | easing: 'ease',
87 | speed: 200,
88 | },
89 | show: {
90 | easing: 'ease',
91 | preset: 'slide',
92 | speed: 300,
93 | },
94 | },
95 | behaviour: {
96 | autoHide: 5000,
97 | onClick: false,
98 | onMouseover: 'pauseAutoHide',
99 | showDismissButton: true,
100 | stacking: 7,
101 | },
102 | position: {
103 | horizontal: {
104 | distance: 20,
105 | position: 'left',
106 | },
107 | vertical: {
108 | distance: 12,
109 | gap: 10,
110 | position: 'bottom',
111 | },
112 | },
113 | theme: 'my-custom-theme',
114 | });
115 |
116 | expect(testNotifierConfig).toEqual(expectedNotifierConfig);
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-config.model.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Notifier options
3 | */
4 | export interface NotifierOptions {
5 | animations?: {
6 | enabled?: boolean;
7 | hide?: {
8 | easing?: string;
9 | offset?: number | false;
10 | preset?: string;
11 | speed?: number;
12 | };
13 | overlap?: number | false;
14 | shift?: {
15 | easing?: string;
16 | speed?: number;
17 | };
18 | show?: {
19 | easing?: string;
20 | preset?: string;
21 | speed?: number;
22 | };
23 | };
24 | behaviour?: {
25 | autoHide?: number | false;
26 | onClick?: 'hide' | false;
27 | onMouseover?: 'pauseAutoHide' | 'resetAutoHide' | false;
28 | showDismissButton?: boolean;
29 | stacking?: number | false;
30 | };
31 | position?: {
32 | horizontal?: {
33 | distance?: number;
34 | position?: 'left' | 'middle' | 'right';
35 | };
36 | vertical?: {
37 | distance?: number;
38 | gap?: number;
39 | position?: 'top' | 'bottom';
40 | };
41 | };
42 | theme?: string;
43 | }
44 |
45 | /**
46 | * Notifier configuration
47 | *
48 | * The notifier configuration defines what notifications look like, how they behave, and how they get animated. It is a global
49 | * configuration, which means that it only can be set once (at the beginning), and cannot be changed afterwards. Aligning to the world of
50 | * Angular, this configuration can be provided in the root app module - alternatively, a meaningful default configuration will be used.
51 | */
52 | export class NotifierConfig implements NotifierOptions {
53 | /**
54 | * Customize animations
55 | */
56 | public animations: {
57 | enabled: boolean;
58 | hide: {
59 | easing: string;
60 | offset: number | false;
61 | preset: string;
62 | speed: number;
63 | };
64 | overlap: number | false;
65 | shift: {
66 | easing: string;
67 | speed: number;
68 | };
69 | show: {
70 | easing: string;
71 | preset: string;
72 | speed: number;
73 | };
74 | };
75 |
76 | /**
77 | * Customize behaviour
78 | */
79 | public behaviour: {
80 | autoHide: number | false;
81 | onClick: 'hide' | false;
82 | onMouseover: 'pauseAutoHide' | 'resetAutoHide' | false;
83 | showDismissButton: boolean;
84 | stacking: number | false;
85 | };
86 |
87 | /**
88 | * Customize positioning
89 | */
90 | public position: {
91 | horizontal: {
92 | distance: number;
93 | position: 'left' | 'middle' | 'right';
94 | };
95 | vertical: {
96 | distance: number;
97 | gap: number;
98 | position: 'top' | 'bottom';
99 | };
100 | };
101 |
102 | /**
103 | * Customize theming
104 | */
105 | public theme: string;
106 |
107 | /**
108 | * Constructor
109 | *
110 | * @param [customOptions={}] Custom notifier options, optional
111 | */
112 | public constructor(customOptions: NotifierOptions = {}) {
113 | // Set default values
114 | this.animations = {
115 | enabled: true,
116 | hide: {
117 | easing: 'ease',
118 | offset: 50,
119 | preset: 'fade',
120 | speed: 300,
121 | },
122 | overlap: 150,
123 | shift: {
124 | easing: 'ease',
125 | speed: 300,
126 | },
127 | show: {
128 | easing: 'ease',
129 | preset: 'slide',
130 | speed: 300,
131 | },
132 | };
133 | this.behaviour = {
134 | autoHide: 7000,
135 | onClick: false,
136 | onMouseover: 'pauseAutoHide',
137 | showDismissButton: true,
138 | stacking: 4,
139 | };
140 | this.position = {
141 | horizontal: {
142 | distance: 12,
143 | position: 'left',
144 | },
145 | vertical: {
146 | distance: 12,
147 | gap: 10,
148 | position: 'bottom',
149 | },
150 | };
151 | this.theme = 'material';
152 |
153 | // The following merges the custom options into the notifier config, respecting the already set default values
154 | // This linear, more explicit and code-sizy workflow is preferred here over a recursive one (because we know the object structure)
155 | // Technical sidenote: Objects are merged, other types of values simply overwritten / copied
156 | if (customOptions.theme !== undefined) {
157 | this.theme = customOptions.theme;
158 | }
159 | if (customOptions.animations !== undefined) {
160 | if (customOptions.animations.enabled !== undefined) {
161 | this.animations.enabled = customOptions.animations.enabled;
162 | }
163 | if (customOptions.animations.overlap !== undefined) {
164 | this.animations.overlap = customOptions.animations.overlap;
165 | }
166 | if (customOptions.animations.hide !== undefined) {
167 | Object.assign(this.animations.hide, customOptions.animations.hide);
168 | }
169 | if (customOptions.animations.shift !== undefined) {
170 | Object.assign(this.animations.shift, customOptions.animations.shift);
171 | }
172 | if (customOptions.animations.show !== undefined) {
173 | Object.assign(this.animations.show, customOptions.animations.show);
174 | }
175 | }
176 | if (customOptions.behaviour !== undefined) {
177 | Object.assign(this.behaviour, customOptions.behaviour);
178 | }
179 | if (customOptions.position !== undefined) {
180 | if (customOptions.position.horizontal !== undefined) {
181 | Object.assign(this.position.horizontal, customOptions.position.horizontal);
182 | }
183 | if (customOptions.position.vertical !== undefined) {
184 | Object.assign(this.position.vertical, customOptions.position.vertical);
185 | }
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-notification.model.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierNotification } from './notifier-notification.model';
2 |
3 | /**
4 | * Notifier Notification Model - Unit Test
5 | */
6 | describe('Notifier Notification Model', () => {
7 | const testNotificationType = 'SUCCESS';
8 | const testNotificationMessage = 'Lorem ipsum dolor sit amet.';
9 | const testNotificationId = 'FAKE_ID';
10 |
11 | it('should set the custom options', () => {
12 | const testNotifierNotification: NotifierNotification = new NotifierNotification({
13 | id: testNotificationId,
14 | message: testNotificationMessage,
15 | type: testNotificationType,
16 | });
17 |
18 | expect(testNotifierNotification.type).toBe(testNotificationType);
19 | expect(testNotifierNotification.message).toBe(testNotificationMessage);
20 | expect(testNotifierNotification.id).toBe(testNotificationId);
21 | });
22 |
23 | it('should generate a notification ID automatically, if not defined', () => {
24 | // Mock the date (as the ID generation is based on it)
25 | const mockDate: MockDate = new MockDate();
26 | jest.spyOn(window, 'Date').mockImplementation(() => mockDate);
27 |
28 | const testNotifierNotification: NotifierNotification = new NotifierNotification({
29 | message: testNotificationMessage,
30 | type: testNotificationType,
31 | });
32 |
33 | expect(testNotifierNotification.type).toBe(testNotificationType);
34 | expect(testNotifierNotification.message).toBe(testNotificationMessage);
35 | expect(testNotifierNotification.id).toBe(`ID_${mockDate.getTime()}`);
36 | });
37 | });
38 |
39 | /**
40 | * Mock current time
41 | */
42 | const mockCurrentTime = 1482312338350;
43 |
44 | /**
45 | * Mock Date
46 | */
47 | class MockDate extends Date {
48 | /**
49 | * Get the time - always returns the same value, useful for comparison
50 | */
51 | public getTime(): number {
52 | return mockCurrentTime;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-notification.model.ts:
--------------------------------------------------------------------------------
1 | import { TemplateRef } from '@angular/core';
2 |
3 | import { NotifierNotificationComponent } from '../components/notifier-notification.component';
4 |
5 | /**
6 | * Notification
7 | *
8 | * This class describes the structure of a notifiction, including all information it needs to live, and everyone else needs to work with it.
9 | */
10 | export class NotifierNotification {
11 | /**
12 | * Unique notification ID, can be set manually to control the notification from outside later on
13 | */
14 | public id: string;
15 |
16 | /**
17 | * Notification type, will be used for constructing an appropriate class name
18 | */
19 | public type: string;
20 |
21 | /**
22 | * Notification message
23 | */
24 | public message: string;
25 |
26 | /**
27 | * The template to customize
28 | * the appearance of the notification
29 | */
30 | public template?: TemplateRef = null;
31 |
32 | /**
33 | * Component reference of this notification, created and set during creation time
34 | */
35 | public component: NotifierNotificationComponent;
36 |
37 | /**
38 | * Constructor
39 | *
40 | * @param options Notifier options
41 | */
42 | public constructor(options: NotifierNotificationOptions) {
43 | Object.assign(this, options);
44 |
45 | // If not set manually, we have to create a unique notification ID by ourselves. The ID generation relies on the current browser
46 | // datetime in ms, in praticular the moment this notification gets constructed. Concurrency, and thus two IDs being the exact same,
47 | // is not possible due to the action queue concept.
48 | if (options.id === undefined) {
49 | this.id = `ID_${new Date().getTime()}`;
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * Notifiction options
56 | *
57 | * This interface describes which information are needed to create a new notification, or in other words, which information the external API
58 | * call must provide.
59 | */
60 | export interface NotifierNotificationOptions {
61 | /**
62 | * Notification ID, optional
63 | */
64 | id?: string;
65 |
66 | /**
67 | * Notification type
68 | */
69 | type: string;
70 |
71 | /**
72 | * Notificatin message
73 | */
74 | message: string;
75 |
76 | /**
77 | * The template to customize
78 | * the appearance of the notification
79 | */
80 | template?: TemplateRef;
81 | }
82 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/notifier.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { NotifierConfig, NotifierOptions } from './models/notifier-config.model';
4 | import { NotifierModule } from './notifier.module';
5 | import { NotifierService } from './services/notifier.service';
6 |
7 | /**
8 | * Notifier Module - Unit Test
9 | */
10 | describe('Notifier Module', () => {
11 | it('should instantiate', () => {
12 | TestBed.configureTestingModule({
13 | imports: [NotifierModule],
14 | });
15 | const service: NotifierService = TestBed.inject(NotifierService);
16 |
17 | expect(service).toBeDefined();
18 | });
19 |
20 | it('should instantiate with default options', () => {
21 | TestBed.configureTestingModule({
22 | imports: [NotifierModule],
23 | });
24 | const service: NotifierService = TestBed.inject(NotifierService);
25 |
26 | expect(service.getConfig()).toEqual(new NotifierConfig());
27 | });
28 |
29 | it('should instantiate with custom options', () => {
30 | const testNotifierOptions: NotifierOptions = {
31 | animations: {
32 | hide: {
33 | easing: 'ease-in-out',
34 | },
35 | overlap: 100,
36 | shift: {
37 | speed: 200,
38 | },
39 | },
40 | behaviour: {
41 | autoHide: 5000,
42 | stacking: 7,
43 | },
44 | position: {
45 | horizontal: {
46 | distance: 20,
47 | },
48 | },
49 | theme: 'my-custom-theme',
50 | };
51 | const expectedNotifierConfig: NotifierConfig = new NotifierConfig({
52 | animations: {
53 | enabled: true,
54 | hide: {
55 | easing: 'ease-in-out',
56 | offset: 50,
57 | preset: 'fade',
58 | speed: 300,
59 | },
60 | overlap: 100,
61 | shift: {
62 | easing: 'ease',
63 | speed: 200,
64 | },
65 | show: {
66 | easing: 'ease',
67 | preset: 'slide',
68 | speed: 300,
69 | },
70 | },
71 | behaviour: {
72 | autoHide: 5000,
73 | onClick: false,
74 | onMouseover: 'pauseAutoHide',
75 | showDismissButton: true,
76 | stacking: 7,
77 | },
78 | position: {
79 | horizontal: {
80 | distance: 20,
81 | position: 'left',
82 | },
83 | vertical: {
84 | distance: 12,
85 | gap: 10,
86 | position: 'bottom',
87 | },
88 | },
89 | theme: 'my-custom-theme',
90 | });
91 |
92 | TestBed.configureTestingModule({
93 | imports: [NotifierModule.withConfig(testNotifierOptions)],
94 | });
95 | const service: NotifierService = TestBed.inject(NotifierService);
96 |
97 | expect(service.getConfig()).toEqual(expectedNotifierConfig);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/notifier.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { ModuleWithProviders, NgModule } from '@angular/core';
3 |
4 | import { NotifierContainerComponent } from './components/notifier-container.component';
5 | import { NotifierNotificationComponent } from './components/notifier-notification.component';
6 | import { NotifierConfig, NotifierOptions } from './models/notifier-config.model';
7 | import { NotifierConfigToken, NotifierOptionsToken } from './notifier.tokens';
8 | import { NotifierService } from './services/notifier.service';
9 | import { NotifierAnimationService } from './services/notifier-animation.service';
10 | import { NotifierQueueService } from './services/notifier-queue.service';
11 |
12 | /**
13 | * Factory for a notifier configuration with custom options
14 | *
15 | * Sidenote:
16 | * Required as Angular AoT compilation cannot handle dynamic functions; see .
17 | *
18 | * @param options - Custom notifier options
19 | * @returns - Notifier configuration as result
20 | */
21 | export function notifierCustomConfigFactory(options: NotifierOptions): NotifierConfig {
22 | return new NotifierConfig(options);
23 | }
24 |
25 | /**
26 | * Factory for a notifier configuration with default options
27 | *
28 | * Sidenote:
29 | * Required as Angular AoT compilation cannot handle dynamic functions; see .
30 | *
31 | * @returns - Notifier configuration as result
32 | */
33 | export function notifierDefaultConfigFactory(): NotifierConfig {
34 | return new NotifierConfig({});
35 | }
36 |
37 | /**
38 | * Notifier module
39 | */
40 | @NgModule({
41 | declarations: [NotifierContainerComponent, NotifierNotificationComponent],
42 | exports: [NotifierContainerComponent],
43 | imports: [CommonModule],
44 | providers: [
45 | NotifierAnimationService,
46 | NotifierService,
47 | NotifierQueueService,
48 |
49 | // Provide the default notifier configuration if just the module is imported
50 | {
51 | provide: NotifierConfigToken,
52 | useFactory: notifierDefaultConfigFactory,
53 | },
54 | ],
55 | })
56 | export class NotifierModule {
57 | /**
58 | * Setup the notifier module with custom providers, in this case with a custom configuration based on the givne options
59 | *
60 | * @param [options={}] - Custom notifier options
61 | * @returns - Notifier module with custom providers
62 | */
63 | public static withConfig(options: NotifierOptions = {}): ModuleWithProviders {
64 | return {
65 | ngModule: NotifierModule,
66 | providers: [
67 | // Provide the options itself upfront (as we need to inject them as dependencies -- see below)
68 | {
69 | provide: NotifierOptionsToken,
70 | useValue: options,
71 | },
72 |
73 | // Provide a custom notifier configuration, based on the given notifier options
74 | {
75 | deps: [NotifierOptionsToken],
76 | provide: NotifierConfigToken,
77 | useFactory: notifierCustomConfigFactory,
78 | },
79 | ],
80 | };
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/notifier.tokens.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 |
3 | import { NotifierConfig, NotifierOptions } from './models/notifier-config.model';
4 |
5 | /**
6 | * Injection Token for notifier options
7 | */
8 | export const NotifierOptionsToken: InjectionToken = new InjectionToken(
9 | '[angular-notifier] Notifier Options',
10 | );
11 |
12 | /**
13 | * Injection Token for notifier configuration
14 | */
15 | export const NotifierConfigToken: InjectionToken = new InjectionToken('[anuglar-notifier] Notifier Config');
16 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-animation.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { inject, TestBed } from '@angular/core/testing';
2 |
3 | import { NotifierAnimationData } from '../models/notifier-animation.model';
4 | import { NotifierConfig } from '../models/notifier-config.model';
5 | import { NotifierAnimationService } from './notifier-animation.service';
6 |
7 | /**
8 | * Notifier Animation Service - Unit Test
9 | */
10 | describe('Notifier Animation Service', () => {
11 | let animationService: NotifierAnimationService;
12 |
13 | // Setup test module
14 | beforeEach(() => {
15 | TestBed.configureTestingModule({
16 | providers: [NotifierAnimationService],
17 | });
18 | });
19 |
20 | // Inject dependencies
21 | beforeEach(inject([NotifierAnimationService], (notifierAnimationService: NotifierAnimationService) => {
22 | animationService = notifierAnimationService;
23 | }));
24 |
25 | it('should instantiate', () => {
26 | expect(animationService).toBeDefined();
27 | });
28 |
29 | it('should build the animation data for showing a notification', () => {
30 | const testConfig: NotifierConfig = new NotifierConfig({
31 | animations: {
32 | show: {
33 | easing: 'ease-in-out',
34 | preset: 'fade',
35 | speed: 400,
36 | },
37 | },
38 | });
39 | const testNotification: MockNotification = new MockNotification(testConfig);
40 | const expectedAnimationData: NotifierAnimationData = {
41 | keyframes: [
42 | {
43 | opacity: '0',
44 | },
45 | {
46 | opacity: '1',
47 | },
48 | ],
49 | options: {
50 | duration: testConfig.animations.show.speed,
51 | easing: testConfig.animations.show.easing,
52 | fill: 'forwards',
53 | },
54 | };
55 | const animationData: NotifierAnimationData = animationService.getAnimationData('show', testNotification);
56 |
57 | expect(animationData).toEqual(expectedAnimationData);
58 | });
59 |
60 | it('should build the animation data for hiding a notification', () => {
61 | const testConfig: NotifierConfig = new NotifierConfig({
62 | animations: {
63 | hide: {
64 | easing: 'ease-in-out',
65 | preset: 'fade',
66 | speed: 400,
67 | },
68 | },
69 | });
70 | const testNotification: MockNotification = new MockNotification(testConfig);
71 | const expectedAnimationData: NotifierAnimationData = {
72 | keyframes: [
73 | {
74 | opacity: '1',
75 | },
76 | {
77 | opacity: '0',
78 | },
79 | ],
80 | options: {
81 | duration: testConfig.animations.hide.speed,
82 | easing: testConfig.animations.hide.easing,
83 | fill: 'forwards',
84 | },
85 | };
86 | const animationData: NotifierAnimationData = animationService.getAnimationData('hide', testNotification);
87 |
88 | expect(animationData).toEqual(expectedAnimationData);
89 | });
90 | });
91 |
92 | /**
93 | * Mock Notification Height
94 | */
95 | const mockNotificationHeight = 40;
96 |
97 | /**
98 | * Mock Notification Shift
99 | */
100 | const mockNotificationShift = 80;
101 |
102 | /**
103 | * Mock Notification Width
104 | */
105 | const mockNotificationWidth = 300;
106 |
107 | /**
108 | * Mock notification
109 | */
110 | class MockNotification {
111 | /**
112 | * Configuration
113 | */
114 | public config: NotifierConfig;
115 |
116 | /**
117 | * Notification ID
118 | */
119 | public id = 'ID_FAKE';
120 |
121 | /**
122 | * Notification type
123 | */
124 | public type = 'SUCCESS';
125 |
126 | /**
127 | * Notification message
128 | */
129 | public message = 'Lorem ipsum dolor sit amet.';
130 |
131 | /**
132 | * Notification component
133 | */
134 | public component: { [key: string]: () => any } = {
135 | getConfig: () => this.config,
136 | getHeight: () => mockNotificationHeight,
137 | getShift: () => mockNotificationShift,
138 | getWidth: () => mockNotificationWidth,
139 | };
140 |
141 | /**
142 | * Constructor
143 | *
144 | * @param {NotifierConfig} config Configuration
145 | */
146 | public constructor(config: NotifierConfig) {
147 | this.config = config;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-animation.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { fade } from '../animation-presets/fade.animation-preset';
4 | import { slide } from '../animation-presets/slide.animation-preset';
5 | import { NotifierAnimationData, NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
6 | import { NotifierNotification } from '../models/notifier-notification.model';
7 |
8 | /**
9 | * Notifier animation service
10 | */
11 | @Injectable()
12 | export class NotifierAnimationService {
13 | /**
14 | * List of animation presets (currently static)
15 | */
16 | private readonly animationPresets: {
17 | [animationPresetName: string]: NotifierAnimationPreset;
18 | };
19 |
20 | /**
21 | * Constructor
22 | */
23 | public constructor() {
24 | this.animationPresets = {
25 | fade,
26 | slide,
27 | };
28 | }
29 |
30 | /**
31 | * Get animation data
32 | *
33 | * This method generates all data the Web Animations API needs to animate our notification. The result depends on both the animation
34 | * direction (either in or out) as well as the notifications (and its attributes) itself.
35 | *
36 | * @param direction Animation direction, either in or out
37 | * @param notification Notification the animation data should be generated for
38 | * @returns Animation information
39 | */
40 | public getAnimationData(direction: 'show' | 'hide', notification: NotifierNotification): NotifierAnimationData {
41 | // Get all necessary animation data
42 | let keyframes: NotifierAnimationPresetKeyframes;
43 | let duration: number;
44 | let easing: string;
45 | if (direction === 'show') {
46 | keyframes = this.animationPresets[notification.component.getConfig().animations.show.preset].show(notification);
47 | duration = notification.component.getConfig().animations.show.speed;
48 | easing = notification.component.getConfig().animations.show.easing;
49 | } else {
50 | keyframes = this.animationPresets[notification.component.getConfig().animations.hide.preset].hide(notification);
51 | duration = notification.component.getConfig().animations.hide.speed;
52 | easing = notification.component.getConfig().animations.hide.easing;
53 | }
54 |
55 | // Build and return animation data
56 | return {
57 | keyframes: [keyframes.from, keyframes.to],
58 | options: {
59 | duration,
60 | easing,
61 | fill: 'forwards', // Keep the newly painted state after the animation finished
62 | },
63 | };
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-queue.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { inject, TestBed } from '@angular/core/testing';
2 | import { Subject } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 | import { NotifierQueueService } from './notifier-queue.service';
6 |
7 | /**
8 | * Notifier Queue Service - Unit Test
9 | */
10 | describe('Notifier Queue Service', () => {
11 | let queueService: NotifierQueueService;
12 |
13 | // Setup test module
14 | beforeEach(() => {
15 | TestBed.configureTestingModule({
16 | providers: [NotifierQueueService],
17 | });
18 | });
19 |
20 | // Inject dependencies
21 | beforeEach(inject([NotifierQueueService], (notifierQueueService: NotifierQueueService) => {
22 | queueService = notifierQueueService;
23 | }));
24 |
25 | it('should instantiate', () => {
26 | expect(queueService).toBeDefined();
27 | expect(queueService.actionStream).toEqual(expect.any(Subject));
28 | });
29 |
30 | it('should pass through one action', () => {
31 | const testAction: NotifierAction = {
32 | payload: 'FANCY',
33 | type: 'SHOW',
34 | };
35 | let expectedTestAction: NotifierAction | undefined;
36 |
37 | queueService.actionStream.subscribe((action: NotifierAction) => {
38 | expectedTestAction = action;
39 | });
40 | queueService.push(testAction);
41 |
42 | expect(expectedTestAction).toEqual(testAction);
43 | });
44 |
45 | it('should pass through multiple actions in order', () => {
46 | const firstTestAction: NotifierAction = {
47 | payload: 'AWESOME',
48 | type: 'SHOW',
49 | };
50 | const secondTestAction: NotifierAction = {
51 | payload: 'GREAT',
52 | type: 'HIDE',
53 | };
54 | let expectedTestAction: NotifierAction | undefined;
55 |
56 | queueService.actionStream.subscribe((action: NotifierAction) => {
57 | expectedTestAction = action;
58 | });
59 | queueService.push(firstTestAction);
60 | queueService.push(secondTestAction);
61 |
62 | expect(expectedTestAction).toEqual(firstTestAction);
63 |
64 | queueService.continue();
65 |
66 | expect(expectedTestAction).toEqual(secondTestAction);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-queue.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Subject } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 |
6 | /**
7 | * Notifier queue service
8 | *
9 | * In general, API calls don't get processed right away. Instead, we have to queue them up in order to prevent simultanious API calls
10 | * interfering with each other. This, at least in theory, is possible at any time. In particular, animations - which potentially overlap -
11 | * can cause changes in JS classes as well as affect the DOM. Therefore, the queue service takes all actions, puts them in a queue, and
12 | * processes them at the right time (which is when the previous action has been processed successfully).
13 | *
14 | * Technical sidenote:
15 | * An action looks pretty similar to the ones within the Flux / Redux pattern.
16 | */
17 | @Injectable()
18 | export class NotifierQueueService {
19 | /**
20 | * Stream of actions, subscribable from outside
21 | */
22 | public readonly actionStream: Subject;
23 |
24 | /**
25 | * Queue of actions
26 | */
27 | private actionQueue: Array;
28 |
29 | /**
30 | * Flag, true if some action is currently in progress
31 | */
32 | private isActionInProgress: boolean;
33 |
34 | /**
35 | * Constructor
36 | */
37 | public constructor() {
38 | this.actionStream = new Subject();
39 | this.actionQueue = [];
40 | this.isActionInProgress = false;
41 | }
42 |
43 | /**
44 | * Push a new action to the queue, and try to run it
45 | *
46 | * @param action Action object
47 | */
48 | public push(action: NotifierAction): void {
49 | this.actionQueue.push(action);
50 | this.tryToRunNextAction();
51 | }
52 |
53 | /**
54 | * Continue with the next action (called when the current action is finished)
55 | */
56 | public continue(): void {
57 | this.isActionInProgress = false;
58 | this.tryToRunNextAction();
59 | }
60 |
61 | /**
62 | * Try to run the next action in the queue; we skip if there already is some action in progress, or if there is no action left
63 | */
64 | private tryToRunNextAction(): void {
65 | if (this.isActionInProgress || this.actionQueue.length === 0) {
66 | return; // Skip (the queue can now go drink a coffee as it has nothing to do anymore)
67 | }
68 | this.isActionInProgress = true;
69 | this.actionStream.next(this.actionQueue.shift()); // Push next action to the stream, and remove the current action from the queue
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-timer.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
2 |
3 | import { NotifierTimerService } from './notifier-timer.service';
4 |
5 | /**
6 | * Notifier Timer Service - Unit Test
7 | */
8 | describe('Notifier Timer Service', () => {
9 | const fullAnimationTime = 5000;
10 | const longAnimationTime = 4000;
11 | const shortAnimationTime = 1000;
12 |
13 | let timerService: NotifierTimerService;
14 | let mockDate: MockDate;
15 |
16 | // Setup test module
17 | beforeEach(() => {
18 | TestBed.configureTestingModule({
19 | providers: [NotifierTimerService],
20 | });
21 | });
22 |
23 | // Inject dependencies
24 | beforeEach(inject([NotifierTimerService], (notifierTimerService: NotifierTimerService) => {
25 | timerService = notifierTimerService;
26 | mockDate = new MockDate();
27 | }));
28 |
29 | it('should instantiate', () => {
30 | expect(timerService).toBeDefined();
31 | });
32 |
33 | it('should start and stop the timer', fakeAsync(() => {
34 | const timerServiceCallback = jest.fn();
35 | timerService.start(fullAnimationTime).then(timerServiceCallback);
36 |
37 | tick(longAnimationTime);
38 |
39 | expect(timerServiceCallback).not.toHaveBeenCalled();
40 |
41 | tick(shortAnimationTime);
42 |
43 | expect(timerServiceCallback).toHaveBeenCalled();
44 | }));
45 |
46 | it('should pause and resume the timer', fakeAsync(() => {
47 | jest.spyOn(window, 'Date').mockImplementation(() => mockDate);
48 | const timerServiceCallback = jest.fn();
49 | timerService.start(fullAnimationTime).then(timerServiceCallback);
50 |
51 | tick(longAnimationTime);
52 | mockDate.fastForwardTime(longAnimationTime); // Also update the global Date (in addition to the tick)
53 |
54 | timerService.pause();
55 |
56 | tick(shortAnimationTime);
57 | mockDate.fastForwardTime(shortAnimationTime); // Also update the global Date (in addition to the tick)
58 |
59 | expect(timerServiceCallback).not.toHaveBeenCalled();
60 |
61 | // Resumes the timer, using the same duration as above (a continue doesn't exist yet)
62 | timerService.continue();
63 | tick(shortAnimationTime);
64 |
65 | expect(timerServiceCallback).toHaveBeenCalled();
66 | }));
67 |
68 | it('should stop the timer', fakeAsync(() => {
69 | const timerServiceCallback = jest.fn();
70 | timerService.start(fullAnimationTime).then(timerServiceCallback);
71 |
72 | tick(longAnimationTime);
73 | timerService.stop();
74 | tick(shortAnimationTime);
75 |
76 | expect(timerServiceCallback).not.toHaveBeenCalled();
77 | }));
78 | });
79 |
80 | /**
81 | * Mock Date, allows for fast-forwarding the time even in the global Date object
82 | */
83 | class MockDate extends Date {
84 | /**
85 | * Start time (at init)
86 | */
87 | private startTime: number;
88 |
89 | /**
90 | * Elapsed time (since init)
91 | */
92 | private elapsedTime: number;
93 |
94 | /**
95 | * Fast-forward the current time manually
96 | */
97 | public fastForwardTime(duration: number): void {
98 | this.elapsedTime += duration;
99 | }
100 |
101 | /**
102 | * Get the current time
103 | *
104 | * @override
105 | */
106 | public getTime(): number {
107 | return this.startTime + this.elapsedTime;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-timer.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | /**
4 | * Notifier timer service
5 | *
6 | * This service acts as a timer, needed due to the still rather limited setTimeout JavaScript API. The timer service can start and stop a
7 | * timer. Furthermore, it can also pause the timer at any time, and resume later on. The timer API workd promise-based.
8 | */
9 | @Injectable()
10 | export class NotifierTimerService {
11 | /**
12 | * Timestamp (in ms), created in the moment the timer starts
13 | */
14 | private now: number;
15 |
16 | /**
17 | * Remaining time (in ms)
18 | */
19 | private remaining: number;
20 |
21 | /**
22 | * Timeout ID, used for clearing the timeout later on
23 | */
24 | private timerId: number;
25 |
26 | /**
27 | * Promise resolve function, eventually getting called once the timer finishes
28 | */
29 | private finishPromiseResolver: () => void;
30 |
31 | /**
32 | * Constructor
33 | */
34 | public constructor() {
35 | this.now = 0;
36 | this.remaining = 0;
37 | }
38 |
39 | /**
40 | * Start (or resume) the timer
41 | *
42 | * @param duration Timer duration, in ms
43 | * @returns Promise, resolved once the timer finishes
44 | */
45 | public start(duration: number): Promise {
46 | return new Promise((resolve: () => void) => {
47 | // For the first run ...
48 | this.remaining = duration;
49 |
50 | // Setup, then start the timer
51 | this.finishPromiseResolver = resolve;
52 | this.continue();
53 | });
54 | }
55 |
56 | /**
57 | * Pause the timer
58 | */
59 | public pause(): void {
60 | clearTimeout(this.timerId);
61 | this.remaining -= new Date().getTime() - this.now;
62 | }
63 |
64 | /**
65 | * Continue the timer
66 | */
67 | public continue(): void {
68 | this.now = new Date().getTime();
69 | this.timerId = window.setTimeout(() => {
70 | this.finish();
71 | }, this.remaining);
72 | }
73 |
74 | /**
75 | * Stop the timer
76 | */
77 | public stop(): void {
78 | clearTimeout(this.timerId);
79 | this.remaining = 0;
80 | }
81 |
82 | /**
83 | * Finish up the timeout by resolving the timer promise
84 | */
85 | private finish(): void {
86 | this.finishPromiseResolver();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { inject, TestBed } from '@angular/core/testing';
3 | import { Subject } from 'rxjs';
4 |
5 | import { NotifierAction } from '../models/notifier-action.model';
6 | import { NotifierConfig } from '../models/notifier-config.model';
7 | import { NotifierNotificationOptions } from '../models/notifier-notification.model';
8 | import { NotifierConfigToken } from '../notifier.tokens';
9 | import { NotifierService } from './notifier.service';
10 | import { NotifierQueueService } from './notifier-queue.service';
11 |
12 | /**
13 | * Notifier Service - Unit Test
14 | */
15 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
16 | animations: {
17 | enabled: true,
18 | hide: {
19 | easing: 'ease',
20 | offset: 50,
21 | preset: 'fade',
22 | speed: 300,
23 | },
24 | overlap: 150,
25 | shift: {
26 | easing: 'ease',
27 | speed: 300,
28 | },
29 | show: {
30 | easing: 'ease',
31 | preset: 'slide',
32 | speed: 300,
33 | },
34 | },
35 | behaviour: {
36 | autoHide: 7000,
37 | onClick: false,
38 | onMouseover: 'pauseAutoHide',
39 | showDismissButton: true,
40 | stacking: 4,
41 | },
42 | position: {
43 | horizontal: {
44 | distance: 12,
45 | position: 'left',
46 | },
47 | vertical: {
48 | distance: 12,
49 | gap: 10,
50 | position: 'bottom',
51 | },
52 | },
53 | theme: 'material',
54 | });
55 |
56 | describe('Notifier Service', () => {
57 | let service: NotifierService;
58 | let queueService: MockNotifierQueueService;
59 |
60 | // Setup test module
61 | beforeEach(() => {
62 | TestBed.configureTestingModule({
63 | providers: [
64 | NotifierService,
65 | {
66 | provide: NotifierQueueService,
67 | useClass: MockNotifierQueueService,
68 | },
69 | {
70 | provide: NotifierConfigToken,
71 | useValue: testNotifierConfig,
72 | },
73 | ],
74 | });
75 | });
76 |
77 | // Inject dependencies
78 | beforeEach(inject(
79 | [NotifierService, NotifierQueueService],
80 | (notifierService: NotifierService, notifierQueueService: MockNotifierQueueService) => {
81 | service = notifierService;
82 | queueService = notifierQueueService;
83 | },
84 | ));
85 |
86 | it('should instantiate', () => {
87 | expect(service).toBeDefined();
88 | });
89 |
90 | it('should show a notification', () => {
91 | const testNotificationOptions: NotifierNotificationOptions = {
92 | id: 'ID_FAKE',
93 | message: 'Lorem ipsum dolor sit amet.',
94 | type: 'SUCCESS',
95 | };
96 | const expectedAction: NotifierAction = {
97 | payload: testNotificationOptions,
98 | type: 'SHOW',
99 | };
100 | service.show(testNotificationOptions);
101 |
102 | expect(queueService.lastAction).toEqual(expectedAction);
103 | });
104 |
105 | it('should show a notification, the simply way', () => {
106 | const testNotificationOptions: NotifierNotificationOptions = {
107 | message: 'Lorem ipsum dolor sit amet.',
108 | type: 'SUCCESS',
109 | };
110 | const expectedAction: NotifierAction = {
111 | payload: testNotificationOptions,
112 | type: 'SHOW',
113 | };
114 | service.notify(testNotificationOptions.type, testNotificationOptions.message);
115 |
116 | expect(queueService.lastAction).toEqual(expectedAction);
117 | });
118 |
119 | it('should show a notification with an explicit ID, the simply way', () => {
120 | const testNotificationOptions: NotifierNotificationOptions = {
121 | id: 'ID_FAKE',
122 | message: 'Lorem ipsum dolor sit amet.',
123 | type: 'SUCCESS',
124 | };
125 | const expectedAction: NotifierAction = {
126 | payload: testNotificationOptions,
127 | type: 'SHOW',
128 | };
129 | service.notify(testNotificationOptions.type, testNotificationOptions.message, testNotificationOptions.id);
130 |
131 | expect(queueService.lastAction).toEqual(expectedAction);
132 | });
133 |
134 | it('should hide a specific notification', () => {
135 | const testNotificationId = 'ID_FAKE';
136 | const expectedAction: NotifierAction = {
137 | payload: testNotificationId,
138 | type: 'HIDE',
139 | };
140 | service.hide(testNotificationId);
141 |
142 | expect(queueService.lastAction).toEqual(expectedAction);
143 | });
144 |
145 | it('should hide the newest notification', () => {
146 | const expectedAction: NotifierAction = {
147 | type: 'HIDE_NEWEST',
148 | };
149 | service.hideNewest();
150 |
151 | expect(queueService.lastAction).toEqual(expectedAction);
152 | });
153 |
154 | it('should hide the oldest notification', () => {
155 | const expectedAction: NotifierAction = {
156 | type: 'HIDE_OLDEST',
157 | };
158 | service.hideOldest();
159 |
160 | expect(queueService.lastAction).toEqual(expectedAction);
161 | });
162 |
163 | it('should hide all notifications', () => {
164 | const expectedAction: NotifierAction = {
165 | type: 'HIDE_ALL',
166 | };
167 | service.hideAll();
168 |
169 | expect(queueService.lastAction).toEqual(expectedAction);
170 | });
171 |
172 | it('should return the configuration', () => {
173 | expect(service.getConfig()).toEqual(testNotifierConfig);
174 | });
175 |
176 | it('should return the notification action', () => {
177 | const testNotificationId = 'ID_FAKE';
178 | const expectedAction: NotifierAction = {
179 | payload: testNotificationId,
180 | type: 'HIDE',
181 | };
182 | service.actionStream.subscribe((action) => expect(action).toEqual(expectedAction));
183 | service.hide(testNotificationId);
184 | });
185 | });
186 |
187 | /**
188 | * Mock Notifier Queue Service
189 | */
190 | @Injectable()
191 | class MockNotifierQueueService extends NotifierQueueService {
192 | /**
193 | * Last action
194 | */
195 | public lastAction: NotifierAction;
196 | public actionStream = new Subject();
197 |
198 | /**
199 | * Push a new action to the queue
200 | *
201 | * @param {NotifierAction} action Action object
202 | *
203 | * @override
204 | */
205 | public push(action: NotifierAction): void {
206 | this.lastAction = action;
207 | this.actionStream.next(action);
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@angular/core';
2 | import { Observable } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 | import { NotifierConfig } from '../models/notifier-config.model';
6 | import { NotifierNotificationOptions } from '../models/notifier-notification.model';
7 | import { NotifierConfigToken } from '../notifier.tokens';
8 | import { NotifierQueueService } from './notifier-queue.service';
9 |
10 | /**
11 | * Notifier service
12 | *
13 | * This service provides access to the public notifier API. Once injected into a component, directive, pipe, service, or any other building
14 | * block of an applications, it can be used to show new notifications, and hide existing ones. Internally, it transforms API calls into
15 | * actions, which then get thrown into the action queue - eventually being processed at the right moment.
16 | */
17 | @Injectable()
18 | export class NotifierService {
19 | /**
20 | * Notifier queue service
21 | */
22 | private readonly queueService: NotifierQueueService;
23 |
24 | /**
25 | * Notifier configuration
26 | */
27 | private readonly config: NotifierConfig;
28 |
29 | /**
30 | * Constructor
31 | *
32 | * @param notifierQueueService Notifier queue service
33 | * @param config Notifier configuration, optionally injected as a dependency
34 | */
35 | public constructor(notifierQueueService: NotifierQueueService, @Inject(NotifierConfigToken) config: NotifierConfig) {
36 | this.queueService = notifierQueueService;
37 | this.config = config;
38 | }
39 |
40 | /**
41 | * Get the notifier configuration
42 | *
43 | * @returns Notifier configuration
44 | */
45 | public getConfig(): NotifierConfig {
46 | return this.config;
47 | }
48 |
49 | /**
50 | * Get the observable for handling actions
51 | *
52 | * @returns Observable of NotifierAction
53 | */
54 | public get actionStream(): Observable {
55 | return this.queueService.actionStream.asObservable();
56 | }
57 |
58 | /**
59 | * API: Show a new notification
60 | *
61 | * @param notificationOptions Notification options
62 | */
63 | public show(notificationOptions: NotifierNotificationOptions): void {
64 | this.queueService.push({
65 | payload: notificationOptions,
66 | type: 'SHOW',
67 | });
68 | }
69 |
70 | /**
71 | * API: Hide a specific notification, given its ID
72 | *
73 | * @param notificationId ID of the notification to hide
74 | */
75 | public hide(notificationId: string): void {
76 | this.queueService.push({
77 | payload: notificationId,
78 | type: 'HIDE',
79 | });
80 | }
81 |
82 | /**
83 | * API: Hide the newest notification
84 | */
85 | public hideNewest(): void {
86 | this.queueService.push({
87 | type: 'HIDE_NEWEST',
88 | });
89 | }
90 |
91 | /**
92 | * API: Hide the oldest notification
93 | */
94 | public hideOldest(): void {
95 | this.queueService.push({
96 | type: 'HIDE_OLDEST',
97 | });
98 | }
99 |
100 | /**
101 | * API: Hide all notifications at once
102 | */
103 | public hideAll(): void {
104 | this.queueService.push({
105 | type: 'HIDE_ALL',
106 | });
107 | }
108 |
109 | /**
110 | * API: Shortcut for showing a new notification
111 | *
112 | * @param type Type of the notification
113 | * @param message Message of the notification
114 | * @param [notificationId] Unique ID for the notification (optional)
115 | */
116 | public notify(type: string, message: string, notificationId?: string): void {
117 | const notificationOptions: NotifierNotificationOptions = {
118 | message,
119 | type,
120 | };
121 | if (notificationId !== undefined) {
122 | notificationOptions.id = notificationId;
123 | }
124 | this.show(notificationOptions);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: COMBINED STYLES
2 | //
3 | // This file acts as an index - it imports the core styles as well as all provided themes and types. For performance and maintainability
4 | // reasons, it's probably better include only the files needed within the specific project. Alternatively, it's also possible to only use
5 | // those parts (SASS files) used, or even write the styles completely from scratch.
6 | //
7 | // Technical sidenote:
8 | // We do *NOT* use the partial syntax, and also explicitely write out the file type, so compiling works properly.
9 |
10 | // Core
11 | @import './styles/core.scss';
12 |
13 | // Themes
14 | @import './styles/themes/theme-material.scss';
15 |
16 | // Types
17 | @import './styles/types/type-default.scss';
18 | @import './styles/types/type-error.scss';
19 | @import './styles/types/type-info.scss';
20 | @import './styles/types/type-success.scss';
21 | @import './styles/types/type-warning.scss';
22 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/core.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: CORE STYLES
2 |
3 | // Container
4 |
5 | .notifier {
6 | &__container {
7 | * {
8 | box-sizing: border-box;
9 | }
10 |
11 | &-list {
12 | margin: {
13 | top: 0;
14 | bottom: 0;
15 | }
16 | padding: {
17 | left: 0;
18 | }
19 | list-style-type: none;
20 | }
21 | }
22 |
23 | &__notification {
24 | display: flex;
25 | align-items: center;
26 | position: fixed; // Overlay
27 | visibility: hidden; // Notifications are hidden by default, and get shown (or animated in) dynamically by the Angular component
28 | z-index: 10000; // Pretty much random ...
29 |
30 | // This attribute forces this element to be rendered on a new layer, by the GPU, in order to improve its performance (#perfmatters)
31 | will-change: transform;
32 |
33 | // This attribute improves the overall scrolling performance for fixed position elements, such as this one (#perfmatters)
34 | // See
35 | backface-visibility: hidden;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/themes/theme-material.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: MATERIAL THEME
2 | //
3 | // This material theme tries its best to look as material-design'ish as possible - round edges, shadows, and small animations.
4 | // And, of course, thanks to #Google for creating and sharing such an awesome design language!
5 | // I highly encourage everyone to read into the Material Design specs:
6 |
7 | $notifier-shadow-color: rgba(0, 0, 0, 0.2) !default;
8 |
9 | .notifier__notification--material {
10 | border-radius: 3px;
11 | box-shadow: 0 1px 3px $notifier-shadow-color;
12 | cursor: default; // Default cursor, even when hovering over text
13 | padding: {
14 | top: 11px;
15 | right: 26px;
16 | bottom: 10px;
17 | left: 26px;
18 | }
19 |
20 | .notifier__notification {
21 | &-message {
22 | display: inline-block;
23 | margin: {
24 | // Reset paragraph default styles
25 | top: 0;
26 | bottom: 0;
27 | }
28 | vertical-align: top;
29 | line-height: 32px;
30 | font-size: 15px;
31 | }
32 |
33 | &-button {
34 | display: inline-block;
35 | transition: opacity 0.2s ease;
36 | opacity: 0.5;
37 | margin: {
38 | right: -10px;
39 | left: 10px;
40 | }
41 | outline: none;
42 | border: none;
43 | background: none;
44 | cursor: pointer; // Make it obvious that the "button" (or, more honestly, icon) is clickable (#UX)
45 | padding: 6px;
46 | width: 32px;
47 | height: 32px;
48 | vertical-align: top;
49 |
50 | &:hover,
51 | &:focus {
52 | opacity: 1; // Make me "feel" the clickability with a transparency change (#UX)
53 | }
54 |
55 | &:active {
56 | transform: scale(0.82); // Make me "feel" the click by a push back (#UX)
57 | opacity: 1;
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/types/type-default.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: DEFAULT TYPE STYLES
2 |
3 | $notifier-default-background-color: #444 !default;
4 | $notifier-default-font-color: #fff !default;
5 | $notifier-default-icon-color: #fff !default;
6 |
7 | .notifier__notification--default {
8 | background-color: $notifier-default-background-color;
9 | color: $notifier-default-font-color;
10 |
11 | .notifier__notification-button-icon {
12 | // 16x16 fixed size
13 | fill: $notifier-default-icon-color;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/types/type-error.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: ERROR TYPE STYLES
2 |
3 | $notifier-error-background-color: #d9534f !default;
4 | $notifier-error-font-color: #fff !default;
5 | $notifier-error-icon-color: #fff !default;
6 |
7 | .notifier__notification--error {
8 | background-color: $notifier-error-background-color;
9 | color: $notifier-error-font-color;
10 |
11 | .notifier__notification-button-icon {
12 | // 16x16 fixed size
13 | fill: $notifier-error-icon-color;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/types/type-info.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: INFO TYPE STYLES
2 |
3 | $notifier-info-background-color: #5bc0de !default;
4 | $notifier-info-font-color: #fff !default;
5 | $notifier-info-icon-color: #fff !default;
6 |
7 | .notifier__notification--info {
8 | background-color: $notifier-info-background-color;
9 | color: $notifier-info-font-color;
10 |
11 | .notifier__notification-button-icon {
12 | // 16x16 fixed size
13 | fill: $notifier-info-icon-color;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/types/type-success.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: SUCCESS TYPE STYLES
2 |
3 | $notifier-success-background-color: #5cb85c !default;
4 | $notifier-success-font-color: #fff !default;
5 | $notifier-success-icon-color: #fff !default;
6 |
7 | .notifier__notification--success {
8 | background-color: $notifier-success-background-color;
9 | color: $notifier-success-font-color;
10 |
11 | .notifier__notification-button-icon {
12 | // 16x16 fixed size
13 | fill: $notifier-success-icon-color;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/styles/types/type-warning.scss:
--------------------------------------------------------------------------------
1 | // NOTIFIER: WARNING TYPE STYLES
2 |
3 | $notifier-warning-background-color: #f0ad4e !default;
4 | $notifier-warning-font-color: #fff !default;
5 | $notifier-warning-icon-color: #fff !default;
6 |
7 | .notifier__notification--warning {
8 | background-color: $notifier-warning-background-color;
9 | color: $notifier-warning-font-color;
10 |
11 | .notifier__notification-button-icon {
12 | // 16x16 fixed size
13 | fill: $notifier-warning-icon-color;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/angular-notifier/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/lib",
5 | "declarationMap": true,
6 | "declaration": true,
7 | "inlineSources": true,
8 | "types": [],
9 | "lib": ["dom", "es2018"]
10 | },
11 | "angularCompilerOptions": {
12 | "skipTemplateCodegen": true,
13 | "strictMetadataEmit": true,
14 | "enableResourceInlining": true
15 | },
16 | "exclude": ["src/test.ts", "**/*.spec.ts"]
17 | }
18 |
--------------------------------------------------------------------------------
/projects/angular-notifier/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "compilationMode": "partial"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/projects/angular-notifier/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "emitDecoratorMetadata": true,
5 | "esModuleInterop": true,
6 | "outDir": "../../out-tsc/spec",
7 | "types": ["jest", "node"]
8 | },
9 | "include": ["**/*.spec.ts", "**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/tools/update-package.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | console.log('Update package.json info ...');
4 |
5 | // Read both package.json files
6 | const rootPackageJson = JSON.parse(fs.readFileSync('./package.json').toString());
7 | const distPackageJson = JSON.parse(fs.readFileSync('./dist/angular-notifier/package.json').toString());
8 |
9 | // Update dist package.json file with some info from root package.json file
10 | const keys = ['description', 'version', 'license', 'repository', 'keywords', 'peerDependencies', 'dependencies'];
11 | keys.forEach((key) => {
12 | distPackageJson[key] = rootPackageJson[key];
13 | });
14 |
15 | // Write updated package.json file
16 | fs.writeFileSync('./dist/angular-notifier/package.json', JSON.stringify(distPackageJson, null, ' '));
17 |
18 | console.log('Done.');
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "es2020",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "ES2022",
14 | "lib": [
15 | "es2018",
16 | "dom"
17 | ],
18 | "paths": {
19 | "angular-notifier": [
20 | "dist/angular-notifier"
21 | ]
22 | },
23 | "useDefineForClassFields": false
24 | },
25 | "angularCompilerOptions": {
26 | "fullTemplateTypeCheck": true,
27 | "strictInjectionParameters": true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "emitDecoratorMetadata": true,
5 | "esModuleInterop": true,
6 | "outDir": "./out-tsc/spec",
7 | "types": ["jest", "node"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------