32 |
33 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-queue.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { inject, TestBed } from '@angular/core/testing';
2 | import { Subject } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 | import { NotifierQueueService } from './notifier-queue.service';
6 |
7 | /**
8 | * Notifier Queue Service - Unit Test
9 | */
10 | describe('Notifier Queue Service', () => {
11 | let queueService: NotifierQueueService;
12 |
13 | // Setup test module
14 | beforeEach(() => {
15 | TestBed.configureTestingModule({
16 | providers: [NotifierQueueService],
17 | });
18 | });
19 |
20 | // Inject dependencies
21 | beforeEach(inject([NotifierQueueService], (notifierQueueService: NotifierQueueService) => {
22 | queueService = notifierQueueService;
23 | }));
24 |
25 | it('should instantiate', () => {
26 | expect(queueService).toBeDefined();
27 | expect(queueService.actionStream).toEqual(expect.any(Subject));
28 | });
29 |
30 | it('should pass through one action', () => {
31 | const testAction: NotifierAction = {
32 | payload: 'FANCY',
33 | type: 'SHOW',
34 | };
35 | let expectedTestAction: NotifierAction | undefined;
36 |
37 | queueService.actionStream.subscribe((action: NotifierAction) => {
38 | expectedTestAction = action;
39 | });
40 | queueService.push(testAction);
41 |
42 | expect(expectedTestAction).toEqual(testAction);
43 | });
44 |
45 | it('should pass through multiple actions in order', () => {
46 | const firstTestAction: NotifierAction = {
47 | payload: 'AWESOME',
48 | type: 'SHOW',
49 | };
50 | const secondTestAction: NotifierAction = {
51 | payload: 'GREAT',
52 | type: 'HIDE',
53 | };
54 | let expectedTestAction: NotifierAction | undefined;
55 |
56 | queueService.actionStream.subscribe((action: NotifierAction) => {
57 | expectedTestAction = action;
58 | });
59 | queueService.push(firstTestAction);
60 | queueService.push(secondTestAction);
61 |
62 | expect(expectedTestAction).toEqual(firstTestAction);
63 |
64 | queueService.continue();
65 |
66 | expect(expectedTestAction).toEqual(secondTestAction);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-timer.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | /**
4 | * Notifier timer service
5 | *
6 | * This service acts as a timer, needed due to the still rather limited setTimeout JavaScript API. The timer service can start and stop a
7 | * timer. Furthermore, it can also pause the timer at any time, and resume later on. The timer API workd promise-based.
8 | */
9 | @Injectable()
10 | export class NotifierTimerService {
11 | /**
12 | * Timestamp (in ms), created in the moment the timer starts
13 | */
14 | private now: number;
15 |
16 | /**
17 | * Remaining time (in ms)
18 | */
19 | private remaining: number;
20 |
21 | /**
22 | * Timeout ID, used for clearing the timeout later on
23 | */
24 | private timerId: number;
25 |
26 | /**
27 | * Promise resolve function, eventually getting called once the timer finishes
28 | */
29 | private finishPromiseResolver: () => void;
30 |
31 | /**
32 | * Constructor
33 | */
34 | public constructor() {
35 | this.now = 0;
36 | this.remaining = 0;
37 | }
38 |
39 | /**
40 | * Start (or resume) the timer
41 | *
42 | * @param duration Timer duration, in ms
43 | * @returns Promise, resolved once the timer finishes
44 | */
45 | public start(duration: number): Promise {
46 | return new Promise((resolve: () => void) => {
47 | // For the first run ...
48 | this.remaining = duration;
49 |
50 | // Setup, then start the timer
51 | this.finishPromiseResolver = resolve;
52 | this.continue();
53 | });
54 | }
55 |
56 | /**
57 | * Pause the timer
58 | */
59 | public pause(): void {
60 | clearTimeout(this.timerId);
61 | this.remaining -= new Date().getTime() - this.now;
62 | }
63 |
64 | /**
65 | * Continue the timer
66 | */
67 | public continue(): void {
68 | this.now = new Date().getTime();
69 | this.timerId = window.setTimeout(() => {
70 | this.finish();
71 | }, this.remaining);
72 | }
73 |
74 | /**
75 | * Stop the timer
76 | */
77 | public stop(): void {
78 | clearTimeout(this.timerId);
79 | this.remaining = 0;
80 | }
81 |
82 | /**
83 | * Finish up the timeout by resolving the timer promise
84 | */
85 | private finish(): void {
86 | this.finishPromiseResolver();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-notification.model.ts:
--------------------------------------------------------------------------------
1 | import { TemplateRef } from '@angular/core';
2 |
3 | import { NotifierNotificationComponent } from '../components/notifier-notification.component';
4 |
5 | /**
6 | * Notification
7 | *
8 | * This class describes the structure of a notifiction, including all information it needs to live, and everyone else needs to work with it.
9 | */
10 | export class NotifierNotification {
11 | /**
12 | * Unique notification ID, can be set manually to control the notification from outside later on
13 | */
14 | public id: string;
15 |
16 | /**
17 | * Notification type, will be used for constructing an appropriate class name
18 | */
19 | public type: string;
20 |
21 | /**
22 | * Notification message
23 | */
24 | public message: string;
25 |
26 | /**
27 | * The template to customize
28 | * the appearance of the notification
29 | */
30 | public template?: TemplateRef = null;
31 |
32 | /**
33 | * Component reference of this notification, created and set during creation time
34 | */
35 | public component: NotifierNotificationComponent;
36 |
37 | /**
38 | * Constructor
39 | *
40 | * @param options Notifier options
41 | */
42 | public constructor(options: NotifierNotificationOptions) {
43 | Object.assign(this, options);
44 |
45 | // If not set manually, we have to create a unique notification ID by ourselves. The ID generation relies on the current browser
46 | // datetime in ms, in praticular the moment this notification gets constructed. Concurrency, and thus two IDs being the exact same,
47 | // is not possible due to the action queue concept.
48 | if (options.id === undefined) {
49 | this.id = `ID_${new Date().getTime()}`;
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * Notifiction options
56 | *
57 | * This interface describes which information are needed to create a new notification, or in other words, which information the external API
58 | * call must provide.
59 | */
60 | export interface NotifierNotificationOptions {
61 | /**
62 | * Notification ID, optional
63 | */
64 | id?: string;
65 |
66 | /**
67 | * Notification type
68 | */
69 | type: string;
70 |
71 | /**
72 | * Notificatin message
73 | */
74 | message: string;
75 |
76 | /**
77 | * The template to customize
78 | * the appearance of the notification
79 | */
80 | template?: TemplateRef;
81 | }
82 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-queue.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Subject } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 |
6 | /**
7 | * Notifier queue service
8 | *
9 | * In general, API calls don't get processed right away. Instead, we have to queue them up in order to prevent simultanious API calls
10 | * interfering with each other. This, at least in theory, is possible at any time. In particular, animations - which potentially overlap -
11 | * can cause changes in JS classes as well as affect the DOM. Therefore, the queue service takes all actions, puts them in a queue, and
12 | * processes them at the right time (which is when the previous action has been processed successfully).
13 | *
14 | * Technical sidenote:
15 | * An action looks pretty similar to the ones within the Flux / Redux pattern.
16 | */
17 | @Injectable()
18 | export class NotifierQueueService {
19 | /**
20 | * Stream of actions, subscribable from outside
21 | */
22 | public readonly actionStream: Subject;
23 |
24 | /**
25 | * Queue of actions
26 | */
27 | private actionQueue: Array;
28 |
29 | /**
30 | * Flag, true if some action is currently in progress
31 | */
32 | private isActionInProgress: boolean;
33 |
34 | /**
35 | * Constructor
36 | */
37 | public constructor() {
38 | this.actionStream = new Subject();
39 | this.actionQueue = [];
40 | this.isActionInProgress = false;
41 | }
42 |
43 | /**
44 | * Push a new action to the queue, and try to run it
45 | *
46 | * @param action Action object
47 | */
48 | public push(action: NotifierAction): void {
49 | this.actionQueue.push(action);
50 | this.tryToRunNextAction();
51 | }
52 |
53 | /**
54 | * Continue with the next action (called when the current action is finished)
55 | */
56 | public continue(): void {
57 | this.isActionInProgress = false;
58 | this.tryToRunNextAction();
59 | }
60 |
61 | /**
62 | * Try to run the next action in the queue; we skip if there already is some action in progress, or if there is no action left
63 | */
64 | private tryToRunNextAction(): void {
65 | if (this.isActionInProgress || this.actionQueue.length === 0) {
66 | return; // Skip (the queue can now go drink a coffee as it has nothing to do anymore)
67 | }
68 | this.isActionInProgress = true;
69 | this.actionStream.next(this.actionQueue.shift()); // Push next action to the stream, and remove the current action from the queue
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-animation.model.ts:
--------------------------------------------------------------------------------
1 | import { NotifierNotification } from './notifier-notification.model';
2 |
3 | /**
4 | * Notifier animation data
5 | *
6 | * This interface describes an object containing all information necessary to run an animation, in particular to run an animation with the
7 | * all new (shiny) Web Animations API. When other components or services request data for an animation they have to run, this is the object
8 | * they get back from the animation service.
9 | *
10 | * Technical sidenote:
11 | * Nope, it's not a coincidence - the structure looks similar to the Web Animation API syntax.
12 | */
13 | export interface NotifierAnimationData {
14 | /**
15 | * Animation keyframes; the first index ctonaining changes for animate-in, the second index those for animate-out
16 | */
17 | keyframes: Array<{
18 | [animatablePropertyName: string]: string;
19 | }>;
20 |
21 | /**
22 | * Futher animation options
23 | */
24 | options: {
25 | /**
26 | * Animation duration, in ms
27 | */
28 | duration: number;
29 |
30 | /**
31 | * Animation easing function (comp. CSS easing functions)
32 | */
33 | easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | string;
34 |
35 | /**
36 | * Animation fill mode
37 | */
38 | fill: 'none' | 'forwards' | 'backwards';
39 | };
40 | }
41 |
42 | /**
43 | * Notifier animation preset
44 | *
45 | * This interface describes the structure of an animation preset, defining the keyframes for both animating-in and animating-out. Animation
46 | * presets are always defined outside the animation service, and therefore one day may become part of some new API.
47 | */
48 | export interface NotifierAnimationPreset {
49 | /**
50 | * Function generating the keyframes for animating-out
51 | */
52 | hide: (notification: NotifierNotification) => NotifierAnimationPresetKeyframes;
53 |
54 | /**
55 | * Function generating the keyframes for animating-in
56 | */
57 | show: (notification: NotifierNotification) => NotifierAnimationPresetKeyframes;
58 | }
59 |
60 | /**
61 | * Notifier animation keyframes
62 | *
63 | * This interface describes the data, in particular all the keyframes animation presets return.
64 | */
65 | export interface NotifierAnimationPresetKeyframes {
66 | /**
67 | * CSS attributes before the animation starts
68 | */
69 | from: {
70 | [animatablePropertyName: string]: string;
71 | };
72 |
73 | /**
74 | * CSS attributes after the animation ends
75 | */
76 | to: {
77 | [animatablePropertyName: string]: string;
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-animation.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { fade } from '../animation-presets/fade.animation-preset';
4 | import { slide } from '../animation-presets/slide.animation-preset';
5 | import { NotifierAnimationData, NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
6 | import { NotifierNotification } from '../models/notifier-notification.model';
7 |
8 | /**
9 | * Notifier animation service
10 | */
11 | @Injectable()
12 | export class NotifierAnimationService {
13 | /**
14 | * List of animation presets (currently static)
15 | */
16 | private readonly animationPresets: {
17 | [animationPresetName: string]: NotifierAnimationPreset;
18 | };
19 |
20 | /**
21 | * Constructor
22 | */
23 | public constructor() {
24 | this.animationPresets = {
25 | fade,
26 | slide,
27 | };
28 | }
29 |
30 | /**
31 | * Get animation data
32 | *
33 | * This method generates all data the Web Animations API needs to animate our notification. The result depends on both the animation
34 | * direction (either in or out) as well as the notifications (and its attributes) itself.
35 | *
36 | * @param direction Animation direction, either in or out
37 | * @param notification Notification the animation data should be generated for
38 | * @returns Animation information
39 | */
40 | public getAnimationData(direction: 'show' | 'hide', notification: NotifierNotification): NotifierAnimationData {
41 | // Get all necessary animation data
42 | let keyframes: NotifierAnimationPresetKeyframes;
43 | let duration: number;
44 | let easing: string;
45 | if (direction === 'show') {
46 | keyframes = this.animationPresets[notification.component.getConfig().animations.show.preset].show(notification);
47 | duration = notification.component.getConfig().animations.show.speed;
48 | easing = notification.component.getConfig().animations.show.easing;
49 | } else {
50 | keyframes = this.animationPresets[notification.component.getConfig().animations.hide.preset].hide(notification);
51 | duration = notification.component.getConfig().animations.hide.speed;
52 | easing = notification.component.getConfig().animations.hide.easing;
53 | }
54 |
55 | // Build and return animation data
56 | return {
57 | keyframes: [keyframes.from, keyframes.to],
58 | options: {
59 | duration,
60 | easing,
61 | fill: 'forwards', // Keep the newly painted state after the animation finished
62 | },
63 | };
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/projects/angular-notifier-demo/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild } from '@angular/core';
2 | import { NotifierService } from 'angular-notifier';
3 |
4 | /**
5 | * App component
6 | */
7 | @Component({
8 | host: {
9 | class: 'app',
10 | },
11 | selector: 'app',
12 | templateUrl: './app.component.html',
13 | })
14 | export class AppComponent {
15 | @ViewChild('customTemplate', { static: true }) customNotificationTmpl;
16 |
17 | /**
18 | * Notifier service
19 | */
20 | private notifier: NotifierService;
21 |
22 | /**
23 | * Constructor
24 | *
25 | * @param {NotifierService} notifier Notifier service
26 | */
27 | public constructor(notifier: NotifierService) {
28 | this.notifier = notifier;
29 | }
30 |
31 | /**
32 | * Show a notification
33 | *
34 | * @param {string} type Notification type
35 | * @param {string} message Notification message
36 | */
37 | public showNotification(type: string, message: string): void {
38 | this.notifier.notify(type, message);
39 | }
40 |
41 | /**
42 | * Hide oldest notification
43 | */
44 | public hideOldestNotification(): void {
45 | this.notifier.hideOldest();
46 | }
47 |
48 | /**
49 | * Hide newest notification
50 | */
51 | public hideNewestNotification(): void {
52 | this.notifier.hideNewest();
53 | }
54 |
55 | /**
56 | * Hide all notifications at once
57 | */
58 | public hideAllNotifications(): void {
59 | this.notifier.hideAll();
60 | }
61 |
62 | /**
63 | * Show custom notification using template
64 | *
65 | * @param {string} type Notification type
66 | * @param {string} message Notification message
67 | */
68 | public showCustomNotificationTemplate(type: string, message: string): void {
69 | this.notifier.show({
70 | message,
71 | type,
72 | template: this.customNotificationTmpl,
73 | });
74 | }
75 |
76 | /**
77 | * Show a specific notification (with a custom notification ID)
78 | *
79 | * @param {string} type Notification type
80 | * @param {string} message Notification message
81 | * @param {string} id Notification ID
82 | */
83 | public showSpecificNotification(type: string, message: string, id: string): void {
84 | this.notifier.show({
85 | id,
86 | message,
87 | type,
88 | });
89 | }
90 |
91 | /**
92 | * Hide a specific notification (by a given notification ID)
93 | *
94 | * @param {string} id Notification ID
95 | */
96 | public hideSpecificNotification(id: string): void {
97 | this.notifier.hide(id);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.angular/cache
2 |
3 | # Created by https://www.toptal.com/developers/gitignore/api/node,vscode
4 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,vscode
5 |
6 | ### Node ###
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # Diagnostic reports (https://nodejs.org/api/report.html)
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage
29 | *.lcov
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (https://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 | jspm_packages/
49 |
50 | # TypeScript v1 declaration files
51 | typings/
52 |
53 | # TypeScript cache
54 | *.tsbuildinfo
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional eslint cache
60 | .eslintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # dotenv environment variables file
78 | .env
79 | .env.test
80 | .env*.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 |
89 | # Nuxt.js build / generate output
90 | .nuxt
91 | dist
92 |
93 | # Gatsby files
94 | .cache/
95 | # Comment in the public line in if your project uses Gatsby and not Next.js
96 | # https://nextjs.org/blog/next-9-1#public-directory-support
97 | # public
98 |
99 | # vuepress build output
100 | .vuepress/dist
101 |
102 | # Serverless directories
103 | .serverless/
104 |
105 | # FuseBox cache
106 | .fusebox/
107 |
108 | # DynamoDB Local files
109 | .dynamodb/
110 |
111 | # TernJS port file
112 | .tern-port
113 |
114 | # Stores VSCode versions used for testing VSCode extensions
115 | .vscode-test
116 |
117 | ### vscode ###
118 | .vscode/*
119 | !.vscode/settings.json
120 | !.vscode/tasks.json
121 | !.vscode/launch.json
122 | !.vscode/extensions.json
123 | *.code-workspace
124 |
125 | # End of https://www.toptal.com/developers/gitignore/api/node,vscode
126 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/fade.animation-preset.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 | import { NotifierConfig } from '../models/notifier-config.model';
3 | import { fade } from './fade.animation-preset';
4 |
5 | /**
6 | * Fade animation preset - Unit Test
7 | */
8 | describe('Fade Animation Preset', () => {
9 | describe('(show)', () => {
10 | it('should return animation keyframes', () => {
11 | const testNotification: MockNotification = new MockNotification({});
12 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
13 | from: {
14 | opacity: '0',
15 | },
16 | to: {
17 | opacity: '1',
18 | },
19 | };
20 | const keyframes: NotifierAnimationPresetKeyframes = fade.show(testNotification);
21 |
22 | expect(keyframes).toEqual(expectedKeyframes);
23 | });
24 | });
25 |
26 | describe('(hide)', () => {
27 | it('should return animation keyframes', () => {
28 | const testNotification: MockNotification = new MockNotification({});
29 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
30 | from: {
31 | opacity: '1',
32 | },
33 | to: {
34 | opacity: '0',
35 | },
36 | };
37 | const keyframes: NotifierAnimationPresetKeyframes = fade.hide(testNotification);
38 |
39 | expect(keyframes).toEqual(expectedKeyframes);
40 | });
41 | });
42 | });
43 |
44 | /**
45 | * Mock Notification Height
46 | */
47 | const mockNotificationHeight = 40;
48 |
49 | /**
50 | * Mock Notification Shift
51 | */
52 | const mockNotificationShift = 80;
53 |
54 | /**
55 | * Mock Notification Width
56 | */
57 | const mockNotificationWidth = 300;
58 |
59 | /**
60 | * Mock notification, providing static values except the global configuration
61 | */
62 | class MockNotification {
63 | /**
64 | * Configuration
65 | */
66 | public config: NotifierConfig;
67 |
68 | /**
69 | * Notification ID
70 | */
71 | public id = 'ID_FAKE';
72 |
73 | /**
74 | * Notification type
75 | */
76 | public type = 'SUCCESS';
77 |
78 | /**
79 | * Notification message
80 | */
81 | public message = 'Lorem ipsum dolor sit amet.';
82 |
83 | /**
84 | * Notification component
85 | */
86 | public component: any = {
87 | getConfig: () => this.config,
88 | getHeight: () => mockNotificationHeight,
89 | getShift: () => mockNotificationShift,
90 | getWidth: () => mockNotificationWidth,
91 | };
92 |
93 | /**
94 | * Constructor
95 | *
96 | * @param {NotifierConfig} config Configuration
97 | */
98 | public constructor(config: NotifierConfig) {
99 | this.config = config;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/notifier.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { NotifierConfig, NotifierOptions } from './models/notifier-config.model';
4 | import { NotifierModule } from './notifier.module';
5 | import { NotifierService } from './services/notifier.service';
6 |
7 | /**
8 | * Notifier Module - Unit Test
9 | */
10 | describe('Notifier Module', () => {
11 | it('should instantiate', () => {
12 | TestBed.configureTestingModule({
13 | imports: [NotifierModule],
14 | });
15 | const service: NotifierService = TestBed.inject(NotifierService);
16 |
17 | expect(service).toBeDefined();
18 | });
19 |
20 | it('should instantiate with default options', () => {
21 | TestBed.configureTestingModule({
22 | imports: [NotifierModule],
23 | });
24 | const service: NotifierService = TestBed.inject(NotifierService);
25 |
26 | expect(service.getConfig()).toEqual(new NotifierConfig());
27 | });
28 |
29 | it('should instantiate with custom options', () => {
30 | const testNotifierOptions: NotifierOptions = {
31 | animations: {
32 | hide: {
33 | easing: 'ease-in-out',
34 | },
35 | overlap: 100,
36 | shift: {
37 | speed: 200,
38 | },
39 | },
40 | behaviour: {
41 | autoHide: 5000,
42 | stacking: 7,
43 | },
44 | position: {
45 | horizontal: {
46 | distance: 20,
47 | },
48 | },
49 | theme: 'my-custom-theme',
50 | };
51 | const expectedNotifierConfig: NotifierConfig = new NotifierConfig({
52 | animations: {
53 | enabled: true,
54 | hide: {
55 | easing: 'ease-in-out',
56 | offset: 50,
57 | preset: 'fade',
58 | speed: 300,
59 | },
60 | overlap: 100,
61 | shift: {
62 | easing: 'ease',
63 | speed: 200,
64 | },
65 | show: {
66 | easing: 'ease',
67 | preset: 'slide',
68 | speed: 300,
69 | },
70 | },
71 | behaviour: {
72 | autoHide: 5000,
73 | onClick: false,
74 | onMouseover: 'pauseAutoHide',
75 | showDismissButton: true,
76 | stacking: 7,
77 | },
78 | position: {
79 | horizontal: {
80 | distance: 20,
81 | position: 'left',
82 | },
83 | vertical: {
84 | distance: 12,
85 | gap: 10,
86 | position: 'bottom',
87 | },
88 | },
89 | theme: 'my-custom-theme',
90 | });
91 |
92 | TestBed.configureTestingModule({
93 | imports: [NotifierModule.withConfig(testNotifierOptions)],
94 | });
95 | const service: NotifierService = TestBed.inject(NotifierService);
96 |
97 | expect(service.getConfig()).toEqual(expectedNotifierConfig);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-notifier",
3 | "description": "A well designed, fully animated, highly customizable, and easy-to-use notification library for your Angular application.",
4 | "version": "14.0.0",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/dominique-mueller/angular-notifier"
9 | },
10 | "keywords": [
11 | "angular",
12 | "angular2",
13 | "ng",
14 | "ng2",
15 | "notifier",
16 | "notification",
17 | "notifications",
18 | "toast",
19 | "toasts",
20 | "alert",
21 | "library"
22 | ],
23 | "scripts": {
24 | "build:demo": "ng build angular-notifier-demo --configuration production",
25 | "build:library": "rimraf -r dist && npm run build:library:angular && npm run build:library:sass && npm run build:library:css && npm run build:library:docs && npm run build:library:package",
26 | "build:library:angular": "ng build angular-notifier --configuration production",
27 | "build:library:css": "sass projects/angular-notifier/src:dist/angular-notifier --style=expanded",
28 | "build:library:docs": "copyfiles \"docs/**\" CHANGELOG.md MIGRATION-GUIDE.md LICENSE README.md \"dist/angular-notifier\"",
29 | "build:library:package": "node tools/update-package.js",
30 | "build:library:sass": "copyfiles \"projects/angular-notifier/src/**/*.scss\" \"dist/angular-notifier\" --up 3",
31 | "lint:library": "eslint projects/angular-notifier/src/**/*.ts --max-warnings 0",
32 | "lint:library:fix": "eslint projects/angular-notifier/src/**/*.ts --max-warnings 0 --fix",
33 | "start": "ng serve angular-notifier-demo",
34 | "test:library": "ng test angular-notifier",
35 | "test:library:upload-coverage": "codecov -f coverage/coverage-final.json"
36 | },
37 | "dependencies": {
38 | "tslib": "2.4.x"
39 | },
40 | "peerDependencies": {
41 | "@angular/common": ">= 16.0.0 < 17.0.0",
42 | "@angular/core": ">= 16.0.0 < 17.0.0"
43 | },
44 | "devDependencies": {
45 | "@angular-builders/jest": "16.0.x",
46 | "@angular-devkit/build-angular": "16.0.x",
47 | "@angular/cli": "16.0.x",
48 | "@angular/common": "16.0.x",
49 | "@angular/compiler": "16.0.x",
50 | "@angular/compiler-cli": "16.0.x",
51 | "@angular/core": "16.0.x",
52 | "@angular/platform-browser": "16.0.x",
53 | "@angular/platform-browser-dynamic": "16.0.x",
54 | "@types/jest": "29.5.x",
55 | "@types/node": "16.x",
56 | "@types/web-animations-js": "2.2.x",
57 | "@typescript-eslint/eslint-plugin": "5.59.x",
58 | "@typescript-eslint/parser": "5.59.x",
59 | "codecov": "3.8.x",
60 | "copyfiles": "2.4.x",
61 | "eslint": "8.42.x",
62 | "eslint-config-prettier": "8.5.x",
63 | "eslint-plugin-import": "2.26.x",
64 | "eslint-plugin-prettier": "4.2.x",
65 | "eslint-plugin-simple-import-sort": "8.0.x",
66 | "jest": "29.5.x",
67 | "ng-packagr": "16.0.x",
68 | "prettier": "2.7.x",
69 | "rimraf": "3.0.x",
70 | "rxjs": "7.5.x",
71 | "sass": "1.54.x",
72 | "ts-node": "10.9.x",
73 | "typescript": "4.9.x",
74 | "zone.js": "0.13.x"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/notifier.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { ModuleWithProviders, NgModule } from '@angular/core';
3 |
4 | import { NotifierContainerComponent } from './components/notifier-container.component';
5 | import { NotifierNotificationComponent } from './components/notifier-notification.component';
6 | import { NotifierConfig, NotifierOptions } from './models/notifier-config.model';
7 | import { NotifierConfigToken, NotifierOptionsToken } from './notifier.tokens';
8 | import { NotifierService } from './services/notifier.service';
9 | import { NotifierAnimationService } from './services/notifier-animation.service';
10 | import { NotifierQueueService } from './services/notifier-queue.service';
11 |
12 | /**
13 | * Factory for a notifier configuration with custom options
14 | *
15 | * Sidenote:
16 | * Required as Angular AoT compilation cannot handle dynamic functions; see .
17 | *
18 | * @param options - Custom notifier options
19 | * @returns - Notifier configuration as result
20 | */
21 | export function notifierCustomConfigFactory(options: NotifierOptions): NotifierConfig {
22 | return new NotifierConfig(options);
23 | }
24 |
25 | /**
26 | * Factory for a notifier configuration with default options
27 | *
28 | * Sidenote:
29 | * Required as Angular AoT compilation cannot handle dynamic functions; see .
30 | *
31 | * @returns - Notifier configuration as result
32 | */
33 | export function notifierDefaultConfigFactory(): NotifierConfig {
34 | return new NotifierConfig({});
35 | }
36 |
37 | /**
38 | * Notifier module
39 | */
40 | @NgModule({
41 | declarations: [NotifierContainerComponent, NotifierNotificationComponent],
42 | exports: [NotifierContainerComponent],
43 | imports: [CommonModule],
44 | providers: [
45 | NotifierAnimationService,
46 | NotifierService,
47 | NotifierQueueService,
48 |
49 | // Provide the default notifier configuration if just the module is imported
50 | {
51 | provide: NotifierConfigToken,
52 | useFactory: notifierDefaultConfigFactory,
53 | },
54 | ],
55 | })
56 | export class NotifierModule {
57 | /**
58 | * Setup the notifier module with custom providers, in this case with a custom configuration based on the givne options
59 | *
60 | * @param [options={}] - Custom notifier options
61 | * @returns - Notifier module with custom providers
62 | */
63 | public static withConfig(options: NotifierOptions = {}): ModuleWithProviders {
64 | return {
65 | ngModule: NotifierModule,
66 | providers: [
67 | // Provide the options itself upfront (as we need to inject them as dependencies -- see below)
68 | {
69 | provide: NotifierOptionsToken,
70 | useValue: options,
71 | },
72 |
73 | // Provide a custom notifier configuration, based on the given notifier options
74 | {
75 | deps: [NotifierOptionsToken],
76 | provide: NotifierConfigToken,
77 | useFactory: notifierCustomConfigFactory,
78 | },
79 | ],
80 | };
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-config.model.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierConfig } from './notifier-config.model';
2 |
3 | /**
4 | * Notifier Configuration - Unit Test
5 | */
6 | describe('Notifier Configuration', () => {
7 | it('should initialize with the default configuration', () => {
8 | const testNotifierConfig: NotifierConfig = new NotifierConfig();
9 | const expectedNotifierConfig: NotifierConfig = new NotifierConfig({
10 | animations: {
11 | enabled: true,
12 | hide: {
13 | easing: 'ease',
14 | offset: 50,
15 | preset: 'fade',
16 | speed: 300,
17 | },
18 | overlap: 150,
19 | shift: {
20 | easing: 'ease',
21 | speed: 300,
22 | },
23 | show: {
24 | easing: 'ease',
25 | preset: 'slide',
26 | speed: 300,
27 | },
28 | },
29 | behaviour: {
30 | autoHide: 7000,
31 | onClick: false,
32 | onMouseover: 'pauseAutoHide',
33 | showDismissButton: true,
34 | stacking: 4,
35 | },
36 | position: {
37 | horizontal: {
38 | distance: 12,
39 | position: 'left',
40 | },
41 | vertical: {
42 | distance: 12,
43 | gap: 10,
44 | position: 'bottom',
45 | },
46 | },
47 | theme: 'material',
48 | });
49 |
50 | expect(testNotifierConfig).toEqual(expectedNotifierConfig);
51 | });
52 |
53 | it('should override custom bits of the configuration', () => {
54 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
55 | animations: {
56 | hide: {
57 | easing: 'ease-in-out',
58 | },
59 | overlap: 100,
60 | shift: {
61 | speed: 200,
62 | },
63 | },
64 | behaviour: {
65 | autoHide: 5000,
66 | stacking: 7,
67 | },
68 | position: {
69 | horizontal: {
70 | distance: 20,
71 | },
72 | },
73 | theme: 'my-custom-theme',
74 | });
75 | const expectedNotifierConfig: NotifierConfig = new NotifierConfig({
76 | animations: {
77 | enabled: true,
78 | hide: {
79 | easing: 'ease-in-out',
80 | offset: 50,
81 | preset: 'fade',
82 | speed: 300,
83 | },
84 | overlap: 100,
85 | shift: {
86 | easing: 'ease',
87 | speed: 200,
88 | },
89 | show: {
90 | easing: 'ease',
91 | preset: 'slide',
92 | speed: 300,
93 | },
94 | },
95 | behaviour: {
96 | autoHide: 5000,
97 | onClick: false,
98 | onMouseover: 'pauseAutoHide',
99 | showDismissButton: true,
100 | stacking: 7,
101 | },
102 | position: {
103 | horizontal: {
104 | distance: 20,
105 | position: 'left',
106 | },
107 | vertical: {
108 | distance: 12,
109 | gap: 10,
110 | position: 'bottom',
111 | },
112 | },
113 | theme: 'my-custom-theme',
114 | });
115 |
116 | expect(testNotifierConfig).toEqual(expectedNotifierConfig);
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "cli": {
4 | "analytics": false
5 | },
6 | "version": 1,
7 | "newProjectRoot": "projects",
8 | "projects": {
9 | "angular-notifier": {
10 | "projectType": "library",
11 | "root": "projects/angular-notifier",
12 | "sourceRoot": "projects/angular-notifier/src",
13 | "architect": {
14 | "build": {
15 | "builder": "@angular-devkit/build-angular:ng-packagr",
16 | "options": {
17 | "tsConfig": "projects/angular-notifier/tsconfig.lib.json",
18 | "project": "projects/angular-notifier/ng-package.json"
19 | },
20 | "configurations": {
21 | "production": {
22 | "tsConfig": "projects/angular-notifier/tsconfig.lib.prod.json"
23 | }
24 | }
25 | },
26 | "test": {
27 | "builder": "@angular-builders/jest:run",
28 | "options": {
29 | "configPath": "jest.config.json"
30 | }
31 | }
32 | }
33 | },
34 | "angular-notifier-demo": {
35 | "projectType": "application",
36 | "root": "projects/angular-notifier-demo/",
37 | "sourceRoot": "projects/angular-notifier-demo/src",
38 | "architect": {
39 | "build": {
40 | "builder": "@angular-devkit/build-angular:browser",
41 | "options": {
42 | "outputPath": "dist/angular-notifier-demo",
43 | "index": "projects/angular-notifier-demo/src/index.html",
44 | "main": "projects/angular-notifier-demo/src/main.ts",
45 | "polyfills": "projects/angular-notifier-demo/src/polyfills.ts",
46 | "tsConfig": "projects/angular-notifier-demo/tsconfig.app.json",
47 | "assets": ["projects/angular-notifier-demo/src/favicon.ico", "projects/angular-notifier-demo/src/assets"],
48 | "styles": ["projects/angular-notifier-demo/src/styles.scss"],
49 | "scripts": [],
50 | "vendorChunk": true,
51 | "extractLicenses": false,
52 | "buildOptimizer": false,
53 | "sourceMap": true,
54 | "optimization": false,
55 | "namedChunks": true
56 | },
57 | "configurations": {
58 | "production": {
59 | "fileReplacements": [
60 | {
61 | "replace": "projects/angular-notifier-demo/src/environments/environment.ts",
62 | "with": "projects/angular-notifier-demo/src/environments/environment.prod.ts"
63 | }
64 | ],
65 | "optimization": true,
66 | "outputHashing": "all",
67 | "sourceMap": false,
68 | "namedChunks": false,
69 | "vendorChunk": false,
70 | "buildOptimizer": true
71 | }
72 | },
73 | "defaultConfiguration": ""
74 | },
75 | "serve": {
76 | "builder": "@angular-devkit/build-angular:dev-server",
77 | "options": {
78 | "browserTarget": "angular-notifier-demo:build"
79 | },
80 | "configurations": {
81 | "production": {
82 | "browserTarget": "angular-notifier-demo:build:production"
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-timer.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
2 |
3 | import { NotifierTimerService } from './notifier-timer.service';
4 |
5 | /**
6 | * Notifier Timer Service - Unit Test
7 | */
8 | describe('Notifier Timer Service', () => {
9 | const fullAnimationTime = 5000;
10 | const longAnimationTime = 4000;
11 | const shortAnimationTime = 1000;
12 |
13 | let timerService: NotifierTimerService;
14 | let mockDate: MockDate;
15 |
16 | // Setup test module
17 | beforeEach(() => {
18 | TestBed.configureTestingModule({
19 | providers: [NotifierTimerService],
20 | });
21 | });
22 |
23 | // Inject dependencies
24 | beforeEach(inject([NotifierTimerService], (notifierTimerService: NotifierTimerService) => {
25 | timerService = notifierTimerService;
26 | mockDate = new MockDate();
27 | }));
28 |
29 | it('should instantiate', () => {
30 | expect(timerService).toBeDefined();
31 | });
32 |
33 | it('should start and stop the timer', fakeAsync(() => {
34 | const timerServiceCallback = jest.fn();
35 | timerService.start(fullAnimationTime).then(timerServiceCallback);
36 |
37 | tick(longAnimationTime);
38 |
39 | expect(timerServiceCallback).not.toHaveBeenCalled();
40 |
41 | tick(shortAnimationTime);
42 |
43 | expect(timerServiceCallback).toHaveBeenCalled();
44 | }));
45 |
46 | it('should pause and resume the timer', fakeAsync(() => {
47 | jest.spyOn(window, 'Date').mockImplementation(() => mockDate);
48 | const timerServiceCallback = jest.fn();
49 | timerService.start(fullAnimationTime).then(timerServiceCallback);
50 |
51 | tick(longAnimationTime);
52 | mockDate.fastForwardTime(longAnimationTime); // Also update the global Date (in addition to the tick)
53 |
54 | timerService.pause();
55 |
56 | tick(shortAnimationTime);
57 | mockDate.fastForwardTime(shortAnimationTime); // Also update the global Date (in addition to the tick)
58 |
59 | expect(timerServiceCallback).not.toHaveBeenCalled();
60 |
61 | // Resumes the timer, using the same duration as above (a continue doesn't exist yet)
62 | timerService.continue();
63 | tick(shortAnimationTime);
64 |
65 | expect(timerServiceCallback).toHaveBeenCalled();
66 | }));
67 |
68 | it('should stop the timer', fakeAsync(() => {
69 | const timerServiceCallback = jest.fn();
70 | timerService.start(fullAnimationTime).then(timerServiceCallback);
71 |
72 | tick(longAnimationTime);
73 | timerService.stop();
74 | tick(shortAnimationTime);
75 |
76 | expect(timerServiceCallback).not.toHaveBeenCalled();
77 | }));
78 | });
79 |
80 | /**
81 | * Mock Date, allows for fast-forwarding the time even in the global Date object
82 | */
83 | class MockDate extends Date {
84 | /**
85 | * Start time (at init)
86 | */
87 | private startTime: number;
88 |
89 | /**
90 | * Elapsed time (since init)
91 | */
92 | private elapsedTime: number;
93 |
94 | /**
95 | * Fast-forward the current time manually
96 | */
97 | public fastForwardTime(duration: number): void {
98 | this.elapsedTime += duration;
99 | }
100 |
101 | /**
102 | * Get the current time
103 | *
104 | * @override
105 | */
106 | public getTime(): number {
107 | return this.startTime + this.elapsedTime;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/slide.animation-preset.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 | import { NotifierConfig } from '../models/notifier-config.model';
3 | import { NotifierNotification } from '../models/notifier-notification.model';
4 |
5 | /**
6 | * Slide animation preset
7 | */
8 | export const slide: NotifierAnimationPreset = {
9 | hide: (notification: NotifierNotification): NotifierAnimationPresetKeyframes => {
10 | // Prepare variables
11 | const config: NotifierConfig = notification.component.getConfig();
12 | const shift: number = notification.component.getShift();
13 | let from: {
14 | [animatablePropertyName: string]: string;
15 | };
16 | let to: {
17 | [animatablePropertyName: string]: string;
18 | };
19 |
20 | // Configure variables, depending on configuration and component
21 | if (config.position.horizontal.position === 'left') {
22 | from = {
23 | transform: `translate3d( 0, ${shift}px, 0 )`,
24 | };
25 | to = {
26 | transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), ${shift}px, 0 )`,
27 | };
28 | } else if (config.position.horizontal.position === 'right') {
29 | from = {
30 | transform: `translate3d( 0, ${shift}px, 0 )`,
31 | };
32 | to = {
33 | transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), ${shift}px, 0 )`,
34 | };
35 | } else {
36 | let horizontalPosition: string;
37 | if (config.position.vertical.position === 'top') {
38 | horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`;
39 | } else {
40 | horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`;
41 | }
42 | from = {
43 | transform: `translate3d( -50%, ${shift}px, 0 )`,
44 | };
45 | to = {
46 | transform: `translate3d( -50%, ${horizontalPosition}, 0 )`,
47 | };
48 | }
49 |
50 | // Done
51 | return {
52 | from,
53 | to,
54 | };
55 | },
56 | show: (notification: NotifierNotification): NotifierAnimationPresetKeyframes => {
57 | // Prepare variables
58 | const config: NotifierConfig = notification.component.getConfig();
59 | let from: {
60 | [animatablePropertyName: string]: string;
61 | };
62 | let to: {
63 | [animatablePropertyName: string]: string;
64 | };
65 |
66 | // Configure variables, depending on configuration and component
67 | if (config.position.horizontal.position === 'left') {
68 | from = {
69 | transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), 0, 0 )`,
70 | };
71 | to = {
72 | transform: 'translate3d( 0, 0, 0 )',
73 | };
74 | } else if (config.position.horizontal.position === 'right') {
75 | from = {
76 | transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), 0, 0 )`,
77 | };
78 | to = {
79 | transform: 'translate3d( 0, 0, 0 )',
80 | };
81 | } else {
82 | let horizontalPosition: string;
83 | if (config.position.vertical.position === 'top') {
84 | horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`;
85 | } else {
86 | horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`;
87 | }
88 | from = {
89 | transform: `translate3d( -50%, ${horizontalPosition}, 0 )`,
90 | };
91 | to = {
92 | transform: 'translate3d( -50%, 0, 0 )',
93 | };
94 | }
95 |
96 | // Done
97 | return {
98 | from,
99 | to,
100 | };
101 | },
102 | };
103 |
--------------------------------------------------------------------------------
/MIGRATION-GUIDE.md:
--------------------------------------------------------------------------------
1 | # Migration Guide
2 |
3 | Strictly following the principle of semantic versioning, breaking changes only occur between major versions. This Migration Guide gives
4 | developers a more detailed insight into the changes introduced with new major releases, in particular the breaking changes and their
5 | consequences, while also suggesting a migration strategy.
6 |
7 | Also see then **[CHANGELOG](./CHANGELOG.md)** and **[GitHub releases](https://github.com/dominique-mueller/angular-notifier/releases)**.
8 |
9 |
10 |
11 | ## Migration from `1.x` to `2.x`
12 |
13 | > The amount of breaking changes from `1.x` to `2.x` is rather small, a migration shouldn't take longer than 5 minutes.
14 |
15 | #### Compatible with Angular 4+ only
16 |
17 | The library is now compatible with Angular 4+, using the all new & improved Angular APIs (such as the new `Renderer2`). Consequently, this
18 | also means that the compatibility with Angualr 2 breaks. If you still want to stick to Angular 2, you can continue using the latest `1.x`
19 | release; however, all new development (inlcuding bug fixes and features) will happen in the new `2.x` releases.
20 |
21 | #### Renaming of component selectors and classes
22 |
23 | For consistency reasons, all component selectors and CSS class names got renamed to no longer use the `x-` prefix. To migrate your
24 | application, simply rename the `x-notifier-container` tag to `notifier-container`. Also, if you did write custom themes or overwrote the
25 | default styling, you should remove the `x-` prefix from all CSS class names. The SASS variables, however, are still named the same.
26 |
27 | #### Renaming of module `forRoot()` method
28 |
29 | The `NotifierModule.forRoot()` method was used for passing custom options to the notifier. While the functionality stays the exact same, the
30 | method is now called `NotifierModule.withConfig()` instead. This seemed to be the more semantic, meaningful name here.
31 |
32 | #### Names & paths of published files
33 |
34 | With Angular 4+, a new recommendation regarding the publishment of Angular libraries has been defined. This includes a different folder
35 | structure, and also different output files. Therefore, the published files now include:
36 |
37 | - `angular-notifier.js` as the "rolled up" ES6 FESM (Flat ECMAScript Module) bundle
38 | - `angular-notifier.es5.js` as the "rolled up" ES5 FESM (Flat ECMAScript Module) bundle, however using ES6 import
39 | - `angular-notifier.umd.js` as the ES5 UMD (Universal Module Definition) bundle, here for compatibility reasons
40 | - Both the original `styles.scss` and compiled `styles.css` file exist, yet are available at the root path; sub-files are now located in the
41 | "styles" folder
42 | - Also, the places of all the sourcemaps and TypeScript definition files changed (which, however, shouldn't affect anyone)
43 |
44 | *The only change affecting developers is probably the path change of the SASS / CSS files. When using SystemJS, a path change of JavaScript
45 | files might also be necessary. Most modern frontend build tools (such as Webpack or Rollup) will recognize and understand this library and
46 | its published files automatically.*
47 |
48 | #### Web Animations API polyfill
49 |
50 | The implementation of animations has been changed slightly, so that now the *default* Web Animations API polyfill should be sufficient to
51 | make this library work in older browsers. This is also the polyfill defined within Angular CLI based projects in the `polyfills.ts` file by
52 | default. While it for sure will save us a few bytes over the network line, it also prevents confusion amongst developers (such as
53 | **[#6](https://github.com/dominique-mueller/angular-notifier/issues/6)**,
54 | **[#10](https://github.com/dominique-mueller/angular-notifier/issues/10)**). In particular:
55 |
56 | ``` typescript
57 | // With 1.x
58 | import 'web-animations-js/web-animations-next.min.js';
59 |
60 | // Now with 2.x
61 | import 'web-animations-js';
62 | // Same as: import 'web-animations-js/web-animations.min.js';
63 | ```
64 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@angular/core';
2 | import { Observable } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 | import { NotifierConfig } from '../models/notifier-config.model';
6 | import { NotifierNotificationOptions } from '../models/notifier-notification.model';
7 | import { NotifierConfigToken } from '../notifier.tokens';
8 | import { NotifierQueueService } from './notifier-queue.service';
9 |
10 | /**
11 | * Notifier service
12 | *
13 | * This service provides access to the public notifier API. Once injected into a component, directive, pipe, service, or any other building
14 | * block of an applications, it can be used to show new notifications, and hide existing ones. Internally, it transforms API calls into
15 | * actions, which then get thrown into the action queue - eventually being processed at the right moment.
16 | */
17 | @Injectable()
18 | export class NotifierService {
19 | /**
20 | * Notifier queue service
21 | */
22 | private readonly queueService: NotifierQueueService;
23 |
24 | /**
25 | * Notifier configuration
26 | */
27 | private readonly config: NotifierConfig;
28 |
29 | /**
30 | * Constructor
31 | *
32 | * @param notifierQueueService Notifier queue service
33 | * @param config Notifier configuration, optionally injected as a dependency
34 | */
35 | public constructor(notifierQueueService: NotifierQueueService, @Inject(NotifierConfigToken) config: NotifierConfig) {
36 | this.queueService = notifierQueueService;
37 | this.config = config;
38 | }
39 |
40 | /**
41 | * Get the notifier configuration
42 | *
43 | * @returns Notifier configuration
44 | */
45 | public getConfig(): NotifierConfig {
46 | return this.config;
47 | }
48 |
49 | /**
50 | * Get the observable for handling actions
51 | *
52 | * @returns Observable of NotifierAction
53 | */
54 | public get actionStream(): Observable {
55 | return this.queueService.actionStream.asObservable();
56 | }
57 |
58 | /**
59 | * API: Show a new notification
60 | *
61 | * @param notificationOptions Notification options
62 | */
63 | public show(notificationOptions: NotifierNotificationOptions): void {
64 | this.queueService.push({
65 | payload: notificationOptions,
66 | type: 'SHOW',
67 | });
68 | }
69 |
70 | /**
71 | * API: Hide a specific notification, given its ID
72 | *
73 | * @param notificationId ID of the notification to hide
74 | */
75 | public hide(notificationId: string): void {
76 | this.queueService.push({
77 | payload: notificationId,
78 | type: 'HIDE',
79 | });
80 | }
81 |
82 | /**
83 | * API: Hide the newest notification
84 | */
85 | public hideNewest(): void {
86 | this.queueService.push({
87 | type: 'HIDE_NEWEST',
88 | });
89 | }
90 |
91 | /**
92 | * API: Hide the oldest notification
93 | */
94 | public hideOldest(): void {
95 | this.queueService.push({
96 | type: 'HIDE_OLDEST',
97 | });
98 | }
99 |
100 | /**
101 | * API: Hide all notifications at once
102 | */
103 | public hideAll(): void {
104 | this.queueService.push({
105 | type: 'HIDE_ALL',
106 | });
107 | }
108 |
109 | /**
110 | * API: Shortcut for showing a new notification
111 | *
112 | * @param type Type of the notification
113 | * @param message Message of the notification
114 | * @param [notificationId] Unique ID for the notification (optional)
115 | */
116 | public notify(type: string, message: string, notificationId?: string): void {
117 | const notificationOptions: NotifierNotificationOptions = {
118 | message,
119 | type,
120 | };
121 | if (notificationId !== undefined) {
122 | notificationOptions.id = notificationId;
123 | }
124 | this.show(notificationOptions);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier-animation.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { inject, TestBed } from '@angular/core/testing';
2 |
3 | import { NotifierAnimationData } from '../models/notifier-animation.model';
4 | import { NotifierConfig } from '../models/notifier-config.model';
5 | import { NotifierAnimationService } from './notifier-animation.service';
6 |
7 | /**
8 | * Notifier Animation Service - Unit Test
9 | */
10 | describe('Notifier Animation Service', () => {
11 | let animationService: NotifierAnimationService;
12 |
13 | // Setup test module
14 | beforeEach(() => {
15 | TestBed.configureTestingModule({
16 | providers: [NotifierAnimationService],
17 | });
18 | });
19 |
20 | // Inject dependencies
21 | beforeEach(inject([NotifierAnimationService], (notifierAnimationService: NotifierAnimationService) => {
22 | animationService = notifierAnimationService;
23 | }));
24 |
25 | it('should instantiate', () => {
26 | expect(animationService).toBeDefined();
27 | });
28 |
29 | it('should build the animation data for showing a notification', () => {
30 | const testConfig: NotifierConfig = new NotifierConfig({
31 | animations: {
32 | show: {
33 | easing: 'ease-in-out',
34 | preset: 'fade',
35 | speed: 400,
36 | },
37 | },
38 | });
39 | const testNotification: MockNotification = new MockNotification(testConfig);
40 | const expectedAnimationData: NotifierAnimationData = {
41 | keyframes: [
42 | {
43 | opacity: '0',
44 | },
45 | {
46 | opacity: '1',
47 | },
48 | ],
49 | options: {
50 | duration: testConfig.animations.show.speed,
51 | easing: testConfig.animations.show.easing,
52 | fill: 'forwards',
53 | },
54 | };
55 | const animationData: NotifierAnimationData = animationService.getAnimationData('show', testNotification);
56 |
57 | expect(animationData).toEqual(expectedAnimationData);
58 | });
59 |
60 | it('should build the animation data for hiding a notification', () => {
61 | const testConfig: NotifierConfig = new NotifierConfig({
62 | animations: {
63 | hide: {
64 | easing: 'ease-in-out',
65 | preset: 'fade',
66 | speed: 400,
67 | },
68 | },
69 | });
70 | const testNotification: MockNotification = new MockNotification(testConfig);
71 | const expectedAnimationData: NotifierAnimationData = {
72 | keyframes: [
73 | {
74 | opacity: '1',
75 | },
76 | {
77 | opacity: '0',
78 | },
79 | ],
80 | options: {
81 | duration: testConfig.animations.hide.speed,
82 | easing: testConfig.animations.hide.easing,
83 | fill: 'forwards',
84 | },
85 | };
86 | const animationData: NotifierAnimationData = animationService.getAnimationData('hide', testNotification);
87 |
88 | expect(animationData).toEqual(expectedAnimationData);
89 | });
90 | });
91 |
92 | /**
93 | * Mock Notification Height
94 | */
95 | const mockNotificationHeight = 40;
96 |
97 | /**
98 | * Mock Notification Shift
99 | */
100 | const mockNotificationShift = 80;
101 |
102 | /**
103 | * Mock Notification Width
104 | */
105 | const mockNotificationWidth = 300;
106 |
107 | /**
108 | * Mock notification
109 | */
110 | class MockNotification {
111 | /**
112 | * Configuration
113 | */
114 | public config: NotifierConfig;
115 |
116 | /**
117 | * Notification ID
118 | */
119 | public id = 'ID_FAKE';
120 |
121 | /**
122 | * Notification type
123 | */
124 | public type = 'SUCCESS';
125 |
126 | /**
127 | * Notification message
128 | */
129 | public message = 'Lorem ipsum dolor sit amet.';
130 |
131 | /**
132 | * Notification component
133 | */
134 | public component: { [key: string]: () => any } = {
135 | getConfig: () => this.config,
136 | getHeight: () => mockNotificationHeight,
137 | getShift: () => mockNotificationShift,
138 | getWidth: () => mockNotificationWidth,
139 | };
140 |
141 | /**
142 | * Constructor
143 | *
144 | * @param {NotifierConfig} config Configuration
145 | */
146 | public constructor(config: NotifierConfig) {
147 | this.config = config;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/models/notifier-config.model.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Notifier options
3 | */
4 | export interface NotifierOptions {
5 | animations?: {
6 | enabled?: boolean;
7 | hide?: {
8 | easing?: string;
9 | offset?: number | false;
10 | preset?: string;
11 | speed?: number;
12 | };
13 | overlap?: number | false;
14 | shift?: {
15 | easing?: string;
16 | speed?: number;
17 | };
18 | show?: {
19 | easing?: string;
20 | preset?: string;
21 | speed?: number;
22 | };
23 | };
24 | behaviour?: {
25 | autoHide?: number | false;
26 | onClick?: 'hide' | false;
27 | onMouseover?: 'pauseAutoHide' | 'resetAutoHide' | false;
28 | showDismissButton?: boolean;
29 | stacking?: number | false;
30 | };
31 | position?: {
32 | horizontal?: {
33 | distance?: number;
34 | position?: 'left' | 'middle' | 'right';
35 | };
36 | vertical?: {
37 | distance?: number;
38 | gap?: number;
39 | position?: 'top' | 'bottom';
40 | };
41 | };
42 | theme?: string;
43 | }
44 |
45 | /**
46 | * Notifier configuration
47 | *
48 | * The notifier configuration defines what notifications look like, how they behave, and how they get animated. It is a global
49 | * configuration, which means that it only can be set once (at the beginning), and cannot be changed afterwards. Aligning to the world of
50 | * Angular, this configuration can be provided in the root app module - alternatively, a meaningful default configuration will be used.
51 | */
52 | export class NotifierConfig implements NotifierOptions {
53 | /**
54 | * Customize animations
55 | */
56 | public animations: {
57 | enabled: boolean;
58 | hide: {
59 | easing: string;
60 | offset: number | false;
61 | preset: string;
62 | speed: number;
63 | };
64 | overlap: number | false;
65 | shift: {
66 | easing: string;
67 | speed: number;
68 | };
69 | show: {
70 | easing: string;
71 | preset: string;
72 | speed: number;
73 | };
74 | };
75 |
76 | /**
77 | * Customize behaviour
78 | */
79 | public behaviour: {
80 | autoHide: number | false;
81 | onClick: 'hide' | false;
82 | onMouseover: 'pauseAutoHide' | 'resetAutoHide' | false;
83 | showDismissButton: boolean;
84 | stacking: number | false;
85 | };
86 |
87 | /**
88 | * Customize positioning
89 | */
90 | public position: {
91 | horizontal: {
92 | distance: number;
93 | position: 'left' | 'middle' | 'right';
94 | };
95 | vertical: {
96 | distance: number;
97 | gap: number;
98 | position: 'top' | 'bottom';
99 | };
100 | };
101 |
102 | /**
103 | * Customize theming
104 | */
105 | public theme: string;
106 |
107 | /**
108 | * Constructor
109 | *
110 | * @param [customOptions={}] Custom notifier options, optional
111 | */
112 | public constructor(customOptions: NotifierOptions = {}) {
113 | // Set default values
114 | this.animations = {
115 | enabled: true,
116 | hide: {
117 | easing: 'ease',
118 | offset: 50,
119 | preset: 'fade',
120 | speed: 300,
121 | },
122 | overlap: 150,
123 | shift: {
124 | easing: 'ease',
125 | speed: 300,
126 | },
127 | show: {
128 | easing: 'ease',
129 | preset: 'slide',
130 | speed: 300,
131 | },
132 | };
133 | this.behaviour = {
134 | autoHide: 7000,
135 | onClick: false,
136 | onMouseover: 'pauseAutoHide',
137 | showDismissButton: true,
138 | stacking: 4,
139 | };
140 | this.position = {
141 | horizontal: {
142 | distance: 12,
143 | position: 'left',
144 | },
145 | vertical: {
146 | distance: 12,
147 | gap: 10,
148 | position: 'bottom',
149 | },
150 | };
151 | this.theme = 'material';
152 |
153 | // The following merges the custom options into the notifier config, respecting the already set default values
154 | // This linear, more explicit and code-sizy workflow is preferred here over a recursive one (because we know the object structure)
155 | // Technical sidenote: Objects are merged, other types of values simply overwritten / copied
156 | if (customOptions.theme !== undefined) {
157 | this.theme = customOptions.theme;
158 | }
159 | if (customOptions.animations !== undefined) {
160 | if (customOptions.animations.enabled !== undefined) {
161 | this.animations.enabled = customOptions.animations.enabled;
162 | }
163 | if (customOptions.animations.overlap !== undefined) {
164 | this.animations.overlap = customOptions.animations.overlap;
165 | }
166 | if (customOptions.animations.hide !== undefined) {
167 | Object.assign(this.animations.hide, customOptions.animations.hide);
168 | }
169 | if (customOptions.animations.shift !== undefined) {
170 | Object.assign(this.animations.shift, customOptions.animations.shift);
171 | }
172 | if (customOptions.animations.show !== undefined) {
173 | Object.assign(this.animations.show, customOptions.animations.show);
174 | }
175 | }
176 | if (customOptions.behaviour !== undefined) {
177 | Object.assign(this.behaviour, customOptions.behaviour);
178 | }
179 | if (customOptions.position !== undefined) {
180 | if (customOptions.position.horizontal !== undefined) {
181 | Object.assign(this.position.horizontal, customOptions.position.horizontal);
182 | }
183 | if (customOptions.position.vertical !== undefined) {
184 | Object.assign(this.position.vertical, customOptions.position.vertical);
185 | }
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/services/notifier.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { inject, TestBed } from '@angular/core/testing';
3 | import { Subject } from 'rxjs';
4 |
5 | import { NotifierAction } from '../models/notifier-action.model';
6 | import { NotifierConfig } from '../models/notifier-config.model';
7 | import { NotifierNotificationOptions } from '../models/notifier-notification.model';
8 | import { NotifierConfigToken } from '../notifier.tokens';
9 | import { NotifierService } from './notifier.service';
10 | import { NotifierQueueService } from './notifier-queue.service';
11 |
12 | /**
13 | * Notifier Service - Unit Test
14 | */
15 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
16 | animations: {
17 | enabled: true,
18 | hide: {
19 | easing: 'ease',
20 | offset: 50,
21 | preset: 'fade',
22 | speed: 300,
23 | },
24 | overlap: 150,
25 | shift: {
26 | easing: 'ease',
27 | speed: 300,
28 | },
29 | show: {
30 | easing: 'ease',
31 | preset: 'slide',
32 | speed: 300,
33 | },
34 | },
35 | behaviour: {
36 | autoHide: 7000,
37 | onClick: false,
38 | onMouseover: 'pauseAutoHide',
39 | showDismissButton: true,
40 | stacking: 4,
41 | },
42 | position: {
43 | horizontal: {
44 | distance: 12,
45 | position: 'left',
46 | },
47 | vertical: {
48 | distance: 12,
49 | gap: 10,
50 | position: 'bottom',
51 | },
52 | },
53 | theme: 'material',
54 | });
55 |
56 | describe('Notifier Service', () => {
57 | let service: NotifierService;
58 | let queueService: MockNotifierQueueService;
59 |
60 | // Setup test module
61 | beforeEach(() => {
62 | TestBed.configureTestingModule({
63 | providers: [
64 | NotifierService,
65 | {
66 | provide: NotifierQueueService,
67 | useClass: MockNotifierQueueService,
68 | },
69 | {
70 | provide: NotifierConfigToken,
71 | useValue: testNotifierConfig,
72 | },
73 | ],
74 | });
75 | });
76 |
77 | // Inject dependencies
78 | beforeEach(inject(
79 | [NotifierService, NotifierQueueService],
80 | (notifierService: NotifierService, notifierQueueService: MockNotifierQueueService) => {
81 | service = notifierService;
82 | queueService = notifierQueueService;
83 | },
84 | ));
85 |
86 | it('should instantiate', () => {
87 | expect(service).toBeDefined();
88 | });
89 |
90 | it('should show a notification', () => {
91 | const testNotificationOptions: NotifierNotificationOptions = {
92 | id: 'ID_FAKE',
93 | message: 'Lorem ipsum dolor sit amet.',
94 | type: 'SUCCESS',
95 | };
96 | const expectedAction: NotifierAction = {
97 | payload: testNotificationOptions,
98 | type: 'SHOW',
99 | };
100 | service.show(testNotificationOptions);
101 |
102 | expect(queueService.lastAction).toEqual(expectedAction);
103 | });
104 |
105 | it('should show a notification, the simply way', () => {
106 | const testNotificationOptions: NotifierNotificationOptions = {
107 | message: 'Lorem ipsum dolor sit amet.',
108 | type: 'SUCCESS',
109 | };
110 | const expectedAction: NotifierAction = {
111 | payload: testNotificationOptions,
112 | type: 'SHOW',
113 | };
114 | service.notify(testNotificationOptions.type, testNotificationOptions.message);
115 |
116 | expect(queueService.lastAction).toEqual(expectedAction);
117 | });
118 |
119 | it('should show a notification with an explicit ID, the simply way', () => {
120 | const testNotificationOptions: NotifierNotificationOptions = {
121 | id: 'ID_FAKE',
122 | message: 'Lorem ipsum dolor sit amet.',
123 | type: 'SUCCESS',
124 | };
125 | const expectedAction: NotifierAction = {
126 | payload: testNotificationOptions,
127 | type: 'SHOW',
128 | };
129 | service.notify(testNotificationOptions.type, testNotificationOptions.message, testNotificationOptions.id);
130 |
131 | expect(queueService.lastAction).toEqual(expectedAction);
132 | });
133 |
134 | it('should hide a specific notification', () => {
135 | const testNotificationId = 'ID_FAKE';
136 | const expectedAction: NotifierAction = {
137 | payload: testNotificationId,
138 | type: 'HIDE',
139 | };
140 | service.hide(testNotificationId);
141 |
142 | expect(queueService.lastAction).toEqual(expectedAction);
143 | });
144 |
145 | it('should hide the newest notification', () => {
146 | const expectedAction: NotifierAction = {
147 | type: 'HIDE_NEWEST',
148 | };
149 | service.hideNewest();
150 |
151 | expect(queueService.lastAction).toEqual(expectedAction);
152 | });
153 |
154 | it('should hide the oldest notification', () => {
155 | const expectedAction: NotifierAction = {
156 | type: 'HIDE_OLDEST',
157 | };
158 | service.hideOldest();
159 |
160 | expect(queueService.lastAction).toEqual(expectedAction);
161 | });
162 |
163 | it('should hide all notifications', () => {
164 | const expectedAction: NotifierAction = {
165 | type: 'HIDE_ALL',
166 | };
167 | service.hideAll();
168 |
169 | expect(queueService.lastAction).toEqual(expectedAction);
170 | });
171 |
172 | it('should return the configuration', () => {
173 | expect(service.getConfig()).toEqual(testNotifierConfig);
174 | });
175 |
176 | it('should return the notification action', () => {
177 | const testNotificationId = 'ID_FAKE';
178 | const expectedAction: NotifierAction = {
179 | payload: testNotificationId,
180 | type: 'HIDE',
181 | };
182 | service.actionStream.subscribe((action) => expect(action).toEqual(expectedAction));
183 | service.hide(testNotificationId);
184 | });
185 | });
186 |
187 | /**
188 | * Mock Notifier Queue Service
189 | */
190 | @Injectable()
191 | class MockNotifierQueueService extends NotifierQueueService {
192 | /**
193 | * Last action
194 | */
195 | public lastAction: NotifierAction;
196 | public actionStream = new Subject();
197 |
198 | /**
199 | * Push a new action to the queue
200 | *
201 | * @param {NotifierAction} action Action object
202 | *
203 | * @override
204 | */
205 | public push(action: NotifierAction): void {
206 | this.lastAction = action;
207 | this.actionStream.next(action);
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/animation-presets/slide.animation-preset.spec.ts:
--------------------------------------------------------------------------------
1 | import { NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';
2 | import { NotifierConfig } from '../models/notifier-config.model';
3 | import { slide } from './slide.animation-preset';
4 |
5 | /**
6 | * Slide Animation Preset - Unit Test
7 | */
8 | describe('Slide Animation Preset', () => {
9 | describe('(show)', () => {
10 | it('should return animation keyframes for top-left position', () => {
11 | const testConfig: NotifierConfig = new NotifierConfig({
12 | position: {
13 | horizontal: {
14 | distance: 50,
15 | position: 'left',
16 | },
17 | vertical: {
18 | distance: 100,
19 | position: 'top',
20 | },
21 | },
22 | });
23 | const testNotification: MockNotification = new MockNotification(testConfig);
24 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
25 | from: {
26 | transform: `translate3d( calc( -100% - ${testConfig.position.horizontal.distance}px - 10px ), 0, 0 )`,
27 | },
28 | to: {
29 | transform: 'translate3d( 0, 0, 0 )',
30 | },
31 | };
32 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
33 |
34 | expect(keyframes).toEqual(expectedKeyframes);
35 | });
36 |
37 | it('should return animation keyframes for top-right position', () => {
38 | const testConfig: NotifierConfig = new NotifierConfig({
39 | position: {
40 | horizontal: {
41 | distance: 50,
42 | position: 'right',
43 | },
44 | vertical: {
45 | distance: 100,
46 | position: 'top',
47 | },
48 | },
49 | });
50 | const testNotification: MockNotification = new MockNotification(testConfig);
51 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
52 | from: {
53 | transform: `translate3d( calc( 100% + ${testConfig.position.horizontal.distance}px + 10px ), 0, 0 )`,
54 | },
55 | to: {
56 | transform: 'translate3d( 0, 0, 0 )',
57 | },
58 | };
59 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
60 |
61 | expect(keyframes).toEqual(expectedKeyframes);
62 | });
63 |
64 | it('should return animation keyframes for top-middle position', () => {
65 | const testConfig: NotifierConfig = new NotifierConfig({
66 | position: {
67 | horizontal: {
68 | distance: 50,
69 | position: 'middle',
70 | },
71 | vertical: {
72 | distance: 100,
73 | position: 'top',
74 | },
75 | },
76 | });
77 | const testNotification: MockNotification = new MockNotification(testConfig);
78 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
79 | from: {
80 | transform: `translate3d( -50%, calc( -100% - ${testConfig.position.horizontal.distance}px - 10px ), 0 )`,
81 | },
82 | to: {
83 | transform: 'translate3d( -50%, 0, 0 )',
84 | },
85 | };
86 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
87 |
88 | expect(keyframes).toEqual(expectedKeyframes);
89 | });
90 |
91 | it('should return animation keyframes for bottom-middle position', () => {
92 | const testConfig: NotifierConfig = new NotifierConfig({
93 | position: {
94 | horizontal: {
95 | distance: 50,
96 | position: 'middle',
97 | },
98 | vertical: {
99 | distance: 100,
100 | position: 'bottom',
101 | },
102 | },
103 | });
104 | const testNotification: MockNotification = new MockNotification(testConfig);
105 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
106 | from: {
107 | transform: `translate3d( -50%, calc( 100% + ${testConfig.position.horizontal.distance}px + 10px ), 0 )`,
108 | },
109 | to: {
110 | transform: 'translate3d( -50%, 0, 0 )',
111 | },
112 | };
113 | const keyframes: NotifierAnimationPresetKeyframes = slide.show(testNotification);
114 |
115 | expect(keyframes).toEqual(expectedKeyframes);
116 | });
117 | });
118 |
119 | describe('(hide)', () => {
120 | it('should return animation keyframes for top-left position', () => {
121 | const testConfig: NotifierConfig = new NotifierConfig({
122 | position: {
123 | horizontal: {
124 | distance: 50,
125 | position: 'left',
126 | },
127 | vertical: {
128 | distance: 100,
129 | position: 'top',
130 | },
131 | },
132 | });
133 | const testNotification: MockNotification = new MockNotification(testConfig);
134 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
135 | from: {
136 | transform: `translate3d( 0, ${testNotification.component.getShift()}px, 0 )`,
137 | },
138 | to: {
139 | transform: `translate3d( calc( -100% - ${
140 | testConfig.position.horizontal.distance
141 | }px - 10px ), ${testNotification.component.getShift()}px, 0 )`,
142 | },
143 | };
144 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
145 |
146 | expect(keyframes).toEqual(expectedKeyframes);
147 | });
148 |
149 | it('should return animation keyframes for top-right position', () => {
150 | const testConfig: NotifierConfig = new NotifierConfig({
151 | position: {
152 | horizontal: {
153 | distance: 50,
154 | position: 'right',
155 | },
156 | vertical: {
157 | distance: 100,
158 | position: 'top',
159 | },
160 | },
161 | });
162 | const testNotification: MockNotification = new MockNotification(testConfig);
163 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
164 | from: {
165 | transform: `translate3d( 0, ${testNotification.component.getShift()}px, 0 )`,
166 | },
167 | to: {
168 | transform: `translate3d( calc( 100% + ${
169 | testConfig.position.horizontal.distance
170 | }px + 10px ), ${testNotification.component.getShift()}px, 0 )`,
171 | },
172 | };
173 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
174 |
175 | expect(keyframes).toEqual(expectedKeyframes);
176 | });
177 |
178 | it('should return animation keyframes for top-middle position', () => {
179 | const testConfig: NotifierConfig = new NotifierConfig({
180 | position: {
181 | horizontal: {
182 | distance: 50,
183 | position: 'middle',
184 | },
185 | vertical: {
186 | distance: 100,
187 | position: 'top',
188 | },
189 | },
190 | });
191 | const testNotification: MockNotification = new MockNotification(testConfig);
192 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
193 | from: {
194 | transform: `translate3d( -50%, ${testNotification.component.getShift()}px, 0 )`,
195 | },
196 | to: {
197 | transform: `translate3d( -50%, calc( -100% - ${testConfig.position.horizontal.distance}px - 10px ), 0 )`,
198 | },
199 | };
200 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
201 |
202 | expect(keyframes).toEqual(expectedKeyframes);
203 | });
204 |
205 | it('should return animation keyframes for bottom-middle position', () => {
206 | const testConfig: NotifierConfig = new NotifierConfig({
207 | position: {
208 | horizontal: {
209 | distance: 50,
210 | position: 'middle',
211 | },
212 | vertical: {
213 | distance: 100,
214 | position: 'bottom',
215 | },
216 | },
217 | });
218 | const testNotification: MockNotification = new MockNotification(testConfig);
219 | const expectedKeyframes: NotifierAnimationPresetKeyframes = {
220 | from: {
221 | transform: `translate3d( -50%, ${testNotification.component.getShift()}px, 0 )`,
222 | },
223 | to: {
224 | transform: `translate3d( -50%, calc( 100% + ${testConfig.position.horizontal.distance}px + 10px ), 0 )`,
225 | },
226 | };
227 | const keyframes: NotifierAnimationPresetKeyframes = slide.hide(testNotification);
228 |
229 | expect(keyframes).toEqual(expectedKeyframes);
230 | });
231 | });
232 | });
233 |
234 | /**
235 | * Mock Notification Height
236 | */
237 | const mockNotificationHeight = 40;
238 |
239 | /**
240 | * Mock Notification Shift
241 | */
242 | const mockNotificationShift = 80;
243 |
244 | /**
245 | * Mock Notification Width
246 | */
247 | const mockNotificationWidth = 300;
248 |
249 | /**
250 | * Mock notification, providing static values except the global configuration
251 | */
252 | class MockNotification {
253 | /**
254 | * Configuration
255 | */
256 | public config: NotifierConfig;
257 |
258 | /**
259 | * Notification ID
260 | */
261 | public id = 'ID_FAKE';
262 |
263 | /**
264 | * Notification type
265 | */
266 | public type = 'SUCCESS';
267 |
268 | /**
269 | * Notification message
270 | */
271 | public message = 'Lorem ipsum dolor sit amet.';
272 |
273 | /**
274 | * Notification component
275 | */
276 | public component: any = {
277 | getConfig: () => this.config,
278 | getHeight: () => mockNotificationHeight,
279 | getShift: () => mockNotificationShift,
280 | getWidth: () => mockNotificationWidth,
281 | };
282 |
283 | /**
284 | * Constructor
285 | *
286 | * @param {NotifierConfig} config Configuration
287 | */
288 | public constructor(config: NotifierConfig) {
289 | this.config = config;
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Also see **[GitHub releases](https://github.com/dominique-mueller/angular-notifier/releases)**.
4 |
5 |
6 |
7 | ## [14.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/14.0.0) (2023-06-05)
8 |
9 | ### Features
10 |
11 | - Upgrade to Angular 16 ([#256](https://github.com/dominique-mueller/angular-notifier/pull/256))
12 |
13 | ### BREAKING CHANGES
14 |
15 | - The upgrade to Angular 16 breaks compatibility with Angular 15.
16 |
17 |
18 |
19 | ## [13.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/13.0.0) (2023-06-05)
20 |
21 | ### Features
22 |
23 | - Upgrade to Angular 15 ([#255](https://github.com/dominique-mueller/angular-notifier/pull/255))
24 |
25 | ### BREAKING CHANGES
26 |
27 | - The upgrade to Angular 15 breaks compatibility with Angular 14.
28 |
29 |
30 |
31 | ## [12.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/12.0.0) (2022-09-13)
32 |
33 | ### Features
34 |
35 | - Upgrade to Angular 14 ([#243](https://github.com/dominique-mueller/angular-notifier/pull/243))
36 |
37 | ### BREAKING CHANGES
38 |
39 | - The upgrade to Angular 14 breaks compatibility with Angular 13.
40 |
41 |
42 |
43 | ## [11.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/11.0.0) (2022-02-20)
44 |
45 | ### Features
46 |
47 | - Upgrade to Angular 13, enable partial-Ivy compilation ([#235](https://github.com/dominique-mueller/angular-notifier/pull/235))
48 |
49 | ### BREAKING CHANGES
50 |
51 | - The upgrade to Angular 13 breaks compatibility with Angular 12. The library is now published as partial-Ivy code.
52 |
53 |
54 |
55 | ## [10.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/10.0.0) (2022-02-20)
56 |
57 | ### Features
58 |
59 | - Upgrade to Angular 12 ([#232](https://github.com/dominique-mueller/angular-notifier/pull/232))
60 |
61 | ### BREAKING CHANGES
62 |
63 | - The upgrade to Angular 12 breaks compatibility with Angular 11.
64 |
65 |
66 |
67 | ## [9.1.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/9.1.0) (2021-07-13)
68 |
69 | ### Features
70 |
71 | - Expose `actionStream` on `NotifierService` ([#214](https://github.com/dominique-mueller/angular-notifier/pull/214))
72 |
73 |
74 |
75 | ## [9.0.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/9.0.1) (2021-03-12)
76 |
77 | ### Styles
78 |
79 | - Remove unnecessary white space around `notifier-container` ([#204](https://github.com/dominique-mueller/angular-notifier/pull/204))
80 |
81 |
82 |
83 | ## [9.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/9.0.0) (2021-03-12)
84 |
85 | ### Features
86 |
87 | - Upgrade to Angular 11 ([#203](https://github.com/dominique-mueller/angular-notifier/pull/203))
88 |
89 | ### BREAKING CHANGES
90 |
91 | - The upgrade to Angular 11 breaks compatibility with Angular 10.
92 |
93 |
94 |
95 | ## [8.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/8.0.0) (2021-03-11)
96 |
97 | ### Features
98 |
99 | - Upgrade to Angular 10 ([#202](https://github.com/dominique-mueller/angular-notifier/pull/202))
100 |
101 | ### BREAKING CHANGES
102 |
103 | - The upgrade to Angular 10 breaks compatibility with Angular 9.
104 |
105 |
106 |
107 | ## [7.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/7.0.0) (2021-03-11)
108 |
109 | ### Features
110 |
111 | - Upgrade to Angular 9 ([#201](https://github.com/dominique-mueller/angular-notifier/pull/201))
112 |
113 | ### BREAKING CHANGES
114 |
115 | - The upgrade to Angular 9 breaks compatibility with Angular 8.
116 |
117 |
118 |
119 | ## [6.0.2](https://github.com/dominique-mueller/angular-notifier/releases/tag/6.0.2) (2021-03-11)
120 |
121 | ### Bug Fixes
122 |
123 | - Fix "sideEffects" property in package.json causing issues when importing styles ([#186](https://github.com/dominique-mueller/angular-notifier/pull/186)), closes [#183](https://github.com/dominique-mueller/angular-notifier/issues/183)
124 |
125 | ### Styles
126 |
127 | - Fix layout falling apart when message becomes multi-line ([#200](https://github.com/dominique-mueller/angular-notifier/pull/200)), closes [#149](https://github.com/dominique-mueller/angular-notifier/issues/149)
128 |
129 |
130 |
131 | ## [6.0.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/6.0.1) (2019-10-20)
132 |
133 | ### Bug Fixes
134 |
135 | - **notifier-container:** Setup notifier-container as early as possible ([#144](https://github.com/dominique-mueller/angular-notifier/issues/144)) ([17b5953](https://github.com/dominique-mueller/angular-notifier/commit/17b5953)), closes [#119](https://github.com/dominique-mueller/angular-notifier/issues/119)
136 |
137 | ### Documentation
138 |
139 | - **README:** Add version information to README ([#143](https://github.com/dominique-mueller/angular-notifier/issues/143)) ([f838719](https://github.com/dominique-mueller/angular-notifier/commit/f838719))
140 |
141 |
142 |
143 | ## [6.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/6.0.0) (2019-10-19)
144 |
145 | ### Features
146 |
147 | - Upgrade to Angular 8 ([#139](https://github.com/dominique-mueller/angular-notifier/issues/139)) ([b355287](https://github.com/dominique-mueller/angular-notifier/commit/b355287))
148 |
149 | ### BREAKING CHANGES
150 |
151 | - The upgrade to Angular 8 breaks compatibility with Angular 7 (and previous versions).
152 |
153 |
154 |
155 | ## [5.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/5.0.0) (2019-10-19)
156 |
157 | ### Features
158 |
159 | - Upgrade to Angular 7 ([#134](https://github.com/dominique-mueller/angular-notifier/issues/134)) ([8f13440](https://github.com/dominique-mueller/angular-notifier/commit/8f13440))
160 |
161 | ### BREAKING CHANGES
162 |
163 | - The upgrade to Angular 7 breaks compatibility with Angular 6 (and previous versions).
164 |
165 |
166 |
167 | ## [4.1.2](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.1.2) (2019-10-18)
168 |
169 | ### Bug Fixes
170 |
171 | - **notifier:** Fix circular dependency issues of injection tokens ([#124](https://github.com/dominique-mueller/angular-notifier/issues/124)) ([139d43c](https://github.com/dominique-mueller/angular-notifier/commit/139d43c))
172 |
173 |
174 |
175 | ## [4.1.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.1.1) (2018-08-09)
176 |
177 | ### Bug Fixes
178 |
179 | - **package:** Fix artifact ([#99](https://github.com/dominique-mueller/angular-notifier/issues/99)) ([7ce901b](https://github.com/dominique-mueller/angular-notifier/commit/7ce901b))
180 |
181 |
182 |
183 | ## [4.1.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.1.0) (2018-08-08)
184 |
185 | ### Features
186 |
187 | - **notification:** Allow templateRef as notification content ([#95](https://github.com/dominique-mueller/angular-notifier/issues/95)) ([d705180](https://github.com/dominique-mueller/angular-notifier/commit/d705180))
188 |
189 | ### Documentation
190 |
191 | - **README:** Update demo to Stackblitz example ([#93](https://github.com/dominique-mueller/angular-notifier/issues/93)) ([1e26507](https://github.com/dominique-mueller/angular-notifier/commit/1e26507))
192 |
193 |
194 |
195 | ## [4.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/4.0.0) (2018-07-04)
196 |
197 | ### Features
198 |
199 | - Upgrade to Angular 6, fix breaking changes ([#83](https://github.com/dominique-mueller/angular-notifier/issues/83)) ([aae723d](https://github.com/dominique-mueller/angular-notifier/commit/aae723d)), closes [#82](https://github.com/dominique-mueller/angular-notifier/issues/82)
200 |
201 | ### BREAKING CHANGES
202 |
203 | - The upgrade to Angular 6 breaks compatibility with Angular 5.
204 |
205 |
206 |
207 | ## [3.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/3.0.0) (2018-01-18)
208 |
209 | ### Features
210 |
211 | - **angular:** Upgrade to Angular 5 ([#38](https://github.com/dominique-mueller/angular-notifier/issues/38)) ([355785e](https://github.com/dominique-mueller/angular-notifier/commit/355785e))
212 |
213 | ### Documentation
214 |
215 | - **README:** Add Angular compatibility details, cleanup ([#40](https://github.com/dominique-mueller/angular-notifier/issues/40)) ([9286920](https://github.com/dominique-mueller/angular-notifier/commit/9286920))
216 | - **README:** Fix wrong notifier container selector ([#32](https://github.com/dominique-mueller/angular-notifier/issues/32)) ([7b82d35](https://github.com/dominique-mueller/angular-notifier/commit/7b82d35)), closes [#30](https://github.com/dominique-mueller/angular-notifier/issues/30)
217 |
218 | ### BREAKING CHANGES
219 |
220 | - **angular:** The upgrade to Angular 5 breaks compatibility with Angular 4.
221 |
222 |
223 |
224 | ## [2.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/2.0.0) (2017-05-11)
225 |
226 | ### Features
227 |
228 | - **angular:** Upgrade to Angular 4 and its new APIs ([#19](https://github.com/dominique-mueller/angular-notifier/issues/19)) ([0a0be99](https://github.com/dominique-mueller/angular-notifier/commit/0a0be99))
229 |
230 | ### Bug Fixes
231 |
232 | - **notifier-config:** Fix notifier config injection error, refactor notifier module ([#22](https://github.com/dominique-mueller/angular-notifier/issues/22)) ([67f09f5](https://github.com/dominique-mueller/angular-notifier/commit/67f09f5)), closes [#17](https://github.com/dominique-mueller/angular-notifier/issues/17)
233 |
234 | ### Documentation
235 |
236 | - **preview:** Update animated GIF preview showing the new colors ([#18](https://github.com/dominique-mueller/angular-notifier/issues/18)) ([571b098](https://github.com/dominique-mueller/angular-notifier/commit/571b098))
237 | - **README, MIGRATION:** Update README, add MIGRATION-GUIDE ([#28](https://github.com/dominique-mueller/angular-notifier/issues/28)) ([f2c7781](https://github.com/dominique-mueller/angular-notifier/commit/f2c7781))
238 |
239 | ### Refactoring
240 |
241 | - **animations:** Refactor usage of Web Animations API, add typings ([#27](https://github.com/dominique-mueller/angular-notifier/issues/27)) ([d34f9f3](https://github.com/dominique-mueller/angular-notifier/commit/d34f9f3)), closes [#6](https://github.com/dominique-mueller/angular-notifier/issues/6) [#10](https://github.com/dominique-mueller/angular-notifier/issues/10)
242 | - **naming:** Refactor namings to no longer use the "x-" prefix ([#26](https://github.com/dominique-mueller/angular-notifier/issues/26)) ([d2158bd](https://github.com/dominique-mueller/angular-notifier/commit/d2158bd))
243 |
244 | ### BREAKING CHANGES
245 |
246 | - **naming:** Compontent selectors and class name no longer have the "x-" prefix (see MIGRATION GUIDE).
247 | - **notifier-config:** The forRoot() method of the NotifierModule is now called withConfig() (see MIGRATION GUIDE).
248 | - **angular:** The upgrade to Angular 4 and its APIs breaks compatibility with all Angular 2 based applications.
249 |
250 |
251 |
252 | ## [1.0.6](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.6) (2017-04-04)
253 |
254 | ### Styles
255 |
256 | - **type-colors:** Use bootstrap colors for notification types ([18eb1d2](https://github.com/dominique-mueller/angular-notifier/commit/18eb1d2)), closes [#11](https://github.com/dominique-mueller/angular-notifier/issues/11)
257 |
258 |
259 |
260 | ## [1.0.5](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.5) (2017-04-03)
261 |
262 | ### Bug Fixes
263 |
264 | - **notification-container:** Fix wrong ngFor trackby implementation ([f086ae4](https://github.com/dominique-mueller/angular-notifier/commit/f086ae4)), closes [#12](https://github.com/dominique-mueller/angular-notifier/issues/12)
265 |
266 |
267 |
268 | ## [1.0.4](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.4) (2017-03-21)
269 |
270 | ### Bug Fixes
271 |
272 | - **aot:** Fixed Angular AoT compilation issue ([e5ed9bb](https://github.com/dominique-mueller/angular-notifier/commit/e5ed9bb)), closes [#7](https://github.com/dominique-mueller/angular-notifier/issues/7)
273 |
274 |
275 |
276 | ## [1.0.3](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.3) (2017-02-05)
277 |
278 | ### Bug Fixes
279 |
280 | - **aot:** Fixed error occuring when using NotifierModule.forRoot with ([a501f40](https://github.com/dominique-mueller/angular-notifier/commit/a501f40)), closes [#5](https://github.com/dominique-mueller/angular-notifier/issues/5)
281 |
282 |
283 |
284 | ## [1.0.2](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.2) (2016-12-21)
285 |
286 | ### Bug Fixes
287 |
288 | - **config:** Fixed broken configuration merge ([9793773](https://github.com/dominique-mueller/angular-notifier/commit/9793773))
289 |
290 |
291 |
292 | ## [1.0.1](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.1) (2016-12-05)
293 |
294 | ### Bug Fixes
295 |
296 | - **dependencies:** Fixed wrong type dependencies in definition files ([#2](https://github.com/dominique-mueller/angular-notifier/issues/2)) ([a986e66](https://github.com/dominique-mueller/angular-notifier/commit/a986e66)), closes [#1](https://github.com/dominique-mueller/angular-notifier/issues/1)
297 | - **gulp:** Fixed broken release task ([#3](https://github.com/dominique-mueller/angular-notifier/issues/3)) ([cdee2d8](https://github.com/dominique-mueller/angular-notifier/commit/cdee2d8))
298 |
299 |
300 |
301 | ## [1.0.0](https://github.com/dominique-mueller/angular-notifier/releases/tag/1.0.0) (2016-12-04)
302 |
303 | Initial release!
304 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-notification.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core';
2 |
3 | import { NotifierAnimationData } from '../models/notifier-animation.model';
4 | import { NotifierConfig } from '../models/notifier-config.model';
5 | import { NotifierNotification } from '../models/notifier-notification.model';
6 | import { NotifierService } from '../services/notifier.service';
7 | import { NotifierAnimationService } from '../services/notifier-animation.service';
8 | import { NotifierTimerService } from '../services/notifier-timer.service';
9 |
10 | /**
11 | * Notifier notification component
12 | * -------------------------------
13 | * This component is responsible for actually displaying the notification on screen. In addition, it's able to show and hide this
14 | * notification, in particular to animate this notification in and out, as well as shift (move) this notification vertically around.
15 | * Furthermore, the notification component handles all interactions the user has with this notification / component, such as clicks and
16 | * mouse movements.
17 | */
18 | @Component({
19 | changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters)
20 | host: {
21 | '(click)': 'onNotificationClick()',
22 | '(mouseout)': 'onNotificationMouseout()',
23 | '(mouseover)': 'onNotificationMouseover()',
24 | class: 'notifier__notification',
25 | },
26 | providers: [
27 | // We provide the timer to the component's local injector, so that every notification components gets its own
28 | // instance of the timer service, thus running their timers independently from each other
29 | NotifierTimerService,
30 | ],
31 | selector: 'notifier-notification',
32 | templateUrl: './notifier-notification.component.html',
33 | })
34 | export class NotifierNotificationComponent implements AfterViewInit {
35 | /**
36 | * Input: Notification object, contains all details necessary to construct the notification
37 | */
38 | @Input()
39 | public notification: NotifierNotification;
40 |
41 | /**
42 | * Output: Ready event, handles the initialization success by emitting a reference to this notification component
43 | */
44 | @Output()
45 | public ready: EventEmitter;
46 |
47 | /**
48 | * Output: Dismiss event, handles the click on the dismiss button by emitting the notification ID of this notification component
49 | */
50 | @Output()
51 | public dismiss: EventEmitter;
52 |
53 | /**
54 | * Notifier configuration
55 | */
56 | public readonly config: NotifierConfig;
57 |
58 | /**
59 | * Notifier timer service
60 | */
61 | private readonly timerService: NotifierTimerService;
62 |
63 | /**
64 | * Notifier animation service
65 | */
66 | private readonly animationService: NotifierAnimationService;
67 |
68 | /**
69 | * Angular renderer, used to preserve the overall DOM abstraction & independence
70 | */
71 | private readonly renderer: Renderer2;
72 |
73 | /**
74 | * Native element reference, used for manipulating DOM properties
75 | */
76 | private readonly element: HTMLElement;
77 |
78 | /**
79 | * Current notification height, calculated and cached here (#perfmatters)
80 | */
81 | private elementHeight: number;
82 |
83 | /**
84 | * Current notification width, calculated and cached here (#perfmatters)
85 | */
86 | private elementWidth: number;
87 |
88 | /**
89 | * Current notification shift, calculated and cached here (#perfmatters)
90 | */
91 | private elementShift: number;
92 |
93 | /**
94 | * Constructor
95 | *
96 | * @param elementRef Reference to the component's element
97 | * @param renderer Angular renderer
98 | * @param notifierService Notifier service
99 | * @param notifierTimerService Notifier timer service
100 | * @param notifierAnimationService Notifier animation service
101 | */
102 | public constructor(
103 | elementRef: ElementRef,
104 | renderer: Renderer2,
105 | notifierService: NotifierService,
106 | notifierTimerService: NotifierTimerService,
107 | notifierAnimationService: NotifierAnimationService,
108 | ) {
109 | this.config = notifierService.getConfig();
110 | this.ready = new EventEmitter();
111 | this.dismiss = new EventEmitter();
112 | this.timerService = notifierTimerService;
113 | this.animationService = notifierAnimationService;
114 | this.renderer = renderer;
115 | this.element = elementRef.nativeElement;
116 | this.elementShift = 0;
117 | }
118 |
119 | /**
120 | * Component after view init lifecycle hook, setts up the component and then emits the ready event
121 | */
122 | public ngAfterViewInit(): void {
123 | this.setup();
124 | this.elementHeight = this.element.offsetHeight;
125 | this.elementWidth = this.element.offsetWidth;
126 | this.ready.emit(this);
127 | }
128 |
129 | /**
130 | * Get the notifier config
131 | *
132 | * @returns Notifier configuration
133 | */
134 | public getConfig(): NotifierConfig {
135 | return this.config;
136 | }
137 |
138 | /**
139 | * Get notification element height (in px)
140 | *
141 | * @returns Notification element height (in px)
142 | */
143 | public getHeight(): number {
144 | return this.elementHeight;
145 | }
146 |
147 | /**
148 | * Get notification element width (in px)
149 | *
150 | * @returns Notification element height (in px)
151 | */
152 | public getWidth(): number {
153 | return this.elementWidth;
154 | }
155 |
156 | /**
157 | * Get notification shift offset (in px)
158 | *
159 | * @returns Notification element shift offset (in px)
160 | */
161 | public getShift(): number {
162 | return this.elementShift;
163 | }
164 |
165 | /**
166 | * Show (animate in) this notification
167 | *
168 | * @returns Promise, resolved when done
169 | */
170 | public show(): Promise {
171 | return new Promise((resolve: () => void) => {
172 | // Are animations enabled?
173 | if (this.config.animations.enabled && this.config.animations.show.speed > 0) {
174 | // Get animation data
175 | const animationData: NotifierAnimationData = this.animationService.getAnimationData('show', this.notification);
176 |
177 | // Set initial styles (styles before animation), prevents quick flicker when animation starts
178 | const animatedProperties: Array = Object.keys(animationData.keyframes[0]);
179 | for (let i: number = animatedProperties.length - 1; i >= 0; i--) {
180 | this.renderer.setStyle(this.element, animatedProperties[i], animationData.keyframes[0][animatedProperties[i]]);
181 | }
182 |
183 | // Animate notification in
184 | this.renderer.setStyle(this.element, 'visibility', 'visible');
185 | const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);
186 | animation.onfinish = () => {
187 | this.startAutoHideTimer();
188 | resolve(); // Done
189 | };
190 | } else {
191 | // Show notification
192 | this.renderer.setStyle(this.element, 'visibility', 'visible');
193 | this.startAutoHideTimer();
194 | resolve(); // Done
195 | }
196 | });
197 | }
198 |
199 | /**
200 | * Hide (animate out) this notification
201 | *
202 | * @returns Promise, resolved when done
203 | */
204 | public hide(): Promise {
205 | return new Promise((resolve: () => void) => {
206 | this.stopAutoHideTimer();
207 |
208 | // Are animations enabled?
209 | if (this.config.animations.enabled && this.config.animations.hide.speed > 0) {
210 | const animationData: NotifierAnimationData = this.animationService.getAnimationData('hide', this.notification);
211 | const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);
212 | animation.onfinish = () => {
213 | resolve(); // Done
214 | };
215 | } else {
216 | resolve(); // Done
217 | }
218 | });
219 | }
220 |
221 | /**
222 | * Shift (move) this notification
223 | *
224 | * @param distance Distance to shift (in px)
225 | * @param shiftToMakePlace Flag, defining in which direction to shift
226 | * @returns Promise, resolved when done
227 | */
228 | public shift(distance: number, shiftToMakePlace: boolean): Promise {
229 | return new Promise((resolve: () => void) => {
230 | // Calculate new position (position after the shift)
231 | let newElementShift: number;
232 | if (
233 | (this.config.position.vertical.position === 'top' && shiftToMakePlace) ||
234 | (this.config.position.vertical.position === 'bottom' && !shiftToMakePlace)
235 | ) {
236 | newElementShift = this.elementShift + distance + this.config.position.vertical.gap;
237 | } else {
238 | newElementShift = this.elementShift - distance - this.config.position.vertical.gap;
239 | }
240 | const horizontalPosition: string = this.config.position.horizontal.position === 'middle' ? '-50%' : '0';
241 |
242 | // Are animations enabled?
243 | if (this.config.animations.enabled && this.config.animations.shift.speed > 0) {
244 | const animationData: NotifierAnimationData = {
245 | // TODO: Extract into animation service
246 | keyframes: [
247 | {
248 | transform: `translate3d( ${horizontalPosition}, ${this.elementShift}px, 0 )`,
249 | },
250 | {
251 | transform: `translate3d( ${horizontalPosition}, ${newElementShift}px, 0 )`,
252 | },
253 | ],
254 | options: {
255 | duration: this.config.animations.shift.speed,
256 | easing: this.config.animations.shift.easing,
257 | fill: 'forwards',
258 | },
259 | };
260 | this.elementShift = newElementShift;
261 | const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);
262 | animation.onfinish = () => {
263 | resolve(); // Done
264 | };
265 | } else {
266 | this.renderer.setStyle(this.element, 'transform', `translate3d( ${horizontalPosition}, ${newElementShift}px, 0 )`);
267 | this.elementShift = newElementShift;
268 | resolve(); // Done
269 | }
270 | });
271 | }
272 |
273 | /**
274 | * Handle click on dismiss button
275 | */
276 | public onClickDismiss(): void {
277 | this.dismiss.emit(this.notification.id);
278 | }
279 |
280 | /**
281 | * Handle mouseover over notification area
282 | */
283 | public onNotificationMouseover(): void {
284 | if (this.config.behaviour.onMouseover === 'pauseAutoHide') {
285 | this.pauseAutoHideTimer();
286 | } else if (this.config.behaviour.onMouseover === 'resetAutoHide') {
287 | this.stopAutoHideTimer();
288 | }
289 | }
290 |
291 | /**
292 | * Handle mouseout from notification area
293 | */
294 | public onNotificationMouseout(): void {
295 | if (this.config.behaviour.onMouseover === 'pauseAutoHide') {
296 | this.continueAutoHideTimer();
297 | } else if (this.config.behaviour.onMouseover === 'resetAutoHide') {
298 | this.startAutoHideTimer();
299 | }
300 | }
301 |
302 | /**
303 | * Handle click on notification area
304 | */
305 | public onNotificationClick(): void {
306 | if (this.config.behaviour.onClick === 'hide') {
307 | this.onClickDismiss();
308 | }
309 | }
310 |
311 | /**
312 | * Start the auto hide timer (if enabled)
313 | */
314 | private startAutoHideTimer(): void {
315 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
316 | this.timerService.start(this.config.behaviour.autoHide).then(() => {
317 | this.onClickDismiss();
318 | });
319 | }
320 | }
321 |
322 | /**
323 | * Pause the auto hide timer (if enabled)
324 | */
325 | private pauseAutoHideTimer(): void {
326 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
327 | this.timerService.pause();
328 | }
329 | }
330 |
331 | /**
332 | * Continue the auto hide timer (if enabled)
333 | */
334 | private continueAutoHideTimer(): void {
335 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
336 | this.timerService.continue();
337 | }
338 | }
339 |
340 | /**
341 | * Stop the auto hide timer (if enabled)
342 | */
343 | private stopAutoHideTimer(): void {
344 | if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {
345 | this.timerService.stop();
346 | }
347 | }
348 |
349 | /**
350 | * Initial notification setup
351 | */
352 | private setup(): void {
353 | // Set start position (initially the exact same for every new notification)
354 | if (this.config.position.horizontal.position === 'left') {
355 | this.renderer.setStyle(this.element, 'left', `${this.config.position.horizontal.distance}px`);
356 | } else if (this.config.position.horizontal.position === 'right') {
357 | this.renderer.setStyle(this.element, 'right', `${this.config.position.horizontal.distance}px`);
358 | } else {
359 | this.renderer.setStyle(this.element, 'left', '50%');
360 | // Let's get the GPU handle some work as well (#perfmatters)
361 | this.renderer.setStyle(this.element, 'transform', 'translate3d( -50%, 0, 0 )');
362 | }
363 | if (this.config.position.vertical.position === 'top') {
364 | this.renderer.setStyle(this.element, 'top', `${this.config.position.vertical.distance}px`);
365 | } else {
366 | this.renderer.setStyle(this.element, 'bottom', `${this.config.position.vertical.distance}px`);
367 | }
368 |
369 | // Add classes (responsible for visual design)
370 | this.renderer.addClass(this.element, `notifier__notification--${this.notification.type}`);
371 | this.renderer.addClass(this.element, `notifier__notification--${this.config.theme}`);
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # angular-notifier
4 |
5 | **A well designed, fully animated, highly customizable, and easy-to-use notification library for your **Angular 2+** application.**
6 |
7 |
8 |
9 |
10 |
11 | ## Demo
12 |
13 | You can play around with this library with **[this Stackblitz right here](https://stackblitz.com/edit/angular-notifier-demo)**.
14 |
15 | 
16 |
17 |
52 |
53 | ## How to setup
54 |
55 | Before actually being able to use the **angular-notifier** library within our code, we have to first set it up within Angular, and also
56 | bring the styles into our project.
57 |
58 |
59 |
60 | ### 1. Import the `NotifierModule`
61 |
62 | First of all, make **angular-notifier** globally available to your Angular application by importing (and optionally also configuring) the
63 | `NotifierModule` the your root Angular module. For example:
64 |
65 | ```typescript
66 | import { NotifierModule } from 'angular-notifier';
67 |
68 | @NgModule({
69 | imports: [NotifierModule],
70 | })
71 | export class AppModule {}
72 | ```
73 |
74 | But wait -- your probably might want to customize your notifications' look and behaviour according to your requirements and needs. To do so,
75 | call the `withConfig` method on the `NotifierModule`, and pass in the options. For example:
76 |
77 | ```typescript
78 | import { NotifierModule } from 'angular-notifier';
79 |
80 | @NgModule({
81 | imports: [
82 | NotifierModule.withConfig({
83 | // Custom options in here
84 | }),
85 | ],
86 | })
87 | export class AppModule {}
88 | ```
89 |
90 |
91 |
92 | ### 2. Use the `notifier-container` component
93 |
94 | In addition, you have to place the `notifier-container` component somewhere in your application, best at the last element of your
95 | root (app) component. For example:
96 |
97 | ```typescript
98 | @Component({
99 | selector: 'my-app',
100 | template: `
101 |
Hello World
102 |
103 | `,
104 | })
105 | export class AppComponent {}
106 | ```
107 |
108 | > Later on, this component will contain and manage all your applications' notifications.
109 |
110 |
111 |
112 | ### 3. Import the styles
113 |
114 | Of course we also need to import the **angular-notifier** styles into our application. Depending on the architecture of your Angular
115 | application, you want to either import the original SASS files, or the already compiled CSS files instead - or none of them if you wish to
116 | write your own styles from scratch.
117 |
118 | #### The easy way: Import all the styles
119 |
120 | To import all the styles, simple include either the `~/angular-notifier/styles.(scss|css)` file. It contains the core styles as well as all
121 | the themes and notification types.
122 |
123 | #### The advanced way: Only import the styles actually needed
124 |
125 | To keep the size if your styles as small as possible (improving performance for the perfect UX), your might instead decide to only import
126 | the styles actually needed by our application. The **angular-notifier** styles are modular:
127 |
128 | - The `~/angular-notifier/styles/core.(scss|css)` file is always required, it defines the basic styles (such as the layout)
129 | - Themes can be imported from the `~/angular-notifier/styles/theme` folder
130 | - The different notification types, then, can be imported from the `~/angular-notifier/styles/types` folder
131 |
132 |
133 |
134 | ## How to use
135 |
136 | Using **angular-notifier** is as simple as it can get -- simple import and inject the `NotifierService` into every component (directive,
137 | service, ...) you want to use in. For example:
138 |
139 | ```typescript
140 | import { NotifierService } from 'angular-notifier';
141 |
142 | @Component({
143 | // ...
144 | })
145 | export class MyAwesomeComponent {
146 | private readonly notifier: NotifierService;
147 |
148 | constructor(notifierService: NotifierService) {
149 | this.notifier = notifierService;
150 | }
151 | }
152 | ```
153 |
154 |
155 |
156 | ### Show notifications
157 |
158 | Showing a notification is simple - all your need is a type, and a message to be displayed. For example:
159 |
160 | ```typescript
161 | this.notifier.notify('success', 'You are awesome! I mean it!');
162 | ```
163 |
164 | You can further pass in a _notification ID_ as the third (optional) argument. Essentially, such a _notification ID_ is nothing more but a
165 | unique string tha can be used later on to gain access (and thus control) to this specific notification. For example:
166 |
167 | ```typescript
168 | this.notifier.notify('success', 'You are awesome! I mean it!', 'THAT_NOTIFICATION_ID');
169 | ```
170 |
171 | > For example, you might want to define a _notification ID_ if you know that, at some point in the future, you will need to remove _this
172 | > exact_ notification.
173 |
174 | **The syntax above is actually just a shorthand version of the following:**
175 |
176 | ```typescript
177 | this.notifier.show({
178 | type: 'success',
179 | message: 'You are awesome! I mean it!',
180 | id: 'THAT_NOTIFICATION_ID', // Again, this is optional
181 | });
182 | ```
183 |
184 |
185 |
186 | ### Hide notifications
187 |
188 | You can also hide notifications. To hide a specific notification - assuming you've defined a _notification ID_ when creating it, simply
189 | call:
190 |
191 | ```typescript
192 | this.notifier.hide('THAT_NOTIFICATION_ID');
193 | ```
194 |
195 | Furthermore, your can hide the newest notification by calling:
196 |
197 | ```typescript
198 | this.notifier.hideNewest();
199 | ```
200 |
201 | Or, your could hide the oldest notification:
202 |
203 | ```typescript
204 | this.notifier.hideOldest();
205 | ```
206 |
207 | And, of course, it's also possible to hide all visible notifications at once:
208 |
209 | ```typescript
210 | this.notifier.hideAll();
211 | ```
212 |
213 |
214 |
215 | ## How to customize
216 |
217 | From the beginning, the **angular-notifier** library has been written with customizability in mind. The idea is that **angular-notifier**
218 | works the way your want it to, so that you can make it blend perfectly into the rest of your application. Still, the default configuration
219 | should already provide a great User Experience.
220 |
221 | > Keep in mind that **angular-notifier** can be configured only once - which is at the time you import the `NotifierModule` into your root
222 | > (app) module.
223 |
224 |
225 |
226 | ### Position
227 |
228 | With the `position` property you can define where exactly notifications will appear on the screen:
229 |
230 | ```typescript
231 | position: {
232 |
233 | horizontal: {
234 |
235 | /**
236 | * Defines the horizontal position on the screen
237 | * @type {'left' | 'middle' | 'right'}
238 | */
239 | position: 'left',
240 |
241 | /**
242 | * Defines the horizontal distance to the screen edge (in px)
243 | * @type {number}
244 | */
245 | distance: 12
246 |
247 | },
248 |
249 | vertical: {
250 |
251 | /**
252 | * Defines the vertical position on the screen
253 | * @type {'top' | 'bottom'}
254 | */
255 | position: 'bottom',
256 |
257 | /**
258 | * Defines the vertical distance to the screen edge (in px)
259 | * @type {number}
260 | */
261 | distance: 12
262 |
263 | /**
264 | * Defines the vertical gap, existing between multiple notifications (in px)
265 | * @type {number}
266 | */
267 | gap: 10
268 |
269 | }
270 |
271 | }
272 | ```
273 |
274 |
275 |
276 | ### Theme
277 |
278 | With the `theme` property you can change the overall look and feel of your notifications:
279 |
280 | ```typescript
281 | /**
282 | * Defines the notification theme, responsible for the Visual Design of notifications
283 | * @type {string}
284 | */
285 | theme: 'material';
286 | ```
287 |
288 | #### Theming in detail
289 |
290 | Well, how does theming actually work? In the end, the value set for the `theme` property will be part of a class added to each notification
291 | when being created. For example, using `material` as the theme results in all notifications getting a class assigned named `x-notifier__notification--material`.
292 |
293 | > Everyone - yes, I'm looking at you - can use this mechanism to write custom notification themes and apply them via the `theme` property.
294 | > For example on how to create a theme from scratch, just take a look at the themes coming along with this library (as for now only the
295 | > `material` theme).
296 |
297 |
298 |
299 | ### Behaviour
300 |
301 | With the `behaviour` property you can define how notifications will behave in different situations:
302 |
303 | ```typescript
304 | behaviour: {
305 |
306 | /**
307 | * Defines whether each notification will hide itself automatically after a timeout passes
308 | * @type {number | false}
309 | */
310 | autoHide: 5000,
311 |
312 | /**
313 | * Defines what happens when someone clicks on a notification
314 | * @type {'hide' | false}
315 | */
316 | onClick: false,
317 |
318 | /**
319 | * Defines what happens when someone hovers over a notification
320 | * @type {'pauseAutoHide' | 'resetAutoHide' | false}
321 | */
322 | onMouseover: 'pauseAutoHide',
323 |
324 | /**
325 | * Defines whether the dismiss button is visible or not
326 | * @type {boolean}
327 | */
328 | showDismissButton: true,
329 |
330 | /**
331 | * Defines whether multiple notification will be stacked, and how high the stack limit is
332 | * @type {number | false}
333 | */
334 | stacking: 4
335 |
336 | }
337 | ```
338 |
339 |
340 |
341 | ### Custom Templates
342 |
343 | If you need more control over how the inner HTML part of the notification looks like, either because your style-guide requires it, or for being able to add icons etc, then you can **define a custom ``** which you pass to the `NotifierService`.
344 |
345 | You can define a custom `ng-template` as follows:
346 |
347 | ```html
348 |
349 | {{ notificationData.message }}
350 |
351 | ```
352 |
353 | In this case you could wrap your own HTML, even a `` component which you might use in your application. The notification data is passed in as a `notification` object, which you can reference inside the `` using the `let-` syntax.
354 |
355 | Inside your component, you can then reference the `` by its template variable `#customNotification` using Angular's `ViewChild`:
356 |
357 | ```typescript
358 | import { ViewChild } from '@angular/core';
359 |
360 | @Component({
361 | // ...
362 | })
363 | export class SomeComponent {
364 | @ViewChild('customNotification', { static: true }) customNotificationTmpl;
365 |
366 | constructor(private notifierService: NotifierService) {}
367 |
368 | showNotification() {
369 | this.notifier.show({
370 | message: 'Hi there!',
371 | type: 'info',
372 | template: this.customNotificationTmpl,
373 | });
374 | }
375 | }
376 | ```
377 |
378 |
379 |
380 | ### Animations
381 |
382 | With the `animations` property your can define whether and how exactly notification will be animated:
383 |
384 | ```typescript
385 | animations: {
386 |
387 | /**
388 | * Defines whether all (!) animations are enabled or disabled
389 | * @type {boolean}
390 | */
391 | enabled: true,
392 |
393 | show: {
394 |
395 | /**
396 | * Defines the animation preset that will be used to animate a new notification in
397 | * @type {'fade' | 'slide'}
398 | */
399 | preset: 'slide',
400 |
401 | /**
402 | * Defines how long it will take to animate a new notification in (in ms)
403 | * @type {number}
404 | */
405 | speed: 300,
406 |
407 | /**
408 | * Defines which easing method will be used when animating a new notification in
409 | * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'}
410 | */
411 | easing: 'ease'
412 |
413 | },
414 |
415 | hide: {
416 |
417 | /**
418 | * Defines the animation preset that will be used to animate a new notification out
419 | * @type {'fade' | 'slide'}
420 | */
421 | preset: 'fade',
422 |
423 | /**
424 | * Defines how long it will take to animate a new notification out (in ms)
425 | * @type {number}
426 | */
427 | speed: 300,
428 |
429 | /**
430 | * Defines which easing method will be used when animating a new notification out
431 | * @type {'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'}
432 | */
433 | easing: 'ease',
434 |
435 | /**
436 | * Defines the animation offset used when hiding multiple notifications at once (in ms)
437 | * @type {number | false}
438 | */
439 | offset: 50
440 |
441 | },
442 |
443 | shift: {
444 |
445 | /**
446 | * Defines how long it will take to shift a notification around (in ms)
447 | * @type {number}
448 | */
449 | speed: 300,
450 |
451 | /**
452 | * Defines which easing method will be used when shifting a notification around
453 | * @type {string}
454 | */
455 | easing: 'ease' // All standard CSS easing methods work
456 |
457 | },
458 |
459 | /**
460 | * Defines the overall animation overlap, allowing for much smoother looking animations (in ms)
461 | * @type {number | false}
462 | */
463 | overlap: 150
464 |
465 | }
466 | ```
467 |
468 |
469 |
470 | ### In short -- the default configuration
471 |
472 | To sum it up, the following is the default configuration _(copy-paste-friendly)_:
473 |
474 | ```typescript
475 | const notifierDefaultOptions: NotifierOptions = {
476 | position: {
477 | horizontal: {
478 | position: 'left',
479 | distance: 12,
480 | },
481 | vertical: {
482 | position: 'bottom',
483 | distance: 12,
484 | gap: 10,
485 | },
486 | },
487 | theme: 'material',
488 | behaviour: {
489 | autoHide: 5000,
490 | onClick: false,
491 | onMouseover: 'pauseAutoHide',
492 | showDismissButton: true,
493 | stacking: 4,
494 | },
495 | animations: {
496 | enabled: true,
497 | show: {
498 | preset: 'slide',
499 | speed: 300,
500 | easing: 'ease',
501 | },
502 | hide: {
503 | preset: 'fade',
504 | speed: 300,
505 | easing: 'ease',
506 | offset: 50,
507 | },
508 | shift: {
509 | speed: 300,
510 | easing: 'ease',
511 | },
512 | overlap: 150,
513 | },
514 | };
515 | ```
516 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-container.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
2 | import { Subscription } from 'rxjs';
3 |
4 | import { NotifierAction } from '../models/notifier-action.model';
5 | import { NotifierConfig } from '../models/notifier-config.model';
6 | import { NotifierNotification } from '../models/notifier-notification.model';
7 | import { NotifierService } from '../services/notifier.service';
8 | import { NotifierQueueService } from '../services/notifier-queue.service';
9 | import { NotifierNotificationComponent } from './notifier-notification.component';
10 |
11 | /**
12 | * Notifier container component
13 | * ----------------------------
14 | * This component acts as a wrapper for all notification components; consequently, it is responsible for creating a new notification
15 | * component and removing an existing notification component. Being more precicely, it also handles side effects of those actions, such as
16 | * shifting or even completely removing other notifications as well. Overall, this components handles actions coming from the queue service
17 | * by subscribing to its action stream.
18 | *
19 | * Technical sidenote:
20 | * This component has to be used somewhere in an application to work; it will not inject and create itself automatically, primarily in order
21 | * to not break the Angular AoT compilation. Moreover, this component (and also the notification components) set their change detection
22 | * strategy onPush, which means that we handle change detection manually in order to get the best performance. (#perfmatters)
23 | */
24 | @Component({
25 | changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters)
26 | host: {
27 | class: 'notifier__container',
28 | },
29 | selector: 'notifier-container',
30 | templateUrl: './notifier-container.component.html',
31 | })
32 | export class NotifierContainerComponent implements OnDestroy {
33 | /**
34 | * List of currently somewhat active notifications
35 | */
36 | public notifications: Array;
37 |
38 | /**
39 | * Change detector
40 | */
41 | private readonly changeDetector: ChangeDetectorRef;
42 |
43 | /**
44 | * Notifier queue service
45 | */
46 | private readonly queueService: NotifierQueueService;
47 |
48 | /**
49 | * Notifier configuration
50 | */
51 | private readonly config: NotifierConfig;
52 |
53 | /**
54 | * Queue service observable subscription (saved for cleanup)
55 | */
56 | private queueServiceSubscription: Subscription;
57 |
58 | /**
59 | * Promise resolve function reference, temporarily used while the notification child component gets created
60 | */
61 | private tempPromiseResolver: () => void;
62 |
63 | /**
64 | * Constructor
65 | *
66 | * @param changeDetector Change detector, used for manually triggering change detection runs
67 | * @param notifierQueueService Notifier queue service
68 | * @param notifierService Notifier service
69 | */
70 | public constructor(changeDetector: ChangeDetectorRef, notifierQueueService: NotifierQueueService, notifierService: NotifierService) {
71 | this.changeDetector = changeDetector;
72 | this.queueService = notifierQueueService;
73 | this.config = notifierService.getConfig();
74 | this.notifications = [];
75 |
76 | // Connects this component up to the action queue, then handle incoming actions
77 | this.queueServiceSubscription = this.queueService.actionStream.subscribe((action: NotifierAction) => {
78 | this.handleAction(action).then(() => {
79 | this.queueService.continue();
80 | });
81 | });
82 | }
83 |
84 | /**
85 | * Component destroyment lifecycle hook, cleans up the observable subsciption
86 | */
87 | public ngOnDestroy(): void {
88 | if (this.queueServiceSubscription) {
89 | this.queueServiceSubscription.unsubscribe();
90 | }
91 | }
92 |
93 | /**
94 | * Notification identifier, used as the ngFor trackby function
95 | *
96 | * @param index Index
97 | * @param notification Notifier notification
98 | * @returns Notification ID as the unique identnfier
99 | */
100 | public identifyNotification(index: number, notification: NotifierNotification): string {
101 | return notification.id;
102 | }
103 |
104 | /**
105 | * Event handler, handles clicks on notification dismiss buttons
106 | *
107 | * @param notificationId ID of the notification to dismiss
108 | */
109 | public onNotificationDismiss(notificationId: string): void {
110 | this.queueService.push({
111 | payload: notificationId,
112 | type: 'HIDE',
113 | });
114 | }
115 |
116 | /**
117 | * Event handler, handles notification ready events
118 | *
119 | * @param notificationComponent Notification component reference
120 | */
121 | public onNotificationReady(notificationComponent: NotifierNotificationComponent): void {
122 | const currentNotification: NotifierNotification = this.notifications[this.notifications.length - 1]; // Get the latest notification
123 | currentNotification.component = notificationComponent; // Save the new omponent reference
124 | this.continueHandleShowAction(currentNotification); // Continue with handling the show action
125 | }
126 |
127 | /**
128 | * Handle incoming actions by mapping action types to methods, and then running them
129 | *
130 | * @param action Action object
131 | * @returns Promise, resolved when done
132 | */
133 | private handleAction(action: NotifierAction): Promise {
134 | switch (
135 | action.type // TODO: Maybe a map (actionType -> class method) is a cleaner solution here?
136 | ) {
137 | case 'SHOW':
138 | return this.handleShowAction(action);
139 | case 'HIDE':
140 | return this.handleHideAction(action);
141 | case 'HIDE_OLDEST':
142 | return this.handleHideOldestAction(action);
143 | case 'HIDE_NEWEST':
144 | return this.handleHideNewestAction(action);
145 | case 'HIDE_ALL':
146 | return this.handleHideAllAction();
147 | default:
148 | return new Promise((resolve: () => void) => {
149 | resolve(); // Ignore unknown action types
150 | });
151 | }
152 | }
153 |
154 | /**
155 | * Show a new notification
156 | *
157 | * We simply add the notification to the list, and then wait until its properly initialized / created / rendered.
158 | *
159 | * @param action Action object
160 | * @returns Promise, resolved when done
161 | */
162 | private handleShowAction(action: NotifierAction): Promise {
163 | return new Promise((resolve: () => void) => {
164 | this.tempPromiseResolver = resolve; // Save the promise resolve function so that it can be called later on by another method
165 | this.addNotificationToList(new NotifierNotification(action.payload));
166 | });
167 | }
168 |
169 | /**
170 | * Continue to show a new notification (after the notification components is initialized / created / rendered).
171 | *
172 | * If this is the first (and thus only) notification, we can simply show it. Otherwhise, if stacking is disabled (or a low value), we
173 | * switch out notifications, in particular we hide the existing one, and then show our new one. Yet, if stacking is enabled, we first
174 | * shift all older notifications, and then show our new notification. In addition, if there are too many notification on the screen,
175 | * we hide the oldest one first. Furthermore, if configured, animation overlapping is applied.
176 | *
177 | * @param notification New notification to show
178 | */
179 | private continueHandleShowAction(notification: NotifierNotification): void {
180 | // First (which means only one) notification in the list?
181 | const numberOfNotifications: number = this.notifications.length;
182 | if (numberOfNotifications === 1) {
183 | notification.component.show().then(this.tempPromiseResolver); // Done
184 | } else {
185 | const implicitStackingLimit = 2;
186 |
187 | // Stacking enabled? (stacking value below 2 means stacking is disabled)
188 | if (this.config.behaviour.stacking === false || this.config.behaviour.stacking < implicitStackingLimit) {
189 | this.notifications[0].component.hide().then(() => {
190 | this.removeNotificationFromList(this.notifications[0]);
191 | notification.component.show().then(this.tempPromiseResolver); // Done
192 | });
193 | } else {
194 | const stepPromises: Array> = [];
195 |
196 | // Are there now too many notifications?
197 | if (numberOfNotifications > this.config.behaviour.stacking) {
198 | const oldNotifications: Array = this.notifications.slice(1, numberOfNotifications - 1);
199 |
200 | // Are animations enabled?
201 | if (this.config.animations.enabled) {
202 | // Is animation overlap enabled?
203 | if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {
204 | stepPromises.push(this.notifications[0].component.hide());
205 | setTimeout(() => {
206 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
207 | }, this.config.animations.hide.speed - this.config.animations.overlap);
208 | setTimeout(() => {
209 | stepPromises.push(notification.component.show());
210 | }, this.config.animations.hide.speed + this.config.animations.shift.speed - this.config.animations.overlap);
211 | } else {
212 | stepPromises.push(
213 | new Promise((resolve: () => void) => {
214 | this.notifications[0].component.hide().then(() => {
215 | this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => {
216 | notification.component.show().then(resolve);
217 | });
218 | });
219 | }),
220 | );
221 | }
222 | } else {
223 | stepPromises.push(this.notifications[0].component.hide());
224 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
225 | stepPromises.push(notification.component.show());
226 | }
227 | } else {
228 | const oldNotifications: Array = this.notifications.slice(0, numberOfNotifications - 1);
229 |
230 | // Are animations enabled?
231 | if (this.config.animations.enabled) {
232 | // Is animation overlap enabled?
233 | if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {
234 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
235 | setTimeout(() => {
236 | stepPromises.push(notification.component.show());
237 | }, this.config.animations.shift.speed - this.config.animations.overlap);
238 | } else {
239 | stepPromises.push(
240 | new Promise((resolve: () => void) => {
241 | this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => {
242 | notification.component.show().then(resolve);
243 | });
244 | }),
245 | );
246 | }
247 | } else {
248 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));
249 | stepPromises.push(notification.component.show());
250 | }
251 | }
252 |
253 | Promise.all(stepPromises).then(() => {
254 | if (numberOfNotifications > this.config.behaviour.stacking) {
255 | this.removeNotificationFromList(this.notifications[0]);
256 | }
257 | this.tempPromiseResolver();
258 | }); // Done
259 | }
260 | }
261 | }
262 |
263 | /**
264 | * Hide an existing notification
265 | *
266 | * Fist, we skip everything if there are no notifications at all, or the given notification does not exist. Then, we hide the given
267 | * notification. If there exist older notifications, we then shift them around to fill the gap. Once both hiding the given notification
268 | * and shifting the older notificaitons is done, the given notification gets finally removed (from the DOM).
269 | *
270 | * @param action Action object, payload contains the notification ID
271 | * @returns Promise, resolved when done
272 | */
273 | private handleHideAction(action: NotifierAction): Promise {
274 | return new Promise((resolve: () => void) => {
275 | const stepPromises: Array> = [];
276 |
277 | // Does the notification exist / are there even any notifications? (let's prevent accidential errors)
278 | const notification: NotifierNotification | undefined = this.findNotificationById(action.payload);
279 | if (notification === undefined) {
280 | resolve();
281 | return;
282 | }
283 |
284 | // Get older notifications
285 | const notificationIndex: number | undefined = this.findNotificationIndexById(action.payload);
286 | if (notificationIndex === undefined) {
287 | resolve();
288 | return;
289 | }
290 | const oldNotifications: Array = this.notifications.slice(0, notificationIndex);
291 |
292 | // Do older notifications exist, and thus do we need to shift other notifications as a consequence?
293 | if (oldNotifications.length > 0) {
294 | // Are animations enabled?
295 | if (this.config.animations.enabled && this.config.animations.hide.speed > 0) {
296 | // Is animation overlap enabled?
297 | if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {
298 | stepPromises.push(notification.component.hide());
299 | setTimeout(() => {
300 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));
301 | }, this.config.animations.hide.speed - this.config.animations.overlap);
302 | } else {
303 | notification.component.hide().then(() => {
304 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));
305 | });
306 | }
307 | } else {
308 | stepPromises.push(notification.component.hide());
309 | stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));
310 | }
311 | } else {
312 | stepPromises.push(notification.component.hide());
313 | }
314 |
315 | // Wait until both hiding and shifting is done, then remove the notification from the list
316 | Promise.all(stepPromises).then(() => {
317 | this.removeNotificationFromList(notification);
318 | resolve(); // Done
319 | });
320 | });
321 | }
322 |
323 | /**
324 | * Hide the oldest notification (bridge to handleHideAction)
325 | *
326 | * @param action Action object
327 | * @returns Promise, resolved when done
328 | */
329 | private handleHideOldestAction(action: NotifierAction): Promise {
330 | // Are there any notifications? (prevent accidential errors)
331 | if (this.notifications.length === 0) {
332 | return new Promise((resolve: () => void) => {
333 | resolve();
334 | }); // Done
335 | } else {
336 | action.payload = this.notifications[0].id;
337 | return this.handleHideAction(action);
338 | }
339 | }
340 |
341 | /**
342 | * Hide the newest notification (bridge to handleHideAction)
343 | *
344 | * @param action Action object
345 | * @returns Promise, resolved when done
346 | */
347 | private handleHideNewestAction(action: NotifierAction): Promise {
348 | // Are there any notifications? (prevent accidential errors)
349 | if (this.notifications.length === 0) {
350 | return new Promise((resolve: () => void) => {
351 | resolve();
352 | }); // Done
353 | } else {
354 | action.payload = this.notifications[this.notifications.length - 1].id;
355 | return this.handleHideAction(action);
356 | }
357 | }
358 |
359 | /**
360 | * Hide all notifications at once
361 | *
362 | * @returns Promise, resolved when done
363 | */
364 | private handleHideAllAction(): Promise {
365 | return new Promise((resolve: () => void) => {
366 | // Are there any notifications? (prevent accidential errors)
367 | const numberOfNotifications: number = this.notifications.length;
368 | if (numberOfNotifications === 0) {
369 | resolve(); // Done
370 | return;
371 | }
372 |
373 | // Are animations enabled?
374 | if (
375 | this.config.animations.enabled &&
376 | this.config.animations.hide.speed > 0 &&
377 | this.config.animations.hide.offset !== false &&
378 | this.config.animations.hide.offset > 0
379 | ) {
380 | for (let i: number = numberOfNotifications - 1; i >= 0; i--) {
381 | const animationOffset: number = this.config.position.vertical.position === 'top' ? numberOfNotifications - 1 : i;
382 | setTimeout(() => {
383 | this.notifications[i].component.hide().then(() => {
384 | // Are we done here, was this the last notification to be hidden?
385 | if (
386 | (this.config.position.vertical.position === 'top' && i === 0) ||
387 | (this.config.position.vertical.position === 'bottom' && i === numberOfNotifications - 1)
388 | ) {
389 | this.removeAllNotificationsFromList();
390 | resolve(); // Done
391 | }
392 | });
393 | }, this.config.animations.hide.offset * animationOffset);
394 | }
395 | } else {
396 | const stepPromises: Array> = [];
397 | for (let i: number = numberOfNotifications - 1; i >= 0; i--) {
398 | stepPromises.push(this.notifications[i].component.hide());
399 | }
400 | Promise.all(stepPromises).then(() => {
401 | this.removeAllNotificationsFromList();
402 | resolve(); // Done
403 | });
404 | }
405 | });
406 | }
407 |
408 | /**
409 | * Shift multiple notifications at once
410 | *
411 | * @param notifications List containing the notifications to be shifted
412 | * @param distance Distance to shift (in px)
413 | * @param toMakePlace Flag, defining in which direciton to shift
414 | * @returns Promise, resolved when done
415 | */
416 | private shiftNotifications(notifications: Array, distance: number, toMakePlace: boolean): Promise {
417 | return new Promise((resolve: () => void) => {
418 | // Are there any notifications to shift?
419 | if (notifications.length === 0) {
420 | resolve();
421 | return;
422 | }
423 |
424 | const notificationPromises: Array> = [];
425 | for (let i: number = notifications.length - 1; i >= 0; i--) {
426 | notificationPromises.push(notifications[i].component.shift(distance, toMakePlace));
427 | }
428 | Promise.all(notificationPromises).then(resolve); // Done
429 | });
430 | }
431 |
432 | /**
433 | * Add a new notification to the list of notifications (triggers change detection)
434 | *
435 | * @param notification Notification to add to the list of notifications
436 | */
437 | private addNotificationToList(notification: NotifierNotification): void {
438 | this.notifications.push(notification);
439 | this.changeDetector.markForCheck(); // Run change detection because the notification list changed
440 | }
441 |
442 | /**
443 | * Remove an existing notification from the list of notifications (triggers change detection)
444 | *
445 | * @param notification Notification to be removed from the list of notifications
446 | */
447 | private removeNotificationFromList(notification: NotifierNotification): void {
448 | this.notifications = this.notifications.filter((item: NotifierNotification) => item.component !== notification.component);
449 | this.changeDetector.markForCheck(); // Run change detection because the notification list changed
450 | }
451 |
452 | /**
453 | * Remove all notifications from the list (triggers change detection)
454 | */
455 | private removeAllNotificationsFromList(): void {
456 | this.notifications = [];
457 | this.changeDetector.markForCheck(); // Run change detection because the notification list changed
458 | }
459 |
460 | /**
461 | * Helper: Find a notification in the notification list by a given notification ID
462 | *
463 | * @param notificationId Notification ID, used for finding notification
464 | * @returns Notification, undefined if not found
465 | */
466 | private findNotificationById(notificationId: string): NotifierNotification | undefined {
467 | return this.notifications.find((currentNotification: NotifierNotification) => currentNotification.id === notificationId);
468 | }
469 |
470 | /**
471 | * Helper: Find a notification's index by a given notification ID
472 | *
473 | * @param notificationId Notification ID, used for finding a notification's index
474 | * @returns Notification index, undefined if not found
475 | */
476 | private findNotificationIndexById(notificationId: string): number | undefined {
477 | const notificationIndex: number = this.notifications.findIndex(
478 | (currentNotification: NotifierNotification) => currentNotification.id === notificationId,
479 | );
480 | return notificationIndex !== -1 ? notificationIndex : undefined;
481 | }
482 | }
483 |
--------------------------------------------------------------------------------
/projects/angular-notifier/src/lib/components/notifier-notification.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, DebugElement, Injectable, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core';
2 | import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 |
5 | import { NotifierAnimationData } from '../models/notifier-animation.model';
6 | import { NotifierConfig } from '../models/notifier-config.model';
7 | import { NotifierNotification } from '../models/notifier-notification.model';
8 | import { NotifierConfigToken } from '../notifier.tokens';
9 | import { NotifierService } from '../services/notifier.service';
10 | import { NotifierAnimationService } from '../services/notifier-animation.service';
11 | import { NotifierTimerService } from '../services/notifier-timer.service';
12 | import { NotifierNotificationComponent } from './notifier-notification.component';
13 |
14 | /**
15 | * Notifier Notification Component - Unit Test
16 | */
17 | describe('Notifier Notification Component', () => {
18 | const fakeAnimation: any = {
19 | onfinish: () => null, // We only need this property to be actually mocked away
20 | };
21 |
22 | const testNotification: NotifierNotification = new NotifierNotification({
23 | id: 'ID_FAKE',
24 | message: 'Lorem ipsum dolor sit amet.',
25 | type: 'SUCCESS',
26 | });
27 |
28 | let componentFixture: ComponentFixture;
29 | let componentInstance: NotifierNotificationComponent;
30 |
31 | let timerService: MockNotifierTimerService;
32 |
33 | it('should instantiate', () => {
34 | // Setup test module
35 | beforeEachWithConfig(new NotifierConfig());
36 |
37 | expect(componentInstance).toBeDefined();
38 | });
39 |
40 | describe('(render)', () => {
41 | it('should render', () => {
42 | // Setup test module
43 | beforeEachWithConfig(new NotifierConfig());
44 |
45 | componentInstance.notification = testNotification;
46 | componentFixture.detectChanges();
47 |
48 | // Check the calculated values
49 | expect(componentInstance.getConfig()).toEqual(new NotifierConfig());
50 | expect(componentInstance.getHeight()).toBe(componentFixture.nativeElement.offsetHeight);
51 | expect(componentInstance.getWidth()).toBe(componentFixture.nativeElement.offsetWidth);
52 | expect(componentInstance.getShift()).toBe(0);
53 |
54 | // Check the template
55 | const messageElement: DebugElement = componentFixture.debugElement.query(By.css('.notifier__notification-message'));
56 | expect(messageElement.nativeElement.textContent).toContain(componentInstance.notification.message);
57 | const dismissButtonElement: DebugElement = componentFixture.debugElement.query(By.css('.notifier__notification-button'));
58 | expect(dismissButtonElement).not.toBeNull();
59 |
60 | // Check the class names
61 | const classNameType = `notifier__notification--${componentInstance.notification.type}`;
62 | expect(componentFixture.nativeElement.classList.contains(classNameType)).toBeTruthy();
63 | const classNameTheme = `notifier__notification--${componentInstance.getConfig().theme}`;
64 | expect(componentFixture.nativeElement.classList.contains(classNameTheme)).toBeTruthy();
65 | });
66 |
67 | it('should render the custom template if provided by the user', async(() => {
68 | // Setup test module
69 | const testNotifierConfig: NotifierConfig = new NotifierConfig({
70 | position: {
71 | horizontal: {
72 | distance: 10,
73 | position: 'left',
74 | },
75 | vertical: {
76 | distance: 10,
77 | gap: 4,
78 | position: 'top',
79 | },
80 | },
81 | });
82 | beforeEachWithConfig(testNotifierConfig, false);
83 |
84 | const template = `