├── .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 | ![Angular Notifier Animated Preview GIF](/docs/angular-notifier-preview.gif?raw=true) 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 | 5 | 6 | 7 | 10 | 11 | 14 | 15 |

Hide notifications

16 | 17 | 18 | 19 | 20 |

Show & hide a specific notification

21 | 24 | 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 |
    2 |
  • 3 | 4 | 5 |
  • 6 |
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 | 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 | --------------------------------------------------------------------------------