From fb4055d12463e7c5f3a1435595c12d4b36a4d6b3 Mon Sep 17 00:00:00 2001 From: Juri Date: Fri, 3 Aug 2018 13:33:50 +0200 Subject: [PATCH 1/2] build: remove @types/jest due to conflicts The jest-preset-angular already has the necessary types. If both get installed, then we get build-time errors due to duplicate type definition files --- package-lock.json | 8 +------- package.json | 11 +++++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19f3d32a..5126ea23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "angular-notifier", - "version": "3.0.0", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -140,12 +140,6 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, - "@types/jest": { - "version": "23.1.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.1.4.tgz", - "integrity": "sha512-w2KaQFm8zaZMsvPGYWoCxvpDli+dXhee2QZCk35sCpG595KKinyxfpKlfvxZWH776K2kPk53AgO5zv+U4JIrdg==", - "dev": true - }, "@types/node": { "version": "10.5.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.1.tgz", diff --git a/package.json b/package.json index d23274c3..b7c16256 100644 --- a/package.json +++ b/package.json @@ -50,28 +50,27 @@ }, "devDependencies": { "@angular/common": "6.0.x", - "@angular/compiler-cli": "6.0.x", "@angular/compiler": "6.0.x", + "@angular/compiler-cli": "6.0.x", "@angular/core": "6.0.x", - "@angular/platform-browser-dynamic": "6.0.x", "@angular/platform-browser": "6.0.x", - "@types/jest": "23.1.x", + "@angular/platform-browser-dynamic": "6.0.x", "@types/node": "10.5.x", "@types/web-animations-js": "2.2.x", "angular-package-builder": "2.0.x", "angular2-template-loader": "0.6.x", "automatic-release": "1.1.x", "awesome-typescript-loader": "5.2.x", - "browser-sync-webpack-plugin": "2.0.x", "browser-sync": "2.24.x", + "browser-sync-webpack-plugin": "2.0.x", "codecov": "3.0.x", "copyfiles": "2.0.x", "core-js": "2.5.x", "css-loader": "0.28.x", "friendly-errors-webpack-plugin": "1.7.x", "html-webpack-plugin": "3.2.x", - "jest-preset-angular": "5.2.x", "jest": "23.2.x", + "jest-preset-angular": "5.2.x", "node-sass": "4.9.x", "progress-bar-webpack-plugin": "1.11.x", "raw-loader": "0.5.x", @@ -86,9 +85,9 @@ "ts-jest": "22.4.x", "typescript": "2.7.x", "web-animations-js": "2.3.x", - "webpack-dev-server": "3.1.x", "webpack": "4.13.x", "webpack-cli": "3.0.x", + "webpack-dev-server": "3.1.x", "zone.js": "0.8.26" } } From 223733a19799949db1d64bf9f842f463d0b5473c Mon Sep 17 00:00:00 2001 From: Juri Date: Fri, 3 Aug 2018 14:41:03 +0200 Subject: [PATCH 2/2] feat: add possibility to define custom notification templates fixes #94 --- README.md | 1054 ++++---- src/demo/app.component.ts | 279 ++- .../notifier-notification.component.html | 18 +- .../notifier-notification.component.spec.ts | 2188 +++++++++-------- .../src/models/notifier-notification.model.ts | 153 +- 5 files changed, 1914 insertions(+), 1778 deletions(-) diff --git a/README.md b/README.md index a8237b0c..dd90f08a 100644 --- a/README.md +++ b/README.md @@ -1,505 +1,549 @@ -
- -# angular-notifier - -**A well designed, fully animated, highly customizable, and easy-to-use notification library for your **Angular 2+** application.** - -[![npm version](https://img.shields.io/npm/v/angular-notifier.svg?maxAge=3600&style=flat)](https://www.npmjs.com/package/angular-notifier) -[![dependency status](https://img.shields.io/david/dominique-mueller/angular-notifier.svg?maxAge=3600&style=flat)](https://david-dm.org/dominique-mueller/angular-notifier) -[![travis ci build status](https://img.shields.io/travis/dominique-mueller/angular-notifier/master.svg?maxAge=3600&style=flat)](https://travis-ci.org/dominique-mueller/angular-notifier) -[![Codecov](https://img.shields.io/codecov/c/github/dominique-mueller/angular-notifier.svg?maxAge=3600&style=flat)](https://codecov.io/gh/dominique-mueller/angular-notifier) -[![Known Vulnerabilities](https://snyk.io/test/github/dominique-mueller/angular-notifier/badge.svg)](https://snyk.io/test/github/dominique-mueller/simple-progress-webpack-plugin) -[![license](https://img.shields.io/npm/l/angular-notifier.svg?maxAge=3600&style=flat)](https://github.com/dominique-mueller/angular-notifier/LICENSE) - -
- -

- -## Demo - -You can play around with this library with **[this Stackblitz right here](https://stackblitz.com/edit/angular-notifier-demo)**. - -![Angular Notifier Animated Preview GIF](/docs/angular-notifier-preview.gif?raw=true) - -


- -## How to install - -You can get **angular-notifier** via **npm** by either adding it as a new *dependency* to your `package.json` file and running npm install, -or running the following command: - -``` bash -npm install angular-notifier -``` - -
- -### Angular versions - -The following list describes the compatibility with Angular: - -| Angular Notifier | Angular | -| ----------------- | ------- | -| `1.x` | `2.x` | -| `2.x` | `4.x` | -| `3.x` | `5.x` | -| `4.x` | `6.x` | - -
- -### Browser support & polyfills - -By default, meaning without any polyfills, **angular-notifier** is compatible with **the latest versions of Chrome, Firefox, and Opera**. -Bringing in the following polyfills will improve browser support: - -- To be able to use the latest and greatest JavaScript features in older browsers (e.g. older version of IE & Safari), you might want to -add **[core-js](https://github.com/zloirock/core-js)** to your polyfills. -- For animation support (in particular, for better -**[Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)** support), you might want to use the **[web-animations-js](https://github.com/web-animations/web-animations-js)** polyfill. For details, see the corresponding -**[CanIUse page](http://caniuse.com/#feat=web-animation)**. - -> For detailed information about the Angular browser support read the -**[official Angular browser support documentation](https://angular.io/docs/ts/latest/guide/browser-support.html)**. If you generated your -Angular project with the **[Angular CLI](https://github.com/angular/angular-cli)**, all the polyfills mentioned above do already exist in -your `polyfills.ts` file - waiting for you to enable and install them. - -


- -## How to setup - -Before actually being able to use the **angular-notifier** library within our code, we have to first set it up within Angular, and also -bring the styles into our project. - -
- -### 1. Import the `NotifierModule` - -First of all, make **angular-notifier** globally available to your Angular application by importing (and optionally also configuring) the -`NotifierModule` the your root Angular module. For example: - -``` typescript -import { NotifierModule } from 'angular-notifier'; - -@NgModule( { - imports: [ - NotifierModule - ] -} ) -export class AppModule {} -``` - -But wait -- your probably might want to customize your notifications' look and behaviour according to your requirements and needs. To do so, -call the `withConfig` method on the `NotifierModule`, and pass in the options. For example: - -``` typescript -import { NotifierModule } from 'angular-notifier'; - -@NgModule( { - imports: [ - NotifierModule.withConfig( { - // Custom options in here - } ) - ] -} ) -export class AppModule {} -``` - -
- -### 2. Use the `notifier-container` component - -In addition, you have to place the `notifier-container` component somewhere in your application, best at the last element of your -root (app) component. For example: - -``` typescript -@Component( { - selector: 'my-app', - template: ` -

Hello World

- - ` -} ) -export class AppComponent {} -``` - -> Later on, this component will contain and manage all your applications' notifications. - -
- -### 3. Import the styles - -Of course we also need to import the **angular-notifier** styles into our application. Depending on the architecture of your Angular -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 -write your own styles from scratch. - -#### The easy way: Import all the styles - -To import all the styles, simple include either the `~/angular-notifier/styles.(scss|css)` file. It contains the core styles as well as all -the themes and notification types. - -#### The advanced way: Only import the styles actually needed - -To keep the size if your styles as small as possible (improving performance for the perfect UX), your might instead decide to only import -the styles actually needed by our application. The **angular-notifier** styles are modular: - -- The `~/angular-notifier/styles/core.(scss|css)` file is always required, it defines the basic styles (such as the layout) -- Themes can be imported from the `~/angular-notifier/styles/theme` folder -- The different notification types, then, can be imported from the `~/angular-notifier/styles/types` folder - -


