Verified Commit 3fb03cff authored by Raphael Ochsenbein's avatar Raphael Ochsenbein
Browse files

Simple Toast Service

parent d0138816
Pipeline #635 failed with stages
in 55 seconds
// from: https://auth0.com/docs/multifactor-authentication/step-up-authentication/step-up-for-apis
// set dependencies
const express = require('express');
const app = express();
......
......@@ -4,8 +4,8 @@
"scripts": {
"ng": "ng",
"start": "ng serve --aot",
"build": "ng build",
"test": "ng test",
"build": "ng build --prod --progress=false",
"test": "ng test --source-map=false --no-watch",
"lint": "ng lint && npm run lint:sass",
"api": "node api/server.js",
"lint:sass": "sass-lint -c .sass-lint.yml -v -q",
......
......@@ -4,14 +4,12 @@
</h1>
</div>
<h2>Log In / Log Out</h2>
<!--<button [ibmButton]="'primary'" [size]="'normal'" (click)="showToast()">Toast</button><span> </span>-->
<button [ibmButton]="'primary'" [size]="'normal'" (click)="loginPopup()">Log In</button>
<button [ibmButton]="'danger--primary'" [size]="'normal'" (click)="logoutPopup()">Log Out</button>
<div class="notification-container"></div>
<ibm-toast [notificationObj]="{
type: 'error',
title: 'Sample toast',
subtitle: 'Sample subtitle message',
caption: 'Sample caption'
}"></ibm-toast>
<hr/>
<button [ibmButton]="'primary'" [size]="'normal'" (click)="loginPopup()">Call 1FA API</button>
<button [ibmButton]="'danger--primary'" [size]="'normal'" (click)="loginPopup()">Call 2FA API</button>
<hr/>
<button [ibmButton]="'primary'" [size]="'normal'" (click)="loginPopup()">Check 2FA</button>
<button [ibmButton]="'danger--primary'" [size]="'normal'" (click)="loginPopup()">Login Request 2FA</button>
......@@ -3,6 +3,7 @@ h2 {
color: whitesmoke;
}
.notification-container {
button {
margin-right: 4px;
}
import { Component } from '@angular/core';
import { OidcFacade} from 'ng-oidc-client';
import { NotificationService } from 'carbon-components-angular';
import {ToastService} from './toast';
@Component({
selector: 'app-root',
......@@ -10,26 +10,35 @@ import { NotificationService } from 'carbon-components-angular';
export class AppComponent {
title = 'openid-connect-playground';
constructor(private oidcFacade: OidcFacade, private notifications: NotificationService) {}
constructor(
private oidcFacade: OidcFacade,
private toast: ToastService,
) {}
loginPopup() {
this.notifications.showNotification({
this.toast.show({
type: 'info',
title: 'Log In',
message: 'Logging in.',
target: '.notification-container',
caption: 'Logging in.',
});
this.oidcFacade.signinPopup();
}
logoutPopup() {
this.notifications.showNotification({
this.toast.show({
type: 'warn',
title: 'Log Out',
message: 'Logging out.',
target: '.notification-container',
caption: 'Logging out.',
});
this.oidcFacade.signoutPopup();
}
showToast() {
this.toast.show({
type: 'info',
title: 'Test',
caption: 'Testing',
});
}
}
......@@ -13,6 +13,9 @@ import {OidcInterceptorService} from './oidc-interceptor.service';
import {OidcEffectsService} from './oidc-effects.service';
import {metaReducers} from './logout.metareducer';
import {ButtonModule, NotificationModule, NotificationService} from 'carbon-components-angular';
import {ToastModule} from './toast';
import {FormsModule} from '@angular/forms';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
// Setup done according to https://www.npmjs.com/package/ng-oidc-client
......@@ -33,6 +36,10 @@ export const rootStore: ActionReducerMap<State> = {
],
imports: [
BrowserModule,
FormsModule,
BrowserAnimationsModule,
ButtonModule,
ToastModule.forRoot(),
StoreModule.forRoot(rootStore, { metaReducers}),
EffectsModule.forRoot([OidcEffectsService]),
NgOidcClientModule.forRoot({
......@@ -53,8 +60,6 @@ export const rootStore: ActionReducerMap<State> = {
// level: 0,
// },
}),
ButtonModule,
NotificationModule,
],
providers: [
OidcGuardService,
......@@ -64,7 +69,6 @@ export const rootStore: ActionReducerMap<State> = {
multi: true
},
OidcEffectsService,
NotificationService,
],
bootstrap: [AppComponent]
})
......
......@@ -3,25 +3,36 @@ import {OidcActions} from 'ng-oidc-client';
import {Actions, Effect, ofType} from '@ngrx/effects';
import {Action} from '@ngrx/store';
import {Observable} from 'rxjs';
import {NotificationService} from 'carbon-components-angular';
import {ToastService} from './toast';
export class OidcEffectsService {
constructor(
private actions$: Actions,
private notifications: NotificationService,
private toast: ToastService,
) {}
@Effect({ dispatch: false })
onUserSignedOut$: Observable<Action> = this.actions$.pipe(
ofType(OidcActions.OidcActionTypes.OnUserSignedOut),
tap(args => {
this.notifications.showNotification({
this.toast.show({
type: 'warn',
title: 'Log Out',
message: 'You have been logged out',
target: '.notification-container',
caption: 'You have been logged out',
});
// this.router.navigate(['/home']);
})
);
@Effect({ dispatch: false })
onUserLoaded$: Observable<Action> = this.actions$.pipe(
ofType(OidcActions.OidcActionTypes.OnUserLoaded),
tap(args => {
this.toast.show({
type: 'info',
title: 'Log In',
caption: 'You have been logged in',
});
})
);
}
export * from './toast.module';
export * from './toast.service';
// https://blog.angularindepth.com/creating-a-toast-service-with-angular-cdk-a0d35fd8cc12
// https://stackblitz.com/edit/angular-toast-service?file=src%2Fapp%2Ftoast%2Ftoast.component.ts
import {
AnimationTriggerMetadata,
trigger,
state,
transition,
style,
animate,
} from '@angular/animations';
export const toastAnimations: {
readonly fadeToast: AnimationTriggerMetadata;
} = {
fadeToast: trigger('fadeAnimation', [
state('default', style({ opacity: 1 })),
transition('void => *', [style({ opacity: 0 }), animate('{{ fadeIn }}ms')]),
transition(
'default => closing',
animate('{{ fadeOut }}ms', style({ opacity: 0 })),
),
]),
};
export type ToastAnimationState = 'default' | 'closing';
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { AnimationEvent } from '@angular/animations';
import {ToastData, _TOAST_CONFIG_TOKEN, MyToastConfig, defaultToastConfig} from './toast.config';
import { ToastRef } from './toast.ref';
import { toastAnimations, ToastAnimationState } from './toast.animation';
@Component({
selector: 'app-toast',
template: `
<ibm-toast [notificationObj]="data"
[@fadeAnimation]="{value: animationState, params:
{ fadeIn: toastConfig.animation.fadeIn, fadeOut: toastConfig.animation.fadeOut }}"
(@fadeAnimation.done)="onFadeFinished($event)">
</ibm-toast>
`,
animations: [toastAnimations.fadeToast],
})
export class ToastComponent implements OnInit, OnDestroy {
animationState: ToastAnimationState = 'default';
private intervalId: number;
constructor(
readonly data: ToastData,
readonly ref: ToastRef,
@Inject(_TOAST_CONFIG_TOKEN) public toastConfig: MyToastConfig
) {
// this.iconType = data.type === 'success' ? 'done' : data.type;
// somehow empty injection of the config... maybe issue with carbon?
this.toastConfig = {...defaultToastConfig, ...this.toastConfig };
}
ngOnInit() {
this.intervalId = setTimeout(() => this.animationState = 'closing', 5000);
}
ngOnDestroy() {
clearTimeout(this.intervalId);
}
close() {
this.ref.close();
}
onFadeFinished(event: AnimationEvent) {
const { toState } = event;
const isFadeOut = (toState as ToastAnimationState) === 'closing';
const itFinished = this.animationState === 'closing';
if (isFadeOut && itFinished) {
this.close();
}
}
}
import { InjectionToken, TemplateRef } from '@angular/core';
export class ToastData {
type: string;
title: string;
caption: string;
subtitle?: string;
template?: TemplateRef<any>;
templateContext?: {};
}
// export class ToastData {
// type: ToastType;
// text?: string;
// template?: TemplateRef<any>;
// templateContext?: {};
// }
export type ToastType = 'warning' | 'info' | 'success';
export interface MyToastConfig {
position?: {
top: number;
right: number;
};
animation?: {
fadeOut: number;
fadeIn: number;
};
}
export const defaultToastConfig: MyToastConfig = {
position: {
top: 20,
right: 20,
},
animation: {
fadeOut: 2500,
fadeIn: 300,
},
};
export const _TOAST_CONFIG_TOKEN = new InjectionToken('_toast-config');
import {ModuleWithProviders, NgModule} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
import { NotificationModule } from 'carbon-components-angular';
import { ToastComponent } from './toast.component';
import { defaultToastConfig, _TOAST_CONFIG_TOKEN } from './toast.config';
@NgModule({
imports: [OverlayModule, NotificationModule],
declarations: [ToastComponent],
entryComponents: [ToastComponent]
})
export class ToastModule {
public static forRoot(config = defaultToastConfig): ModuleWithProviders {
return {
ngModule: ToastModule,
providers: [
{
provide: _TOAST_CONFIG_TOKEN,
useValue: { ...defaultToastConfig, ...config },
},
],
};
}
}
import { OverlayRef } from '@angular/cdk/overlay';
export class ToastRef {
constructor(private readonly overlay: OverlayRef) { }
close() {
this.overlay.dispose();
}
isVisible() {
return this.overlay && this.overlay.overlayElement;
}
getPosition() {
return this.overlay.overlayElement.getBoundingClientRect();
}
}
import { Injectable, Injector, Inject } from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ToastComponent } from './toast.component';
import {ToastData, _TOAST_CONFIG_TOKEN, MyToastConfig, defaultToastConfig} from './toast.config';
import { ToastRef } from './toast.ref';
@Injectable({
providedIn: 'root'
})
export class ToastService {
private lastToast: ToastRef;
constructor(
private overlay: Overlay,
private parentInjector: Injector,
@Inject(_TOAST_CONFIG_TOKEN) private toastConfig: MyToastConfig,
) {
// console.log(parentInjector, this.toastConfig);
// somehow empty injection of the config... maybe issue with carbon?
this.toastConfig = {...defaultToastConfig, ...this.toastConfig };
}
show(data: ToastData) {
const positionStrategy = this.getPositionStrategy();
const overlayRef = this.overlay.create({ positionStrategy });
const toastRef = new ToastRef(overlayRef);
this.lastToast = toastRef;
const injector = this.getInjector(data, toastRef, this.parentInjector);
const toastPortal = new ComponentPortal(ToastComponent, null, injector);
overlayRef.attach(toastPortal);
return toastRef;
}
getPositionStrategy() {
return this.overlay.position()
.global()
.top(this.getPosition())
.right(this.toastConfig.position.right + 'px');
}
getPosition() {
const lastToastIsVisible = this.lastToast && this.lastToast.isVisible();
const position = lastToastIsVisible
? this.lastToast.getPosition().bottom
: this.toastConfig.position.top;
return position + 'px';
}
getInjector(data: ToastData, toastRef: ToastRef, parentInjector: Injector) {
const tokens = new WeakMap();
tokens.set(ToastData, data);
tokens.set(ToastRef, toastRef);
return new PortalInjector(parentInjector, tokens);
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment