fix: toaster container removed from empty layout and account layout

- selectors changed in toaster service test
- documentation update for implementing Toaster.Service interface
pull/5666/head
muhammedaltug 5 years ago
parent 5305d57586
commit 8a113064a5

@ -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<Toaster.ToastOptions>,
) {
return this.show(message, title, 'error', options);
}
clear(): void {
this.toastr.clear();
}
info(
message: Config.LocalizationParam,
title: Config.LocalizationParam | undefined,
options: Partial<Toaster.ToastOptions> | 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.ToastOptions>,
): 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<Toaster.ToastOptions> | undefined,
): Toaster.ToasterId {
return this.show(message, title, 'success', options);
}
warn(
message: Config.LocalizationParam,
title: Config.LocalizationParam | undefined,
options: Partial<Toaster.ToastOptions> | 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

@ -11,3 +11,7 @@ type Serializable = Record<
export type InferredInstanceOf<T> = T extends Type<infer U> ? U : never;
export type InferredContextOf<T> = T extends TemplateRef<infer U> ? U : never;
export type Strict<Class, Contract> = Class extends Contract
? { [K in keyof Class]: K extends keyof Contract ? Contract[K] : never }
: Contract;

@ -6,7 +6,6 @@ import { eLayoutType } from '@abp/ng.core';
template: `
<router-outlet></router-outlet>
<abp-confirmation></abp-confirmation>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`,
})
export class AccountLayoutComponent {

@ -6,7 +6,6 @@ import { eLayoutType } from '@abp/ng.core';
template: `
<router-outlet></router-outlet>
<abp-confirmation></abp-confirmation>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`,
})
export class EmptyLayoutComponent {

@ -30,7 +30,7 @@ export namespace Toaster {
options: Partial<Toaster.ToastOptions>,
) => ToasterId;
remove: (id: number) => void;
clear: (key?: string) => void;
clear: (containerKey?: string) => void;
info: (
message: Config.LocalizationParam,
title?: Config.LocalizationParam,

@ -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<Toaster.Toast[]>(1);
export class ToasterService implements ToasterContract {
private toasts$ = new ReplaySubject<Toaster.Toast[]>(1);
private lastId = -1;
@ -37,7 +37,7 @@ export class ToasterService implements Toaster.Service {
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
): 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<Toaster.ToastOptions>,
): 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<Toaster.ToastOptions>,
): 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<Toaster.ToastOptions>,
): 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<Toaster.ToastOptions>,
): 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<ToasterService, Toaster.Service>;

@ -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<ToasterService>;
@ -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<T extends HTMLElement>(selector = '.toast'): T {
function selectToasterElement<T extends HTMLElement>(selector = `.${toastClassPrefix}`): T {
return document.querySelector(selector);
}

@ -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<ThemeSharedModule> {
return {
ngModule: ThemeSharedModule,

Loading…
Cancel
Save