- -## How to use - -Using **angular-notifier** is as simple as it can get -- simple import and inject the `NotifierService` into every component (directive, -service, ...) you want to use in. For example: - -``` typescript -import { NotifierService } from 'angular-notifier'; - -@Component( { - // ... -} ) -export class MyAwesomeComponent { - - private readonly notifier: NotifierService; - - constructor( notifierService: NotifierService ) { - this.notifier = notifierService; - } - -} -``` - -
- -### Show notifications - -Showing a notification is simple - all your need is a type, and a message to be displayed. For example: - -``` typescript -this.notifier.notify( 'success', 'You are awesome! I mean it!' ); -``` - -You can further pass in a *notification ID* as the third (optional) argument. Essentially, such a *notification ID* is nothing more but a -unique string tha can be used later on to gain access (and thus control) to this specific notification. For example: - -``` typescript -this.notifier.notify( 'success', 'You are awesome! I mean it!', 'THAT_NOTIFICATION_ID' ); -``` - -> 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 -> exact* notification. - -**The syntax above is actually just a shorthand version of the following:** - -``` typescript -this.notifier.show( { - type: 'success', - message: 'You are awesome! I mean it!', - id: 'THAT_NOTIFICATION_ID' // Again, this is optional -} ); -``` - -
- -### Hide notifications - -You can also hide notifications. To hide a specific notification - assuming you've defined a *notification ID* when creating it, simply -call: - -``` typescript -this.notifier.hide( 'THAT_NOTIFICATION_ID' ); -``` - -Furthermore, your can hide the newest notification by calling: - -``` typescript -this.notifier.hideNewest(); -``` - -Or, your could hide the oldest notification: - -``` typescript -this.notifier.hideOldest(); -``` - -And, of course, it's also possible to hide all visible notifications at once: - -``` typescript -this.notifier.hideAll(); -``` - -


- -## How to customize - -From the beginning, the **angular-notifier** library has been written with customizability in mind. The idea is that **angular-notifier** -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 -should already provide a great User Experience. - -> Keep in mind that **angular-notifier** can be configured only once - which is at the time you import the `NotifierModule` into your root -> (app) module. - -
- -### Position - -With the `position` property you can define where exactly notifications will appear on the screen: - -``` typescript -position: { - - horizontal: { - - /** - * Defines the horizontal position on the screen - * @type {'left' | 'middle' | 'right'} - */ - position: 'left', - - /** - * Defines the horizontal distance to the screen edge (in px) - * @type {number} - */ - distance: 12 - - }, - - vertical: { - - /** - * Defines the vertical position on the screen - * @type {'top' | 'bottom'} - */ - position: 'bottom', - - /** - * Defines the vertical distance to the screen edge (in px) - * @type {number} - */ - distance: 12 - - /** - * Defines the vertical gap, existing between multiple notifications (in px) - * @type {number} - */ - gap: 10 - - } - -} -``` - -
- -### Theme - -With the `theme` property you can change the overall look and feel of your notifications: - -``` typescript -/** - * Defines the notification theme, responsible for the Visual Design of notifications - * @type {string} - */ -theme: 'material'; -``` - -#### Theming in detail - -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 -when being created. For example, using `material` as the theme results in all notifications getting a class assigned named `x-notifier__notification--material`. - -> Everyone - yes, I'm looking at you - can use this mechanism to write custom notification themes and apply them via the `theme` property. -> 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 -> `material` theme). - -
- -### Behaviour - -With the `behaviour` property you can define how notifications will behave in different situations: - -``` typescript -behaviour: { - - /** - * Defines whether each notification will hide itself automatically after a timeout passes - * @type {number | false} - */ - autoHide: 5000, - - /** - * Defines what happens when someone clicks on a notification - * @type {'hide' | false} - */ - onClick: false, - - /** - * Defines what happens when someone hovers over a notification - * @type {'pauseAutoHide' | 'resetAutoHide' | false} - */ - onMouseover: 'pauseAutoHide', - - /** - * Defines whether the dismiss button is visible or not - * @type {boolean} - */ - showDismissButton: true, - - /** - * Defines whether multiple notification will be stacked, and how high the stack limit is - * @type {number | false} - */ - stacking: 4 - -} -``` - -
- -### Animations - -With the `animations` property your can define whether and how exactly notification will be animated: - -``` typescript -animations: { - - /** - * Defines whether all (!) animations are enabled or disabled - * @type {boolean} - */ - enabled: true, - - show: { - - /** - * Defines the animation preset that will be used to animate a new notification in - * @type {'fade' | 'slide'} - */ - preset: 'slide', - - /** - * Defines how long it will take to animate a new notification in (in ms) - * @type {number} - */ - speed: 300, - - /** - * Defines which easing method will be used when animating a new notification in - * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'} - */ - easing: 'ease' - - }, - - hide: { - - /** - * Defines the animation preset that will be used to animate a new notification out - * @type {'fade' | 'slide'} - */ - preset: 'fade', - - /** - * Defines how long it will take to animate a new notification out (in ms) - * @type {number} - */ - speed: 300, - - /** - * Defines which easing method will be used when animating a new notification out - * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'} - */ - easing: 'ease', - - /** - * Defines the animation offset used when hiding multiple notifications at once (in ms) - * @type {number | false} - */ - offset: 50 - - }, - - shift: { - - /** - * Defines how long it will take to shift a notification around (in ms) - * @type {number} - */ - speed: 300, - - /** - * Defines which easing method will be used when shifting a notification around - * @type {string} - */ - easing: 'ease' // All standard CSS easing methods work - - }, - - /** - * Defines the overall animation overlap, allowing for much smoother looking animations (in ms) - * @type {number | false} - */ - overlap: 150 - -} -``` - -
- -### In short -- the default configuration - -To sum it up, the following is the default configuration *(copy-paste-friendly)*: - -``` typescript -const notifierDefaultOptions: NotifierOptions = { - position: { - horizontal: { - position: 'left', - distance: 12 - }, - vertical: { - position: 'bottom', - distance: 12, - gap: 10 - } - }, - theme: 'material', - behaviour: { - autoHide: 5000, - onClick: false, - onMouseover: 'pauseAutoHide', - showDismissButton: true, - stacking: 4 - }, - animations: { - enabled: true, - show: { - preset: 'slide', - speed: 300, - easing: 'ease' - }, - hide: { - preset: 'fade', - speed: 300, - easing: 'ease', - offset: 50 - }, - shift: { - speed: 300, - easing: 'ease' - }, - overlap: 150 - } -}; -``` - -


- -## Creator - -**Dominique Müller** - -- E-Mail: **[dominique.m.mueller@gmail.com](mailto:dominique.m.mueller@gmail.com)** -- Website: **[www.devdom.io](https://www.devdom.io)** -- Twitter: **[@itsdevdom](https://twitter.com/itsdevdom)** +
+ +# angular-notifier + +**A well designed, fully animated, highly customizable, and easy-to-use notification library for your **Angular 2+** application.** + +[![npm version](https://img.shields.io/npm/v/angular-notifier.svg?maxAge=3600&style=flat)](https://www.npmjs.com/package/angular-notifier) +[![dependency status](https://img.shields.io/david/dominique-mueller/angular-notifier.svg?maxAge=3600&style=flat)](https://david-dm.org/dominique-mueller/angular-notifier) +[![travis ci build status](https://img.shields.io/travis/dominique-mueller/angular-notifier/master.svg?maxAge=3600&style=flat)](https://travis-ci.org/dominique-mueller/angular-notifier) +[![Codecov](https://img.shields.io/codecov/c/github/dominique-mueller/angular-notifier.svg?maxAge=3600&style=flat)](https://codecov.io/gh/dominique-mueller/angular-notifier) +[![Known Vulnerabilities](https://snyk.io/test/github/dominique-mueller/angular-notifier/badge.svg)](https://snyk.io/test/github/dominique-mueller/simple-progress-webpack-plugin) +[![license](https://img.shields.io/npm/l/angular-notifier.svg?maxAge=3600&style=flat)](https://github.com/dominique-mueller/angular-notifier/LICENSE) + +
+ +

