Browse Source

feat(theme-shared): add custom component features to error.component

add escape listener
refactor destroy method
#2097
pull/2119/head
mehmet-erim 7 years ago
parent
commit
257cc8f459
  1. 11
      npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.html
  2. 60
      npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts
  3. 221
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

11
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.html

@ -1,13 +1,12 @@
<div id="abp-error" class="error"> <div #container id="abp-error" class="error">
<button id="abp-close-button" class="mr-3" type="button" class="close" (click)="destroy()"> <button id="abp-close-button" type="button" class="close mr-3" (click)="destroy()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<div class="row centered">
<div *ngIf="!customComponent" class="row centered">
<div class="col-md-12"> <div class="col-md-12">
<div class="error-template"> <div class="error-template">
<h1> <h1>{{ statusText }} {{ title | abpLocalization }}</h1>
{{ title | abpLocalization }}
</h1>
<div class="error-details"> <div class="error-details">
{{ details | abpLocalization }} {{ details | abpLocalization }}
</div> </div>

60
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts

@ -1,23 +1,65 @@
import { Component, Renderer2, ElementRef } from '@angular/core'; import { Config, takeUntilDestroy } from '@abp/ng.core';
import { Config } from '@abp/ng.core'; import {
AfterViewInit,
Component,
ComponentFactoryResolver,
ElementRef,
EmbeddedViewRef,
OnDestroy,
Type,
ViewChild,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators';
@Component({ @Component({
selector: 'abp-error', selector: 'abp-error',
templateUrl: './error.component.html', templateUrl: './error.component.html',
styleUrls: ['error.component.scss'], styleUrls: ['error.component.scss'],
}) })
export class ErrorComponent { export class ErrorComponent implements AfterViewInit, OnDestroy {
title: string | Config.LocalizationWithDefault = 'Oops!'; cfRes: ComponentFactoryResolver;
details: string | Config.LocalizationWithDefault = 'Sorry, an error has occured.'; status = 0;
renderer: Renderer2; title: Config.LocalizationParam = 'Oops!';
elementRef: ElementRef; details: Config.LocalizationParam = 'Sorry, an error has occured.';
host: any; customComponent: Type<any> = null;
destroy$: Subject<void>;
@ViewChild('container', { static: false })
containerRef: ElementRef<HTMLDivElement>;
get statusText(): string {
return this.status ? `[${this.status}]` : '';
}
ngAfterViewInit() {
if (this.customComponent) {
const customComponentRef = this.cfRes.resolveComponentFactory(this.customComponent).create(null);
customComponentRef.instance.errorStatus = this.status;
this.containerRef.nativeElement.appendChild((customComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);
customComponentRef.changeDetectorRef.detectChanges();
}
fromEvent(document, 'keyup')
.pipe(
takeUntilDestroy(this),
debounceTime(150),
filter((key: KeyboardEvent) => key && key.key === 'Escape'),
)
.subscribe(() => {
this.destroy();
});
}
ngOnDestroy() {}
destroy() { destroy() {
this.renderer.removeChild(this.host, this.elementRef.nativeElement); this.destroy$.next();
this.destroy$.complete();
} }
} }

221
npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

@ -1,21 +1,25 @@
import { RestOccurError } from '@abp/ng.core'; import { Config, RestOccurError } from '@abp/ng.core';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { import {
ApplicationRef, ApplicationRef,
ComponentFactoryResolver, ComponentFactoryResolver,
EmbeddedViewRef, EmbeddedViewRef,
Inject,
Injectable, Injectable,
Injector, Injector,
NgZone,
RendererFactory2, RendererFactory2,
Type,
ComponentRef,
} from '@angular/core'; } from '@angular/core';
import { Router } from '@angular/router'; import { Navigate, RouterError, RouterState, RouterDataResolved } from '@ngxs/router-plugin';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { Observable } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import snq from 'snq'; import snq from 'snq';
import { ErrorComponent } from '../components/error/error.component'; import { ErrorComponent } from '../components/error/error.component';
import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common';
import { Toaster } from '../models/toaster'; import { Toaster } from '../models/toaster';
import { ConfirmationService } from '../services/confirmation.service'; import { ConfirmationService } from '../services/confirmation.service';
import { HTTP_ERROR_CONFIG } from '../tokens/error-pages.token';
export const DEFAULT_ERROR_MESSAGES = { export const DEFAULT_ERROR_MESSAGES = {
defaultError: { defaultError: {
@ -36,82 +40,142 @@ export const DEFAULT_ERROR_MESSAGES = {
}, },
defaultError500: { defaultError500: {
title: '500', title: '500',
details: { key: 'AbpAccount::InternalServerErrorMessage', defaultValue: 'Error detail not sent by server.' }, details: 'Error detail not sent by server.',
},
defaultErrorUnknown: {
title: 'Unknown Error',
details: { key: 'AbpAccount::InternalServerErrorMessage', defaultValue: 'Error detail not sent by server.' },
}, },
}; };
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ErrorHandler { export class ErrorHandler {
componentRef: ComponentRef<ErrorComponent>;
constructor( constructor(
private actions: Actions, private actions: Actions,
private router: Router,
private ngZone: NgZone,
private store: Store, private store: Store,
private confirmationService: ConfirmationService, private confirmationService: ConfirmationService,
private appRef: ApplicationRef, private appRef: ApplicationRef,
private cfRes: ComponentFactoryResolver, private cfRes: ComponentFactoryResolver,
private rendererFactory: RendererFactory2, private rendererFactory: RendererFactory2,
private injector: Injector, private injector: Injector,
@Inject(HTTP_ERROR_CONFIG) private httpErrorConfig: HttpErrorConfig,
) { ) {
actions.pipe(ofActionSuccessful(RestOccurError)).subscribe(res => { this.actions.pipe(ofActionSuccessful(RestOccurError, RouterError, RouterDataResolved)).subscribe(res => {
const { payload: err = {} as HttpErrorResponse | any } = res; if (res instanceof RestOccurError) {
const body = snq(() => (err as HttpErrorResponse).error.error, DEFAULT_ERROR_MESSAGES.defaultError.title); const { payload: err = {} as HttpErrorResponse | any } = res;
const body = snq(() => (err as HttpErrorResponse).error.error, DEFAULT_ERROR_MESSAGES.defaultError.title);
if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) { if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) {
const confirmation$ = this.showError(null, null, body); const confirmation$ = this.showError(null, null, body);
if (err.status === 401) { if (err.status === 401) {
confirmation$.subscribe(() => { confirmation$.subscribe(() => {
this.navigateToLogin(); this.navigateToLogin();
});
}
} else {
switch ((err as HttpErrorResponse).status) {
case 401:
this.showError(
DEFAULT_ERROR_MESSAGES.defaultError401.details,
DEFAULT_ERROR_MESSAGES.defaultError401.title,
).subscribe(() => this.navigateToLogin());
break;
case 403:
this.createErrorComponent({
title: DEFAULT_ERROR_MESSAGES.defaultError403.title,
details: DEFAULT_ERROR_MESSAGES.defaultError403.details,
}); });
break; }
case 404: } else {
this.showError( switch ((err as HttpErrorResponse).status) {
DEFAULT_ERROR_MESSAGES.defaultError404.details, case 401:
DEFAULT_ERROR_MESSAGES.defaultError404.title, this.canCreateCustomError(401)
); ? this.show401Page()
break; : this.showError(
case 500: {
this.createErrorComponent({ key: 'AbpAccount::DefaultErrorMessage401',
title: DEFAULT_ERROR_MESSAGES.defaultError500.title, defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title,
details: DEFAULT_ERROR_MESSAGES.defaultError500.details, },
}); {
break; key: 'AbpAccount::DefaultErrorMessage401Detail',
case 0: defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details,
if ((err as HttpErrorResponse).statusText === 'Unknown Error') { },
).subscribe(() => this.navigateToLogin());
break;
case 403:
this.createErrorComponent({
title: {
key: 'AbpAccount::DefaultErrorMessage403',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title,
},
details: {
key: 'AbpAccount::DefaultErrorMessage403Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details,
},
status: 403,
});
break;
case 404:
this.canCreateCustomError(404)
? this.show404Page()
: this.showError(
{
key: 'AbpAccount::DefaultErrorMessage404',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
{
key: 'AbpAccount::DefaultErrorMessage404Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
},
);
break;
case 500:
this.createErrorComponent({ this.createErrorComponent({
title: DEFAULT_ERROR_MESSAGES.defaultErrorUnknown.title, title: {
details: DEFAULT_ERROR_MESSAGES.defaultErrorUnknown.details, key: 'AbpAccount::500Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title,
},
details: {
key: 'AbpAccount::InternalServerErrorMessage',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details,
},
status: 500,
}); });
} break;
break; case 0:
default: if ((err as HttpErrorResponse).statusText === 'Unknown Error') {
this.showError(DEFAULT_ERROR_MESSAGES.defaultError.details, DEFAULT_ERROR_MESSAGES.defaultError.title); this.createErrorComponent({
break; title: {
key: 'AbpAccount::DefaultErrorMessage',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title,
},
});
}
break;
default:
this.showError(DEFAULT_ERROR_MESSAGES.defaultError.details, DEFAULT_ERROR_MESSAGES.defaultError.title);
break;
}
} }
} else if (res instanceof RouterError && snq(() => res.event.error.indexOf('Cannot match') > -1, false)) {
this.show404Page();
} else if (res instanceof RouterDataResolved && this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
} }
}); });
} }
private showError(message?: string, title?: string, body?: any): Observable<Toaster.Status> { private show401Page() {
this.createErrorComponent({
title: {
key: 'AbpAccount::401Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title,
},
status: 401,
});
}
private show404Page() {
this.createErrorComponent({
title: {
key: 'AbpAccount::404Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
status: 404,
});
}
private showError(
message?: Config.LocalizationParam,
title?: Config.LocalizationParam,
body?: any,
): Observable<Toaster.Status> {
if (body) { if (body) {
if (body.details) { if (body.details) {
message = body.details; message = body.details;
@ -123,35 +187,50 @@ export class ErrorHandler {
return this.confirmationService.error(message, title, { return this.confirmationService.error(message, title, {
hideCancelBtn: true, hideCancelBtn: true,
yesCopy: 'OK', yesText: 'AbpAccount::Close',
}); });
} }
private navigateToLogin() { private navigateToLogin() {
this.ngZone.run(() => { console.warn(this.store.selectSnapshot(RouterState.url));
this.router.navigate(['/account/login'], { this.store.dispatch(
state: { redirectUrl: this.router.url }, new Navigate(['/account/login'], null, { state: { redirectUrl: this.store.selectSnapshot(RouterState.url) } }),
}); );
});
} }
createErrorComponent(instance: Partial<ErrorComponent>) { createErrorComponent(instance: Partial<ErrorComponent>) {
const renderer = this.rendererFactory.createRenderer(null, null); const renderer = this.rendererFactory.createRenderer(null, null);
const host = renderer.selectRootElement(document.body, true); const host = renderer.selectRootElement(document.body, true);
const componentRef = this.cfRes.resolveComponentFactory(ErrorComponent).create(this.injector); this.componentRef = this.cfRes.resolveComponentFactory(ErrorComponent).create(this.injector);
for (const key in componentRef.instance) { for (const key in this.componentRef.instance) {
if (componentRef.instance.hasOwnProperty(key)) { if (this.componentRef.instance.hasOwnProperty(key)) {
componentRef.instance[key] = instance[key]; this.componentRef.instance[key] = instance[key];
} }
} }
this.appRef.attachView(componentRef.hostView); if (this.canCreateCustomError(instance.status as ErrorScreenErrorCodes)) {
renderer.appendChild(host, (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]); this.componentRef.instance.cfRes = this.cfRes;
this.componentRef.instance.customComponent = this.httpErrorConfig.errorScreen.component;
}
this.appRef.attachView(this.componentRef.hostView);
renderer.appendChild(host, (this.componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);
const destroy$ = new Subject<void>();
this.componentRef.instance.destroy$ = destroy$;
destroy$.subscribe(() => {
this.componentRef.destroy();
this.componentRef = null;
});
}
componentRef.instance.renderer = renderer; canCreateCustomError(status: ErrorScreenErrorCodes): boolean {
componentRef.instance.elementRef = componentRef.location; return snq(
componentRef.instance.host = host; () =>
this.httpErrorConfig.errorScreen.component &&
this.httpErrorConfig.errorScreen.forWhichErrors.indexOf(status) > -1,
);
} }
} }

Loading…
Cancel
Save