diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md index d2c1fc08f3..4778af020b 100644 --- a/docs/en/UI/Angular/Toaster-Service.md +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -85,7 +85,103 @@ The all open toasts can be removed manually via the `clear` method: ```js this.toaster.clear(); ``` +## Replacing ToasterService with 3rd party toaster libraries +If you want the ABP Framework to utilize 3rd party libraries for the toasters instead of the built-in one, you can provide a service that implements `Toaster.Service` interface, and provide it as follows (ngx-toastr library used in example): + +> You can use *LocalizationService* for toaster messages translations. +```js +// your-custom-toaster.service.ts +import { Injectable } from '@angular/core'; +import { Config, LocalizationService } from '@abp/ng.core'; +import { Toaster } from '@abp/ng.theme.shared'; +import { ToastrService } from 'ngx-toastr'; + +@Injectable() +export class CustomToasterService implements Toaster.Service { + constructor(private toastr: ToastrService, private localizationService: LocalizationService) {} + + error( + message: Config.LocalizationParam, + title?: Config.LocalizationParam, + options?: Partial, + ) { + return this.show(message, title, 'error', options); + } + + clear(): void { + this.toastr.clear(); + } + + info( + message: Config.LocalizationParam, + title: Config.LocalizationParam | undefined, + options: Partial | undefined, + ): Toaster.ToasterId { + return this.show(message, title, 'info', options); + } + + remove(id: number): void { + this.toastr.remove(id); + } + + show( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + severity: Toaster.Severity, + options: Partial, + ): Toaster.ToasterId { + const translatedMessage = this.localizationService.instant(message); + const translatedTitle = this.localizationService.instant(title); + const toasterOptions = { + positionClass: 'toast-bottom-right', + tapToDismiss: options.tapToDismiss, + ...(options.sticky && { + extendedTimeOut: 0, + timeOut: 0, + }), + }; + const activeToast = this.toastr.show( + translatedMessage, + translatedTitle, + toasterOptions, + `toast-${severity}`, + ); + return activeToast.toastId; + } + + success( + message: Config.LocalizationParam, + title: Config.LocalizationParam | undefined, + options: Partial | undefined, + ): Toaster.ToasterId { + return this.show(message, title, 'success', options); + } + + warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam | undefined, + options: Partial | undefined, + ): Toaster.ToasterId { + return this.show(message, title, 'warning', options); + } +} +``` +```js +// app.module.ts + +import { ToasterService } from '@abp/ng.theme.shared'; + +@NgModule({ + providers: [ + // ... + { + provide: ToasterService, + useClass: CustomToasterService, + }, + ] +}) +``` ## API ### success diff --git a/npm/ng-packs/packages/core/src/lib/models/utility.ts b/npm/ng-packs/packages/core/src/lib/models/utility.ts index 228cab0de3..f1cfbdb6da 100644 --- a/npm/ng-packs/packages/core/src/lib/models/utility.ts +++ b/npm/ng-packs/packages/core/src/lib/models/utility.ts @@ -11,3 +11,7 @@ type Serializable = Record< export type InferredInstanceOf = T extends Type ? U : never; export type InferredContextOf = T extends TemplateRef ? U : never; + +export type Strict = Class extends Contract + ? { [K in keyof Class]: K extends keyof Contract ? Contract[K] : never } + : Contract; diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts index 105c756e54..56851c6d27 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts @@ -6,7 +6,6 @@ import { eLayoutType } from '@abp/ng.core'; template: ` - `, }) export class AccountLayoutComponent { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts index 4f1dac52ca..f961ecc321 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts @@ -6,7 +6,6 @@ import { eLayoutType } from '@abp/ng.core'; template: ` - `, }) export class EmptyLayoutComponent { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts index 7addf08162..beb5d4fc6c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts @@ -30,7 +30,7 @@ export namespace Toaster { options: Partial, ) => ToasterId; remove: (id: number) => void; - clear: (key?: string) => void; + clear: (containerKey?: string) => void; info: ( message: Config.LocalizationParam, title?: Config.LocalizationParam, diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts index 83f6655371..84275a0fc8 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts @@ -1,15 +1,15 @@ import { ComponentRef, Injectable } from '@angular/core'; import { Toaster } from '../models'; import { ReplaySubject } from 'rxjs'; -import { Config, ContentProjectionService, PROJECTION_STRATEGY } from '@abp/ng.core'; +import { Config, ContentProjectionService, PROJECTION_STRATEGY, Strict } from '@abp/ng.core'; import snq from 'snq'; import { ToastContainerComponent } from '../components/toast-container/toast-container.component'; @Injectable({ providedIn: 'root', }) -export class ToasterService implements Toaster.Service { - toasts$ = new ReplaySubject(1); +export class ToasterService implements ToasterContract { + private toasts$ = new ReplaySubject(1); private lastId = -1; @@ -37,7 +37,7 @@ export class ToasterService implements Toaster.Service { message: Config.LocalizationParam, title?: Config.LocalizationParam, options?: Partial, - ): number { + ): Toaster.ToasterId { return this.show(message, title, 'info', options); } @@ -51,7 +51,7 @@ export class ToasterService implements Toaster.Service { message: Config.LocalizationParam, title?: Config.LocalizationParam, options?: Partial, - ): number { + ): Toaster.ToasterId { return this.show(message, title, 'success', options); } @@ -65,7 +65,7 @@ export class ToasterService implements Toaster.Service { message: Config.LocalizationParam, title?: Config.LocalizationParam, options?: Partial, - ): number { + ): Toaster.ToasterId { return this.show(message, title, 'warning', options); } @@ -79,7 +79,7 @@ export class ToasterService implements Toaster.Service { message: Config.LocalizationParam, title?: Config.LocalizationParam, options?: Partial, - ): number { + ): Toaster.ToasterId { return this.show(message, title, 'error', options); } @@ -96,7 +96,7 @@ export class ToasterService implements Toaster.Service { title: Config.LocalizationParam = null, severity: Toaster.Severity = 'neutral', options = {} as Partial, - ): number { + ): Toaster.ToasterId { if (!this.containerComponentRef) this.setContainer(); const id = ++this.lastId; @@ -122,10 +122,12 @@ export class ToasterService implements Toaster.Service { /** * Removes all open toasts at once. */ - clear(key?: string): void { - this.toasts = !key + clear(containerKey?: string): void { + this.toasts = !containerKey ? [] - : this.toasts.filter(toast => snq(() => toast.options.containerKey) !== key); + : this.toasts.filter(toast => snq(() => toast.options.containerKey) !== containerKey); this.toasts$.next(this.toasts); } } + +export type ToasterContract = Strict; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts index 0807bf632e..d47e4a7cf5 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts @@ -14,6 +14,7 @@ import { ToasterService } from '../services/toaster.service'; imports: [CoreModule.forTest()], }) export class MockModule {} +const toastClassPrefix = 'abp-toast'; describe('ToasterService', () => { let spectator: SpectatorService; @@ -39,24 +40,23 @@ describe('ToasterService', () => { service['containerComponentRef'].changeDetectorRef.detectChanges(); expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy(); - expect(selectToasterContent('.toast-title')).toBe('TITLE'); - expect(selectToasterContent('.toast-message')).toBe('MESSAGE'); + expect(selectToasterContent(`.${toastClassPrefix}-title`)).toBe('TITLE'); + expect(selectToasterContent(`.${toastClassPrefix}-message`)).toBe('MESSAGE'); }); test.each` - type | selector | icon - ${'info'} | ${'.toast-info'} | ${'.fa-info-circle'} - ${'success'} | ${'.toast-success'} | ${'.fa-check-circle'} - ${'warn'} | ${'.toast-warning'} | ${'.fa-exclamation-triangle'} - ${'error'} | ${'.toast-error'} | ${'.fa-times-circle'} + type | selector | icon + ${'info'} | ${`.${toastClassPrefix}-info`} | ${'.fa-info-circle'} + ${'success'} | ${`.${toastClassPrefix}-success`} | ${'.fa-check-circle'} + ${'warn'} | ${`.${toastClassPrefix}-warning`} | ${'.fa-exclamation-triangle'} + ${'error'} | ${`.${toastClassPrefix}-error`} | ${'.fa-times-circle'} `('should display $type toast', async ({ type, selector, icon }) => { service[type]('MESSAGE', 'TITLE'); await timer(0).toPromise(); service['containerComponentRef'].changeDetectorRef.detectChanges(); - - expect(selectToasterContent('.toast-title')).toBe('TITLE'); - expect(selectToasterContent('.toast-message')).toBe('MESSAGE'); + expect(selectToasterContent(`.${toastClassPrefix}-title`)).toBe('TITLE'); + expect(selectToasterContent(`.${toastClassPrefix}-message`)).toBe('MESSAGE'); expect(selectToasterElement()).toBe(document.querySelector(selector)); expect(selectToasterElement(icon)).toBeTruthy(); }); @@ -68,10 +68,10 @@ describe('ToasterService', () => { await timer(0).toPromise(); service['containerComponentRef'].changeDetectorRef.detectChanges(); - const titles = document.querySelectorAll('.toast-title'); + const titles = document.querySelectorAll(`.${toastClassPrefix}-title`); expect(titles.length).toBe(2); - const messages = document.querySelectorAll('.toast-message'); + const messages = document.querySelectorAll(`.${toastClassPrefix}-message`); expect(messages.length).toBe(2); }); @@ -104,19 +104,19 @@ describe('ToasterService', () => { service['containerComponentRef'].changeDetectorRef.detectChanges(); expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy(); - expect(selectToasterContent('.toast-title')).toBe('TITLE_2'); - expect(selectToasterContent('.toast-message')).toBe('MESSAGE_2'); + expect(selectToasterContent(`.${toastClassPrefix}-title`)).toBe('TITLE_2'); + expect(selectToasterContent(`.${toastClassPrefix}-message`)).toBe('MESSAGE_2'); }); }); -function clearElements(selector = '.toast') { +function clearElements(selector = `.${toastClassPrefix}`) { document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element)); } -function selectToasterContent(selector = '.toast'): string { +function selectToasterContent(selector = `.${toastClassPrefix}`): string { return selectToasterElement(selector).textContent.trim(); } -function selectToasterElement(selector = '.toast'): T { +function selectToasterElement(selector = `.${toastClassPrefix}`): T { return document.querySelector(selector); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts index 2ba44dfa7a..c4647cbcd1 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts @@ -22,6 +22,7 @@ import { LoadingDirective } from './directives/loading.directive'; import { NgxDatatableDefaultDirective } from './directives/ngx-datatable-default.directive'; import { NgxDatatableListDirective } from './directives/ngx-datatable-list.directive'; import { TableSortDirective } from './directives/table-sort.directive'; +import { ErrorHandler } from './handlers/error.handler'; import { initLazyStyleHandler } from './handlers/lazy-style.handler'; import { RootParams } from './models/common'; import { THEME_SHARED_ROUTE_PROVIDERS } from './providers/route.provider'; @@ -68,6 +69,8 @@ const declarationsWithExports = [ ], }) export class ThemeSharedModule { + constructor(private errorHandler: ErrorHandler) {} + static forRoot(options = {} as RootParams): ModuleWithProviders { return { ngModule: ThemeSharedModule,