+ +## Demo + +You can play around with this library with **[this Stackblitz right here](https://stackblitz.com/edit/angular-notifier-demo)**. + +![Angular Notifier Animated Preview GIF](/docs/angular-notifier-preview.gif?raw=true) + +


+ +## How to install + +You can get **angular-notifier** via **npm** by either adding it as a new *dependency* to your `package.json` file and running npm install, +or running the following command: + +``` bash +npm install angular-notifier +``` + +
+ +### Angular versions + +The following list describes the compatibility with Angular: + +| Angular Notifier | Angular | +| ----------------- | ------- | +| `1.x` | `2.x` | +| `2.x` | `4.x` | +| `3.x` | `5.x` | +| `4.x` | `6.x` | + +
+ +### Browser support & polyfills + +By default, meaning without any polyfills, **angular-notifier** is compatible with **the latest versions of Chrome, Firefox, and Opera**. +Bringing in the following polyfills will improve browser support: + +- To be able to use the latest and greatest JavaScript features in older browsers (e.g. older version of IE & Safari), you might want to +add **[core-js](https://github.com/zloirock/core-js)** to your polyfills. +- For animation support (in particular, for better +**[Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)** support), you might want to use the **[web-animations-js](https://github.com/web-animations/web-animations-js)** polyfill. For details, see the corresponding +**[CanIUse page](http://caniuse.com/#feat=web-animation)**. + +> For detailed information about the Angular browser support read the +**[official Angular browser support documentation](https://angular.io/docs/ts/latest/guide/browser-support.html)**. If you generated your +Angular project with the **[Angular CLI](https://github.com/angular/angular-cli)**, all the polyfills mentioned above do already exist in +your `polyfills.ts` file - waiting for you to enable and install them. + +


+ +## How to setup + +Before actually being able to use the **angular-notifier** library within our code, we have to first set it up within Angular, and also +bring the styles into our project. + +
+ +### 1. Import the `NotifierModule` + +First of all, make **angular-notifier** globally available to your Angular application by importing (and optionally also configuring) the +`NotifierModule` the your root Angular module. For example: + +``` typescript +import { NotifierModule } from 'angular-notifier'; + +@NgModule( { + imports: [ + NotifierModule + ] +} ) +export class AppModule {} +``` + +But wait -- your probably might want to customize your notifications' look and behaviour according to your requirements and needs. To do so, +call the `withConfig` method on the `NotifierModule`, and pass in the options. For example: + +``` typescript +import { NotifierModule } from 'angular-notifier'; + +@NgModule( { + imports: [ + NotifierModule.withConfig( { + // Custom options in here + } ) + ] +} ) +export class AppModule {} +``` + +
+ +### 2. Use the `notifier-container` component + +In addition, you have to place the `notifier-container` component somewhere in your application, best at the last element of your +root (app) component. For example: + +``` typescript +@Component( { + selector: 'my-app', + template: ` +

Hello World

+ + ` +} ) +export class AppComponent {} +``` + +> Later on, this component will contain and manage all your applications' notifications. + +
+ +### 3. Import the styles + +Of course we also need to import the **angular-notifier** styles into our application. Depending on the architecture of your Angular +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 +write your own styles from scratch. + +#### The easy way: Import all the styles + +To import all the styles, simple include either the `~/angular-notifier/styles.(scss|css)` file. It contains the core styles as well as all +the themes and notification types. + +#### The advanced way: Only import the styles actually needed + +To keep the size if your styles as small as possible (improving performance for the perfect UX), your might instead decide to only import +the styles actually needed by our application. The **angular-notifier** styles are modular: + +- The `~/angular-notifier/styles/core.(scss|css)` file is always required, it defines the basic styles (such as the layout) +- Themes can be imported from the `~/angular-notifier/styles/theme` folder +- The different notification types, then, can be imported from the `~/angular-notifier/styles/types` folder + +


+ +## How to use + +Using **angular-notifier** is as simple as it can get -- simple import and inject the `NotifierService` into every component (directive, +service, ...) you want to use in. For example: + +``` typescript +import { NotifierService } from 'angular-notifier'; + +@Component( { + // ... +} ) +export class MyAwesomeComponent { + + private readonly notifier: NotifierService; + + constructor( notifierService: NotifierService ) { + this.notifier = notifierService; + } + +} +``` + +
+ +### Show notifications + +Showing a notification is simple - all your need is a type, and a message to be displayed. For example: + +``` typescript +this.notifier.notify( 'success', 'You are awesome! I mean it!' ); +``` + +You can further pass in a *notification ID* as the third (optional) argument. Essentially, such a *notification ID* is nothing more but a +unique string tha can be used later on to gain access (and thus control) to this specific notification. For example: + +``` typescript +this.notifier.notify( 'success', 'You are awesome! I mean it!', 'THAT_NOTIFICATION_ID' ); +``` + +> 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 +> exact* notification. + +**The syntax above is actually just a shorthand version of the following:** + +``` typescript +this.notifier.show( { + type: 'success', + message: 'You are awesome! I mean it!', + id: 'THAT_NOTIFICATION_ID' // Again, this is optional +} ); +``` + +
+ +### Hide notifications + +You can also hide notifications. To hide a specific notification - assuming you've defined a *notification ID* when creating it, simply +call: + +``` typescript +this.notifier.hide( 'THAT_NOTIFICATION_ID' ); +``` + +Furthermore, your can hide the newest notification by calling: + +``` typescript +this.notifier.hideNewest(); +``` + +Or, your could hide the oldest notification: + +``` typescript +this.notifier.hideOldest(); +``` + +And, of course, it's also possible to hide all visible notifications at once: + +``` typescript +this.notifier.hideAll(); +``` + +


+ +## How to customize + +From the beginning, the **angular-notifier** library has been written with customizability in mind. The idea is that **angular-notifier** +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 +should already provide a great User Experience. + +> Keep in mind that **angular-notifier** can be configured only once - which is at the time you import the `NotifierModule` into your root +> (app) module. + +
+ +### Position + +With the `position` property you can define where exactly notifications will appear on the screen: + +``` typescript +position: { + + horizontal: { + + /** + * Defines the horizontal position on the screen + * @type {'left' | 'middle' | 'right'} + */ + position: 'left', + + /** + * Defines the horizontal distance to the screen edge (in px) + * @type {number} + */ + distance: 12 + + }, + + vertical: { + + /** + * Defines the vertical position on the screen + * @type {'top' | 'bottom'} + */ + position: 'bottom', + + /** + * Defines the vertical distance to the screen edge (in px) + * @type {number} + */ + distance: 12 + + /** + * Defines the vertical gap, existing between multiple notifications (in px) + * @type {number} + */ + gap: 10 + + } + +} +``` + +
+ +### Theme + +With the `theme` property you can change the overall look and feel of your notifications: + +``` typescript +/** + * Defines the notification theme, responsible for the Visual Design of notifications + * @type {string} + */ +theme: 'material'; +``` + +#### Theming in detail + +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 +when being created. For example, using `material` as the theme results in all notifications getting a class assigned named `x-notifier__notification--material`. + +> Everyone - yes, I'm looking at you - can use this mechanism to write custom notification themes and apply them via the `theme` property. +> 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 +> `material` theme). + +
+ +### Behaviour + +With the `behaviour` property you can define how notifications will behave in different situations: + +``` typescript +behaviour: { + + /** + * Defines whether each notification will hide itself automatically after a timeout passes + * @type {number | false} + */ + autoHide: 5000, + + /** + * Defines what happens when someone clicks on a notification + * @type {'hide' | false} + */ + onClick: false, + + /** + * Defines what happens when someone hovers over a notification + * @type {'pauseAutoHide' | 'resetAutoHide' | false} + */ + onMouseover: 'pauseAutoHide', + + /** + * Defines whether the dismiss button is visible or not + * @type {boolean} + */ + showDismissButton: true, + + /** + * Defines whether multiple notification will be stacked, and how high the stack limit is + * @type {number | false} + */ + stacking: 4 + +} +``` + +
+ +### Custom Templates + +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`. + +You can define a custom `ng-template` as follows: + +```html + + + {{ notificationData.message }} + + +``` + +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. + +Inside your component, you can then reference the `` by its template variable `#customNotification` using Angular's `ViewChild`: + +``` +import { ViewChild } from '@angular/core' +... +@Component({...}) +export class SomeComponent { + @ViewChild('customNotification') customNotificationTmpl; + ... + constructor(private notifierService: NotifierService) {} + + showNotification() { + const msg = { + message: 'Hi there!', + type: 'info' + }; + + this.notifier.show({ + message: msg.message, + type: msg.messageType, + template: this.customNotificationTmpl + }); + } +} +``` + +
+ +### Animations + +With the `animations` property your can define whether and how exactly notification will be animated: + +``` typescript +animations: { + + /** + * Defines whether all (!) animations are enabled or disabled + * @type {boolean} + */ + enabled: true, + + show: { + + /** + * Defines the animation preset that will be used to animate a new notification in + * @type {'fade' | 'slide'} + */ + preset: 'slide', + + /** + * Defines how long it will take to animate a new notification in (in ms) + * @type {number} + */ + speed: 300, + + /** + * Defines which easing method will be used when animating a new notification in + * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'} + */ + easing: 'ease' + + }, + + hide: { + + /** + * Defines the animation preset that will be used to animate a new notification out + * @type {'fade' | 'slide'} + */ + preset: 'fade', + + /** + * Defines how long it will take to animate a new notification out (in ms) + * @type {number} + */ + speed: 300, + + /** + * Defines which easing method will be used when animating a new notification out + * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'} + */ + easing: 'ease', + + /** + * Defines the animation offset used when hiding multiple notifications at once (in ms) + * @type {number | false} + */ + offset: 50 + + }, + + shift: { + + /** + * Defines how long it will take to shift a notification around (in ms) + * @type {number} + */ + speed: 300, + + /** + * Defines which easing method will be used when shifting a notification around + * @type {string} + */ + easing: 'ease' // All standard CSS easing methods work + + }, + + /** + * Defines the overall animation overlap, allowing for much smoother looking animations (in ms) + * @type {number | false} + */ + overlap: 150 + +} +``` + +
+ +### In short -- the default configuration + +To sum it up, the following is the default configuration *(copy-paste-friendly)*: + +``` typescript +const notifierDefaultOptions: NotifierOptions = { + position: { + horizontal: { + position: 'left', + distance: 12 + }, + vertical: { + position: 'bottom', + distance: 12, + gap: 10 + } + }, + theme: 'material', + behaviour: { + autoHide: 5000, + onClick: false, + onMouseover: 'pauseAutoHide', + showDismissButton: true, + stacking: 4 + }, + animations: { + enabled: true, + show: { + preset: 'slide', + speed: 300, + easing: 'ease' + }, + hide: { + preset: 'fade', + speed: 300, + easing: 'ease', + offset: 50 + }, + shift: { + speed: 300, + easing: 'ease' + }, + overlap: 150 + } +}; +``` + +


+ +## Creator + +**Dominique Müller** + +- E-Mail: **[dominique.m.mueller@gmail.com](mailto:dominique.m.mueller@gmail.com)** +- Website: **[www.devdom.io](https://www.devdom.io)** +- Twitter: **[@itsdevdom](https://twitter.com/itsdevdom)** diff --git a/src/demo/app.component.ts b/src/demo/app.component.ts index a5477631..3fea6127 100644 --- a/src/demo/app.component.ts +++ b/src/demo/app.component.ts @@ -1,126 +1,153 @@ -import { Component } from '@angular/core'; - -import { NotifierService } from './../lib/index'; - -/** - * App component - */ -@Component( { - host: { - class: 'app' - }, - selector: 'app', - template: ` -

"angular-notifier" demo

- -

Show notifications

- - - - - - -

Hide notifications

- - - - -

Show & hide a specific notification

- - - - - ` -} ) -export class AppComponent { - - /** - * Notifier service - */ - private notifier: NotifierService; - - /** - * Constructor - * - * @param {NotifierService} notifier Notifier service - */ - public constructor( notifier: NotifierService ) { - this.notifier = notifier; - } - - /** - * Show a notification - * - * @param {string} type Notification type - * @param {string} message Notification message - */ - public showNotification( type: string, message: string ): void { - this.notifier.notify( type, message ); - } - - /** - * Hide oldest notification - */ - public hideOldestNotification(): void { - this.notifier.hideOldest(); - } - - /** - * Hide newest notification - */ - public hideNewestNotification(): void { - this.notifier.hideNewest(); - } - - /** - * Hide all notifications at once - */ - public hideAllNotifications(): void { - this.notifier.hideAll(); - } - - /** - * Show a specific notification (with a custom notification ID) - * - * @param {string} type Notification type - * @param {string} message Notification message - * @param {string} id Notification ID - */ - public showSpecificNotification( type: string, message: string, id: string ): void { - this.notifier.show( { - id, - message, - type - } ); - } - - /** - * Hide a specific notification (by a given notification ID) - * - * @param {string} id Notification ID - */ - public hideSpecificNotification( id: string ): void { - this.notifier.hide( id ); - } - -} +import { Component, ViewChild } from '@angular/core'; + +import { NotifierService } from './../lib/index'; + +/** + * App component + */ +@Component({ + host: { + class: 'app' + }, + selector: 'app', + template: ` +

"angular-notifier" demo

+ +

Show notifications

+ + + + + + + +

Hide notifications

+ + + + +

Show & hide a specific notification

+ + + + + + +
+ {{ notification.type}}: {{ notification.message }} +
+
+ + ` +}) +export class AppComponent { + @ViewChild('customTemplate') customNotificationTmpl; + + /** + * Notifier service + */ + private notifier: NotifierService; + + /** + * Constructor + * + * @param {NotifierService} notifier Notifier service + */ + public constructor(notifier: NotifierService) { + this.notifier = notifier; + } + + /** + * Show a notification + * + * @param {string} type Notification type + * @param {string} message Notification message + */ + public showNotification(type: string, message: string): void { + this.notifier.notify(type, message); + } + + /** + * Hide oldest notification + */ + public hideOldestNotification(): void { + this.notifier.hideOldest(); + } + + /** + * Hide newest notification + */ + public hideNewestNotification(): void { + this.notifier.hideNewest(); + } + + /** + * Hide all notifications at once + */ + public hideAllNotifications(): void { + this.notifier.hideAll(); + } + + public showCustomNotificationTemplate( + type: string, + message: string, + id: string + ): void { + this.notifier.show({ + id, + message, + type, + template: this.customNotificationTmpl + }); + } + + /** + * Show a specific notification (with a custom notification ID) + * + * @param {string} type Notification type + * @param {string} message Notification message + * @param {string} id Notification ID + */ + public showSpecificNotification( + type: string, + message: string, + id: string + ): void { + this.notifier.show({ + id, + message, + type + }); + } + + /** + * Hide a specific notification (by a given notification ID) + * + * @param {string} id Notification ID + */ + public hideSpecificNotification(id: string): void { + this.notifier.hide(id); + } +} diff --git a/src/lib/src/components/notifier-notification.component.html b/src/lib/src/components/notifier-notification.component.html index 275152f7..d4127330 100644 --- a/src/lib/src/components/notifier-notification.component.html +++ b/src/lib/src/components/notifier-notification.component.html @@ -1,7 +1,11 @@ -

{{ notification.message }}

- + + + + +

{{ notification.message }}

+ +
diff --git a/src/lib/src/components/notifier-notification.component.spec.ts b/src/lib/src/components/notifier-notification.component.spec.ts index 147c1718..f423c65b 100644 --- a/src/lib/src/components/notifier-notification.component.spec.ts +++ b/src/lib/src/components/notifier-notification.component.spec.ts @@ -1,1067 +1,1121 @@ -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; - -import { NotifierConfigToken } from '../notifier.module'; -import { NotifierAnimationData } from '../models/notifier-animation.model'; -import { NotifierNotification } from '../models/notifier-notification.model'; -import { NotifierConfig } from '../models/notifier-config.model'; -import { NotifierAnimationService } from '../services/notifier-animation.service'; -import { NotifierService } from './../services/notifier.service'; -import { NotifierNotificationComponent } from './notifier-notification.component'; -import { NotifierTimerService } from '../services/notifier-timer.service'; - -/** - * Notifier Notification Component - Unit Test - */ -describe( 'Notifier Notification Component', () => { - - // tslint:disable no-any - const fakeAnimation: any = { - onfinish: () => null // We only need this property to be actually mocked away - }; - // tslint:enable no-any - - const testNotification: NotifierNotification = new NotifierNotification( { - id: 'ID_FAKE', - message: 'Lorem ipsum dolor sit amet.', - type: 'SUCCESS' - } ); - - let componentFixture: ComponentFixture; - let componentInstance: NotifierNotificationComponent; - - let timerService: MockNotifierTimerService; - - it( 'should instantiate', () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig() ); - - expect( componentInstance ).toBeDefined(); - - } ); - - describe( '(render)', () => { - - it( 'should render', () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig() ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Check the calculated values - expect( componentInstance.getConfig() ).toEqual( new NotifierConfig() ); - expect( componentInstance.getHeight() ).toBe( componentFixture.nativeElement.offsetHeight ); - expect( componentInstance.getWidth() ).toBe( componentFixture.nativeElement.offsetWidth ); - expect( componentInstance.getShift() ).toBe( 0 ); - - // Check the template - const messageElement: DebugElement = componentFixture.debugElement.query( By.css( '.notifier__notification-message' ) ); - expect( messageElement.nativeElement.textContent ).toContain( componentInstance.notification.message ); - const dismissButtonElement: DebugElement = componentFixture.debugElement.query( By.css( '.notifier__notification-button' ) ); - expect( dismissButtonElement ).not.toBeNull(); - - // Check the class names - const classNameType: string = `notifier__notification--${ componentInstance.notification.type }`; - expect( componentFixture.nativeElement.classList.contains( classNameType ) ).toBeTruthy(); - const classNameTheme: string = `notifier__notification--${ componentInstance.getConfig().theme }`; - expect( componentFixture.nativeElement.classList.contains( classNameTheme ) ).toBeTruthy(); - - } ); - - it( 'should render on the left', () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - position: { - horizontal: { - distance: 10, - position: 'left' - }, - vertical: { - distance: 10, - gap: 4, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Check position - expect( componentFixture.debugElement.styles[ 'left' ] ).toBe( `${ testNotifierConfig.position.horizontal.distance }px` ); - - } ); - - it( 'should render on the right', () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - position: { - horizontal: { - distance: 10, - position: 'right' - }, - vertical: { - distance: 10, - gap: 4, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Check position - expect( componentFixture.debugElement.styles[ 'right' ] ).toBe( `${ testNotifierConfig.position.horizontal.distance }px` ); - - } ); - - it( 'should render in the middle', () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - position: { - horizontal: { - distance: 10, - position: 'middle' - }, - vertical: { - distance: 10, - gap: 4, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Check position - expect( componentFixture.debugElement.styles[ 'left' ] ).toBe( '50%' ); - expect( componentFixture.debugElement.styles[ 'transform' ] ).toBe( 'translate3d( -50%, 0, 0 )' ); - - } ); - - it( 'should render on the top', () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - position: { - horizontal: { - distance: 10, - position: 'left' - }, - vertical: { - distance: 10, - gap: 4, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Check position - expect( componentFixture.debugElement.styles[ 'top' ] ).toBe( `${ testNotifierConfig.position.vertical.distance }px` ); - - } ); - - it( 'should render on the bottom', () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - position: { - horizontal: { - distance: 10, - position: 'left' - }, - vertical: { - distance: 10, - gap: 4, - position: 'bottom' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Check position - expect( componentFixture.debugElement.styles[ 'bottom' ] ).toBe( `${ testNotifierConfig.position.vertical.distance }px` ); - - } ); - - } ); - - describe( '(show)', () => { - - it( 'should show', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const showCallback: () => {} = jest.fn(); - componentInstance.show().then( showCallback ); - tick(); - - expect( componentFixture.debugElement.styles[ 'visibility' ] ).toBe( 'visible' ); - expect( showCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should show (with animations)', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - behaviour: { - autoHide: false - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Mock away the Web Animations API - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'opacity' ] = '1'; // Fake animation result - return fakeAnimation; - } ); - - const showCallback: () => {} = jest.fn(); - componentInstance.show().then( showCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'visibility' ] ).toBe( 'visible' ); - expect( componentFixture.debugElement.styles[ 'opacity' ] ).toBe( '1' ); - expect( showCallback ).toHaveBeenCalled(); - - } ) ); - - } ); - - describe( '(hide)', () => { - - it( 'should hide', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const hideCallback: () => {} = jest.fn(); - componentInstance.hide().then( hideCallback ); - tick(); - - expect( hideCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should hide (with animations)', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - behaviour: { - autoHide: false - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Mock away the Web Animations API - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'opacity' ] = '0'; // Fake animation result - return fakeAnimation; - } ); - - const hideCallback: () => {} = jest.fn(); - componentInstance.hide().then( hideCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'opacity' ] ).toBe( '0' ); - expect( hideCallback ).toHaveBeenCalled(); - - } ) ); - - } ); - - describe( '(shift)', () => { - - it( 'should shift to make place on top', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const shiftCallback: () => {} = jest.fn(); - const shiftDistance: number = 100; - componentInstance.shift( shiftDistance, true ).then( shiftCallback ); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to make place on top (with animations)', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const shiftDistance: number = 100; - - // Mock away the Web Animations API - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'transform' ] = - `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result - return fakeAnimation; - } ); - - const shiftCallback: () => {} = jest.fn(); - componentInstance.shift( shiftDistance, true ).then( shiftCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to make place on bottom', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'bottom' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const shiftCallback: () => {} = jest.fn(); - const shiftDistance: number = 100; - componentInstance.shift( shiftDistance, true ).then( shiftCallback ); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to make place on bottom (with animations)', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'bottom' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Mock away the Web Animations API - const shiftDistance: number = 100; - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'transform' ] = - `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result - return fakeAnimation; - } ); - - const shiftCallback: () => {} = jest.fn(); - componentInstance.shift( shiftDistance, true ).then( shiftCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to fill place on top', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const shiftCallback: () => {} = jest.fn(); - const shiftDistance: number = 100; - componentInstance.shift( shiftDistance, false ).then( shiftCallback ); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to fill place on top (with animations)', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Mock away the Web Animations API - const shiftDistance: number = 100; - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'transform' ] = - `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result - return fakeAnimation; - } ); - - const shiftCallback: () => {} = jest.fn(); - componentInstance.shift( shiftDistance, false ).then( shiftCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ 0 - shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to fill place on bottom', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'bottom' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const shiftCallback: () => {} = jest.fn(); - const shiftDistance: number = 100; - componentInstance.shift( shiftDistance, false ).then( shiftCallback ); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to fill place on bottom (with animations)', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'bottom' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Mock away the Web Animations API - const shiftDistance: number = 100; - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'transform' ] = - `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result - return fakeAnimation; - } ); - - const shiftCallback: () => {} = jest.fn(); - componentInstance.shift( shiftDistance, false ).then( shiftCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to make place in the middle', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'middle' - }, - vertical: { - distance: 12, - gap: 10, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - const shiftCallback: () => {} = jest.fn(); - const shiftDistance: number = 100; - componentInstance.shift( shiftDistance, true ).then( shiftCallback ); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( -50%, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - it( 'should shift to make place in the middle (with animations)', fakeAsync( () => { - - // Setup test module - const testNotifierConfig: NotifierConfig = new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false - }, - position: { - horizontal: { - distance: 12, - position: 'middle' - }, - vertical: { - distance: 12, - gap: 10, - position: 'top' - } - } - } ); - beforeEachWithConfig( testNotifierConfig ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - // Mock away the Web Animations API - const shiftDistance: number = 100; - jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { - componentFixture.debugElement.styles[ 'transform' ] = - `translate3d( -50%, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result - return fakeAnimation; - } ); - - const shiftCallback: () => {} = jest.fn(); - componentInstance.shift( shiftDistance, true ).then( shiftCallback ); - fakeAnimation.onfinish(); - tick(); - - expect( componentFixture.debugElement.styles[ 'transform' ] ) - .toBe( `translate3d( -50%, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); - expect( shiftCallback ).toHaveBeenCalled(); - - } ) ); - - } ); - - describe( '(behaviour)', () => { - - it( 'should hide automatically after timeout', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: 5000 - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - componentInstance.show(); - jest.spyOn( componentInstance, 'onClickDismiss' ); - tick(); - - timerService.finishManually(); - tick(); - - expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); - - } ) ); - - it( 'should hide after clicking the dismiss button', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false, - showDismissButton: true - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - componentInstance.show(); - jest.spyOn( componentInstance, 'onClickDismiss' ); - - const dismissButtonElement: DebugElement = componentFixture.debugElement.query( By.css( '.notifier__notification-button' ) ); - dismissButtonElement.nativeElement.click(); // Emulate click event - componentFixture.detectChanges(); - - expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); - - } ) ); - - it( 'should hide after clicking on the notification', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false, - onClick: 'hide' - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - componentInstance.show(); - jest.spyOn( componentInstance, 'onClickDismiss' ); - - componentFixture.nativeElement.click(); // Emulate click event - componentFixture.detectChanges(); - - expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); - - } ) ); - - it( 'should not hide after clicking on the notification', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: false, - onClick: false - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - componentInstance.show(); - jest.spyOn( componentInstance, 'onClickDismiss' ); - - componentFixture.nativeElement.click(); // Emulate click event - componentFixture.detectChanges(); - - expect( componentInstance.onClickDismiss ).not.toHaveBeenCalled(); - - } ) ); - - it( 'should pause the autoHide timer on mouseover, and resume again on mouseout', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: 5000, - onMouseover: 'pauseAutoHide' - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - componentInstance.show(); - jest.spyOn( componentInstance, 'onClickDismiss' ); - jest.spyOn( timerService, 'pause' ); - jest.spyOn( timerService, 'continue' ); - - componentInstance.onNotificationMouseover(); - - expect( timerService.pause ).toHaveBeenCalled(); - - componentInstance.onNotificationMouseout(); - - expect( timerService.continue ).toHaveBeenCalled(); - - timerService.finishManually(); - tick(); - - expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); - - } ) ); - - it( 'should restart the autoHide timer on mouseover', fakeAsync( () => { - - // Setup test module - beforeEachWithConfig( new NotifierConfig( { - animations: { - enabled: false - }, - behaviour: { - autoHide: 5000, - onMouseover: 'resetAutoHide' - } - } ) ); - - componentInstance.notification = testNotification; - componentFixture.detectChanges(); - - componentInstance.show(); - jest.spyOn( componentInstance, 'onClickDismiss' ); - jest.spyOn( timerService, 'stop' ); - jest.spyOn( timerService, 'start' ); - - componentInstance.onNotificationMouseover(); - - expect( timerService.stop ).toHaveBeenCalled(); - - componentInstance.onNotificationMouseout(); - - expect( timerService.start ).toHaveBeenCalled(); - - timerService.finishManually(); - tick(); - - expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); - - } ) ); - - } ); - - /** - * Helper for upfront configuration - */ - function beforeEachWithConfig( testNotifierConfig: NotifierConfig ): void { - - TestBed - .configureTestingModule( { - declarations: [ - NotifierNotificationComponent - ], - providers: [ - { - provide: NotifierService, - useValue: { - getConfig: () => testNotifierConfig - } - }, - { // No idea why this is *actually* necessary -- it shouldn't be ... - provide: NotifierConfigToken, - useValue: {} - }, - { - provide: NotifierAnimationService, - useClass: MockNotifierAnimationService - } - ] - } ) - .overrideComponent( NotifierNotificationComponent, { - set: { - providers: [ // Override component-specific providers - { - provide: NotifierTimerService, - useClass: MockNotifierTimerService - } - ] - } - } ); - componentFixture = TestBed.createComponent( NotifierNotificationComponent ); - componentInstance = componentFixture.componentInstance; - - // Get the service from the component's local injector - timerService = componentFixture.debugElement.injector.get( NotifierTimerService ); - - } - -} ); - -/** - * Mock notifier animation service, always returning the animation - */ -class MockNotifierAnimationService extends NotifierAnimationService { - - /** - * Get animation data - * - * @param {'show' | 'hide'} direction Animation direction, either in or out - * @param {NotifierNotification} notification Notification the animation data should be generated for - * @returns {NotifierAnimationData} Animation information - * - * @override - */ - public getAnimationData( direction: 'show' | 'hide', notification: NotifierNotification ): NotifierAnimationData { - if ( direction === 'show' ) { - return { - keyframes: [ - { - opacity: '0' - }, - { - opacity: '1' - } - ], - options: { - duration: 300, - easing: 'ease', - fill: 'forwards' - } - }; - } else { - return { - keyframes: [ - { - opacity: '1' - }, - { - opacity: '0' - } - ], - options: { - duration: 300, - easing: 'ease', - fill: 'forwards' - } - }; - } - } - -} - -/** - * Mock Notifier Timer Service - */ -class MockNotifierTimerService extends NotifierTimerService { - - /** - * Temp resolve function - * - * @override - */ - private resolveFunction: Function; - - /** - * Start (or resume) the timer - doing nothing here - * - * @param {number} duration Timer duration, in ms - * @returns {Promise} Promise, resolved once the timer finishes - * - * @override - */ - public start( duration: number ): Promise { - return new Promise( ( resolve: () => void, reject: () => void ) => { - this.resolveFunction = resolve; - } ); - } - - /** - * Pause the timer - doing nothing here - */ - public pause(): void { - // Do nothing - } - - /** - * Continue the timer - doing nothing here - */ - public continue(): void { - // Do nothing - } - - /** - * Stop the timer - doing nothing here - */ - public stop(): void { - // Do nothing - } - - /** - * Finish the timer manually, from outside - */ - public finishManually(): void { - this.resolveFunction(); - } - -} +import { DebugElement, Component, ViewChild, NO_ERRORS_SCHEMA, TemplateRef } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ComponentFixture, fakeAsync, TestBed, tick, async } from '@angular/core/testing'; + +import { NotifierConfigToken } from '../notifier.module'; +import { NotifierAnimationData } from '../models/notifier-animation.model'; +import { NotifierNotification } from '../models/notifier-notification.model'; +import { NotifierConfig } from '../models/notifier-config.model'; +import { NotifierAnimationService } from '../services/notifier-animation.service'; +import { NotifierService } from './../services/notifier.service'; +import { NotifierNotificationComponent } from './notifier-notification.component'; +import { NotifierTimerService } from '../services/notifier-timer.service'; + +/** + * Notifier Notification Component - Unit Test + */ +describe( 'Notifier Notification Component', () => { + + // tslint:disable no-any + const fakeAnimation: any = { + onfinish: () => null // We only need this property to be actually mocked away + }; + // tslint:enable no-any + + const testNotification: NotifierNotification = new NotifierNotification( { + id: 'ID_FAKE', + message: 'Lorem ipsum dolor sit amet.', + type: 'SUCCESS' + } ); + + let componentFixture: ComponentFixture; + let componentInstance: NotifierNotificationComponent; + + let timerService: MockNotifierTimerService; + + it( 'should instantiate', () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig() ); + + expect( componentInstance ).toBeDefined(); + + } ); + + describe( '(render)', () => { + + it( 'should render', () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig() ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Check the calculated values + expect( componentInstance.getConfig() ).toEqual( new NotifierConfig() ); + expect( componentInstance.getHeight() ).toBe( componentFixture.nativeElement.offsetHeight ); + expect( componentInstance.getWidth() ).toBe( componentFixture.nativeElement.offsetWidth ); + expect( componentInstance.getShift() ).toBe( 0 ); + + // Check the template + const messageElement: DebugElement = componentFixture.debugElement.query( By.css( '.notifier__notification-message' ) ); + expect( messageElement.nativeElement.textContent ).toContain( componentInstance.notification.message ); + const dismissButtonElement: DebugElement = componentFixture.debugElement.query( By.css( '.notifier__notification-button' ) ); + expect( dismissButtonElement ).not.toBeNull(); + + // Check the class names + const classNameType: string = `notifier__notification--${ componentInstance.notification.type }`; + expect( componentFixture.nativeElement.classList.contains( classNameType ) ).toBeTruthy(); + const classNameTheme: string = `notifier__notification--${ componentInstance.getConfig().theme }`; + expect( componentFixture.nativeElement.classList.contains( classNameTheme ) ).toBeTruthy(); + + } ); + + it( 'should render the custom template if provided by the user', async(() => { + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + position: { + horizontal: { + distance: 10, + position: 'left' + }, + vertical: { + distance: 10, + gap: 4, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig, false ); + + const template = `
{{notificationData.message}}
`; + + const testcmp = createTestComponent(template); + + // associate the templateref + const myTestNotification = { + ...testNotification, + template: testcmp.componentInstance.currentTplRef + } + expect(testcmp.componentInstance.currentTplRef).toBeDefined(); + + componentFixture = TestBed.createComponent( NotifierNotificationComponent ); + componentInstance = componentFixture.componentInstance; + + componentInstance.notification = myTestNotification; + componentFixture.detectChanges(); + + // // assert + expect( componentFixture.debugElement.query(By.css('div.custom-notification-body'))).not.toBeNull(); + expect( componentFixture.debugElement.query(By.css('div.custom-notification-body')).nativeElement.innerHTML).toBe(myTestNotification.message); + })); + + it( 'should render on the left', () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + position: { + horizontal: { + distance: 10, + position: 'left' + }, + vertical: { + distance: 10, + gap: 4, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Check position + expect( componentFixture.debugElement.styles[ 'left' ] ).toBe( `${ testNotifierConfig.position.horizontal.distance }px` ); + + } ); + + it( 'should render on the right', () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + position: { + horizontal: { + distance: 10, + position: 'right' + }, + vertical: { + distance: 10, + gap: 4, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Check position + expect( componentFixture.debugElement.styles[ 'right' ] ).toBe( `${ testNotifierConfig.position.horizontal.distance }px` ); + + } ); + + it( 'should render in the middle', () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + position: { + horizontal: { + distance: 10, + position: 'middle' + }, + vertical: { + distance: 10, + gap: 4, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Check position + expect( componentFixture.debugElement.styles[ 'left' ] ).toBe( '50%' ); + expect( componentFixture.debugElement.styles[ 'transform' ] ).toBe( 'translate3d( -50%, 0, 0 )' ); + + } ); + + it( 'should render on the top', () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + position: { + horizontal: { + distance: 10, + position: 'left' + }, + vertical: { + distance: 10, + gap: 4, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Check position + expect( componentFixture.debugElement.styles[ 'top' ] ).toBe( `${ testNotifierConfig.position.vertical.distance }px` ); + + } ); + + it( 'should render on the bottom', () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + position: { + horizontal: { + distance: 10, + position: 'left' + }, + vertical: { + distance: 10, + gap: 4, + position: 'bottom' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Check position + expect( componentFixture.debugElement.styles[ 'bottom' ] ).toBe( `${ testNotifierConfig.position.vertical.distance }px` ); + + } ); + + } ); + + describe( '(show)', () => { + + it( 'should show', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const showCallback: () => {} = jest.fn(); + componentInstance.show().then( showCallback ); + tick(); + + expect( componentFixture.debugElement.styles[ 'visibility' ] ).toBe( 'visible' ); + expect( showCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should show (with animations)', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + behaviour: { + autoHide: false + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Mock away the Web Animations API + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'opacity' ] = '1'; // Fake animation result + return fakeAnimation; + } ); + + const showCallback: () => {} = jest.fn(); + componentInstance.show().then( showCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'visibility' ] ).toBe( 'visible' ); + expect( componentFixture.debugElement.styles[ 'opacity' ] ).toBe( '1' ); + expect( showCallback ).toHaveBeenCalled(); + + } ) ); + + } ); + + describe( '(hide)', () => { + + it( 'should hide', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const hideCallback: () => {} = jest.fn(); + componentInstance.hide().then( hideCallback ); + tick(); + + expect( hideCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should hide (with animations)', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + behaviour: { + autoHide: false + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Mock away the Web Animations API + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'opacity' ] = '0'; // Fake animation result + return fakeAnimation; + } ); + + const hideCallback: () => {} = jest.fn(); + componentInstance.hide().then( hideCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'opacity' ] ).toBe( '0' ); + expect( hideCallback ).toHaveBeenCalled(); + + } ) ); + + } ); + + describe( '(shift)', () => { + + it( 'should shift to make place on top', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const shiftCallback: () => {} = jest.fn(); + const shiftDistance: number = 100; + componentInstance.shift( shiftDistance, true ).then( shiftCallback ); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to make place on top (with animations)', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const shiftDistance: number = 100; + + // Mock away the Web Animations API + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'transform' ] = + `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result + return fakeAnimation; + } ); + + const shiftCallback: () => {} = jest.fn(); + componentInstance.shift( shiftDistance, true ).then( shiftCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to make place on bottom', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'bottom' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const shiftCallback: () => {} = jest.fn(); + const shiftDistance: number = 100; + componentInstance.shift( shiftDistance, true ).then( shiftCallback ); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to make place on bottom (with animations)', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'bottom' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Mock away the Web Animations API + const shiftDistance: number = 100; + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'transform' ] = + `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result + return fakeAnimation; + } ); + + const shiftCallback: () => {} = jest.fn(); + componentInstance.shift( shiftDistance, true ).then( shiftCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to fill place on top', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const shiftCallback: () => {} = jest.fn(); + const shiftDistance: number = 100; + componentInstance.shift( shiftDistance, false ).then( shiftCallback ); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to fill place on top (with animations)', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Mock away the Web Animations API + const shiftDistance: number = 100; + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'transform' ] = + `translate3d( 0, ${ -shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result + return fakeAnimation; + } ); + + const shiftCallback: () => {} = jest.fn(); + componentInstance.shift( shiftDistance, false ).then( shiftCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ 0 - shiftDistance - testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to fill place on bottom', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'bottom' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const shiftCallback: () => {} = jest.fn(); + const shiftDistance: number = 100; + componentInstance.shift( shiftDistance, false ).then( shiftCallback ); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to fill place on bottom (with animations)', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'bottom' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Mock away the Web Animations API + const shiftDistance: number = 100; + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'transform' ] = + `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result + return fakeAnimation; + } ); + + const shiftCallback: () => {} = jest.fn(); + componentInstance.shift( shiftDistance, false ).then( shiftCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( 0, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to make place in the middle', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'middle' + }, + vertical: { + distance: 12, + gap: 10, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + const shiftCallback: () => {} = jest.fn(); + const shiftDistance: number = 100; + componentInstance.shift( shiftDistance, true ).then( shiftCallback ); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( -50%, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + it( 'should shift to make place in the middle (with animations)', fakeAsync( () => { + + // Setup test module + const testNotifierConfig: NotifierConfig = new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false + }, + position: { + horizontal: { + distance: 12, + position: 'middle' + }, + vertical: { + distance: 12, + gap: 10, + position: 'top' + } + } + } ); + beforeEachWithConfig( testNotifierConfig ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + // Mock away the Web Animations API + const shiftDistance: number = 100; + jest.spyOn( componentFixture.nativeElement, 'animate' ).mockImplementation( () => { + componentFixture.debugElement.styles[ 'transform' ] = + `translate3d( -50%, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )`; // Fake animation result + return fakeAnimation; + } ); + + const shiftCallback: () => {} = jest.fn(); + componentInstance.shift( shiftDistance, true ).then( shiftCallback ); + fakeAnimation.onfinish(); + tick(); + + expect( componentFixture.debugElement.styles[ 'transform' ] ) + .toBe( `translate3d( -50%, ${ shiftDistance + testNotifierConfig.position.vertical.gap }px, 0 )` ); + expect( shiftCallback ).toHaveBeenCalled(); + + } ) ); + + } ); + + describe( '(behaviour)', () => { + + it( 'should hide automatically after timeout', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: 5000 + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + componentInstance.show(); + jest.spyOn( componentInstance, 'onClickDismiss' ); + tick(); + + timerService.finishManually(); + tick(); + + expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); + + } ) ); + + it( 'should hide after clicking the dismiss button', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false, + showDismissButton: true + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + componentInstance.show(); + jest.spyOn( componentInstance, 'onClickDismiss' ); + + const dismissButtonElement: DebugElement = componentFixture.debugElement.query( By.css( '.notifier__notification-button' ) ); + dismissButtonElement.nativeElement.click(); // Emulate click event + componentFixture.detectChanges(); + + expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); + + } ) ); + + it( 'should hide after clicking on the notification', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false, + onClick: 'hide' + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + componentInstance.show(); + jest.spyOn( componentInstance, 'onClickDismiss' ); + + componentFixture.nativeElement.click(); // Emulate click event + componentFixture.detectChanges(); + + expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); + + } ) ); + + it( 'should not hide after clicking on the notification', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: false, + onClick: false + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + componentInstance.show(); + jest.spyOn( componentInstance, 'onClickDismiss' ); + + componentFixture.nativeElement.click(); // Emulate click event + componentFixture.detectChanges(); + + expect( componentInstance.onClickDismiss ).not.toHaveBeenCalled(); + + } ) ); + + it( 'should pause the autoHide timer on mouseover, and resume again on mouseout', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: 5000, + onMouseover: 'pauseAutoHide' + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + componentInstance.show(); + jest.spyOn( componentInstance, 'onClickDismiss' ); + jest.spyOn( timerService, 'pause' ); + jest.spyOn( timerService, 'continue' ); + + componentInstance.onNotificationMouseover(); + + expect( timerService.pause ).toHaveBeenCalled(); + + componentInstance.onNotificationMouseout(); + + expect( timerService.continue ).toHaveBeenCalled(); + + timerService.finishManually(); + tick(); + + expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); + + } ) ); + + it( 'should restart the autoHide timer on mouseover', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: 5000, + onMouseover: 'resetAutoHide' + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + + componentInstance.show(); + jest.spyOn( componentInstance, 'onClickDismiss' ); + jest.spyOn( timerService, 'stop' ); + jest.spyOn( timerService, 'start' ); + + componentInstance.onNotificationMouseover(); + + expect( timerService.stop ).toHaveBeenCalled(); + + componentInstance.onNotificationMouseout(); + + expect( timerService.start ).toHaveBeenCalled(); + + timerService.finishManually(); + tick(); + + expect( componentInstance.onClickDismiss ).toHaveBeenCalled(); + + } ) ); + + } ); + + /** + * Helper for upfront configuration + */ + function beforeEachWithConfig( testNotifierConfig: NotifierConfig, extractServices:boolean = true ): void { + + TestBed + .configureTestingModule( { + declarations: [ + NotifierNotificationComponent, + TestComponent + ], + providers: [ + { + provide: NotifierService, + useValue: { + getConfig: () => testNotifierConfig + } + }, + { // No idea why this is *actually* necessary -- it shouldn't be ... + provide: NotifierConfigToken, + useValue: {} + }, + { + provide: NotifierAnimationService, + useClass: MockNotifierAnimationService + } + ] + } ) + .overrideComponent( NotifierNotificationComponent, { + set: { + providers: [ // Override component-specific providers + { + provide: NotifierTimerService, + useClass: MockNotifierTimerService + } + ] + } + } ); + + if(extractServices) { + componentFixture = TestBed.createComponent( NotifierNotificationComponent ); + componentInstance = componentFixture.componentInstance; + + // Get the service from the component's local injector + timerService = componentFixture.debugElement.injector.get( NotifierTimerService ); + } + } + +} ); + +/** + * Mock notifier animation service, always returning the animation + */ +class MockNotifierAnimationService extends NotifierAnimationService { + + /** + * Get animation data + * + * @param {'show' | 'hide'} direction Animation direction, either in or out + * @param {NotifierNotification} notification Notification the animation data should be generated for + * @returns {NotifierAnimationData} Animation information + * + * @override + */ + public getAnimationData( direction: 'show' | 'hide', notification: NotifierNotification ): NotifierAnimationData { + if ( direction === 'show' ) { + return { + keyframes: [ + { + opacity: '0' + }, + { + opacity: '1' + } + ], + options: { + duration: 300, + easing: 'ease', + fill: 'forwards' + } + }; + } else { + return { + keyframes: [ + { + opacity: '1' + }, + { + opacity: '0' + } + ], + options: { + duration: 300, + easing: 'ease', + fill: 'forwards' + } + }; + } + } + +} + +/** + * Mock Notifier Timer Service + */ +class MockNotifierTimerService extends NotifierTimerService { + + /** + * Temp resolve function + * + * @override + */ + private resolveFunction: Function; + + /** + * Start (or resume) the timer - doing nothing here + * + * @param {number} duration Timer duration, in ms + * @returns {Promise} Promise, resolved once the timer finishes + * + * @override + */ + public start( duration: number ): Promise { + return new Promise( ( resolve: () => void, reject: () => void ) => { + this.resolveFunction = resolve; + } ); + } + + /** + * Pause the timer - doing nothing here + */ + public pause(): void { + // Do nothing + } + + /** + * Continue the timer - doing nothing here + */ + public continue(): void { + // Do nothing + } + + /** + * Stop the timer - doing nothing here + */ + public stop(): void { + // Do nothing + } + + /** + * Finish the timer manually, from outside + */ + public finishManually(): void { + this.resolveFunction(); + } + +} + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + @ViewChild('tpl') + currentTplRef: TemplateRef; +} + +function createTestComponent(template: string): ComponentFixture { + return TestBed.overrideComponent(TestComponent, {set: {template: template}}) + .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) + .createComponent(TestComponent); +} diff --git a/src/lib/src/models/notifier-notification.model.ts b/src/lib/src/models/notifier-notification.model.ts index de48d0ee..7818fe7a 100644 --- a/src/lib/src/models/notifier-notification.model.ts +++ b/src/lib/src/models/notifier-notification.model.ts @@ -1,73 +1,80 @@ -import { NotifierNotificationComponent } from './../components/notifier-notification.component'; - -/** - * Notification - * - * This class describes the structure of a notifiction, including all information it needs to live, and everyone else needs to work with it. - */ -export class NotifierNotification { - - /** - * Unique notification ID, can be set manually to control the notification from outside later on - */ - public id: string; - - /** - * Notification type, will be used for constructing an appropriate class name - */ - public type: string; - - /** - * Notification message - */ - public message: string; - - /** - * Component reference of this notification, created and set during creation time - */ - public component: NotifierNotificationComponent; - - /** - * Constructor - * - * @param options Notifier options - */ - public constructor( options: NotifierNotificationOptions ) { - - Object.assign( this, options ); - - // If not set manually, we have to create a unique notification ID by ourselves. The ID generation relies on the current browser - // datetime in ms, in praticular the moment this notification gets constructed. Concurrency, and thus two IDs being the exact same, - // is not possible due to the action queue concept. - if ( options.id === undefined ) { - this.id = `ID_${ new Date().getTime() }`; - } - - } - -} - -/** - * Notifiction options - * - * This interface describes which information are needed to create a new notification, or in other words, which information the external API - * call must provide. - */ -export interface NotifierNotificationOptions { - - /** - * Notification ID, optional - */ - id?: string; - - /** - * Notification type - */ - type: string; - - /** - * Notificatin message - */ - message: string; - -} +import { NotifierNotificationComponent } from './../components/notifier-notification.component'; +import { TemplateRef } from '@angular/core'; + +/** + * Notification + * + * This class describes the structure of a notifiction, including all information it needs to live, and everyone else needs to work with it. + */ +export class NotifierNotification { + /** + * Unique notification ID, can be set manually to control the notification from outside later on + */ + public id: string; + + /** + * Notification type, will be used for constructing an appropriate class name + */ + public type: string; + + /** + * Notification message + */ + public message: string; + + /** + * The template to customize + * the appearance of the notification + */ + public template?: TemplateRef = null; + + /** + * Component reference of this notification, created and set during creation time + */ + public component: NotifierNotificationComponent; + + /** + * Constructor + * + * @param options Notifier options + */ + public constructor(options: NotifierNotificationOptions) { + Object.assign(this, options); + + // If not set manually, we have to create a unique notification ID by ourselves. The ID generation relies on the current browser + // datetime in ms, in praticular the moment this notification gets constructed. Concurrency, and thus two IDs being the exact same, + // is not possible due to the action queue concept. + if (options.id === undefined) { + this.id = `ID_${new Date().getTime()}`; + } + } +} + +/** + * Notifiction options + * + * This interface describes which information are needed to create a new notification, or in other words, which information the external API + * call must provide. + */ +export interface NotifierNotificationOptions { + /** + * Notification ID, optional + */ + id?: string; + + /** + * Notification type + */ + type: string; + + /** + * Notificatin message + */ + message: string; + + /** + * The template to customize + * the appearance of the notification + */ + template?: TemplateRef; +}