diff --git a/src/app/playground-components.ts b/src/app/playground-components.ts index 9a8495cf46..a2037b1ab5 100644 --- a/src/app/playground-components.ts +++ b/src/app/playground-components.ts @@ -1114,6 +1114,12 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [ component: 'ToastrPreventDuplicatesComponent', name: 'Toastr Prevent Duplicates', }, + { + path: 'toastr-prevent-duplicates-behaviour.component', + link: '/toastr/toastr-prevent-duplicates-behaviour.component', + component: 'ToastrPreventDuplicatesBehaviourComponent', + name: 'Toastr Prevent Duplicates Behaviour', + }, { path: 'toastr-showcase.component', link: '/toastr/toastr-showcase.component', diff --git a/src/framework/theme/components/toastr/toastr-config.ts b/src/framework/theme/components/toastr/toastr-config.ts index 0a0c0c10d4..0492e1667b 100644 --- a/src/framework/theme/components/toastr/toastr-config.ts +++ b/src/framework/theme/components/toastr/toastr-config.ts @@ -15,6 +15,8 @@ type IconToClassMap = { export const NB_TOASTR_CONFIG = new InjectionToken('Default toastr options'); +export type NbDuplicateToastBehaviour = 'previous' | 'all'; + /** * The `NbToastrConfig` class describes configuration of the `NbToastrService.show` and global toastr configuration. * */ @@ -36,9 +38,15 @@ export class NbToastrConfig { * */ destroyByClick: boolean = true; /** - * If preventDuplicates is true then the next toast with the same title and message will not be rendered. + * If preventDuplicates is true then the toast with the same title, message and status will not be rendered. + * Find duplicates behaviour determined by `preventDuplicates`. + * The default `previous` duplicate behaviour is used. * */ preventDuplicates: boolean = false; + /** + * Determines the how to threat duplicates. + * */ + duplicatesBehaviour: NbDuplicateToastBehaviour = 'previous'; /** * Determines render icon or not. * */ diff --git a/src/framework/theme/components/toastr/toastr.service.spec.ts b/src/framework/theme/components/toastr/toastr.service.spec.ts index 60fcaa7e5a..f21f1e98c2 100644 --- a/src/framework/theme/components/toastr/toastr.service.spec.ts +++ b/src/framework/theme/components/toastr/toastr.service.spec.ts @@ -1,8 +1,8 @@ -import { NbToastrContainerRegistry, NbToastrService } from './toastr.service'; +import { NbToastContainer, NbToastrContainerRegistry, NbToastrService } from './toastr.service'; import { NbGlobalLogicalPosition, NbGlobalPhysicalPosition } from '../cdk/overlay/position-helper'; import { TestBed } from '@angular/core/testing'; import { ComponentFactoryResolver } from '@angular/core'; -import { NbToastrModule } from '@nebular/theme'; +import { NbToast, NbToastrModule } from '@nebular/theme'; describe('toastr-service', () => { @@ -222,3 +222,62 @@ describe('toastr-container-registry', () => { expect(topEnd).toBe(topRight); }); }); + +describe('toastr-container', () => { + let toastrContainer: NbToastContainer; + let containerRefStub: any; + let positionHelperStub: any; + + beforeEach(() => { + positionHelperStub = { + isTopPosition() { + return true; + }, + }; + + containerRefStub = { + instance: { + toasts: [], + }, + changeDetectorRef: { + detectChanges() {}, + }, + }; + }); + + beforeEach(() => { + toastrContainer = new NbToastContainer( {}, containerRefStub, positionHelperStub); + }); + + it('should prevent duplicates if previous toast is the same', () => { + const toast1 = { + title: 'toast1', + message: 'message', + config: { status: 'dander', preventDuplicates: true, duplicatesBehaviour: 'previous' }, + }; + + const toast2 = Object.assign({title: 'toast2'}, toast1); + + toastrContainer.attach( toast1); + toastrContainer.attach( toast2); + const duplicateToast = toastrContainer.attach( toast2); + + expect(duplicateToast).toBeUndefined(); + }); + + it('should prevent duplicates if existing toast is the same', () => { + const toast1 = { + title: 'toast1', + message: 'message', + config: { status: 'dander', preventDuplicates: true, duplicatesBehaviour: 'all' }, + }; + + const toast2 = Object.assign({title: 'toast2'}, toast1); + + toastrContainer.attach( toast1); + toastrContainer.attach( toast2); + const duplicateToast = toastrContainer.attach( toast1); + + expect(duplicateToast).toBeUndefined(); + }); +}); diff --git a/src/framework/theme/components/toastr/toastr.service.ts b/src/framework/theme/components/toastr/toastr.service.ts index 6a35a3bfb5..7be25d0774 100644 --- a/src/framework/theme/components/toastr/toastr.service.ts +++ b/src/framework/theme/components/toastr/toastr.service.ts @@ -7,10 +7,9 @@ import { ComponentFactoryResolver, ComponentRef, Inject, Injectable } from '@angular/core'; import { NbComponentPortal } from '../cdk/overlay/mapping'; -import { NbOverlayService } from '../cdk/overlay/overlay-service'; +import { NbOverlayService, patch } from '../cdk/overlay/overlay-service'; import { NbPositionBuilderService } from '../cdk/overlay/overlay-position'; import { NbGlobalLogicalPosition, NbGlobalPosition, NbPositionHelper } from '../cdk/overlay/position-helper'; -import { patch } from '../cdk/overlay/overlay-service'; import { NbToastrContainerComponent } from './toastr-container.component'; import { NB_TOASTR_CONFIG, NbToastrConfig } from './toastr-config'; import { NbToast } from './model'; @@ -61,16 +60,34 @@ export class NbToastContainer { } destroy(toast: NbToast) { + if (this.prevToast === toast) { + this.prevToast = null; + } + this.toasts = this.toasts.filter(t => t !== toast); this.updateContainer(); } protected isDuplicate(toast: NbToast): boolean { - return this.prevToast - && this.prevToast.message === toast.message - && this.prevToast.title === toast.title; + return toast.config.duplicatesBehaviour === 'previous' + ? this.isDuplicatePrevious(toast) + : this.isDuplicateAmongAll(toast); } + protected isDuplicatePrevious(toast: NbToast): boolean { + return this.prevToast && this.toastDuplicateCompareFunc(this.prevToast, toast); + } + + protected isDuplicateAmongAll(toast: NbToast): boolean { + return this.toasts.some(t => this.toastDuplicateCompareFunc(t, toast)); + } + + protected toastDuplicateCompareFunc = (t1: NbToast, t2: NbToast): boolean => { + return t1.message === t2.message + && t1.title === t2.title + && t1.config.status === t2.config.status; + }; + protected attachToast(toast: NbToast): NbToastComponent { if (this.positionHelper.isTopPosition(toast.config.position)) { return this.attachToTop(toast); @@ -197,11 +214,17 @@ export class NbToastrContainerRegistry { * * @stacked-example(Destroy by click, toastr/toastr-destroy-by-click.component) * - * `preventDuplicates` - don't create new toast if it has the same title and the same message with previous one. + * `preventDuplicates` - don't create new toast if it has the same title, message and status. * Default is false. * * @stacked-example(Prevent duplicates, toastr/toastr-prevent-duplicates.component) * + * `duplicatesBehaviour` - determines how to threat the toasts duplication. + * Compare with the previous message (`NbDuplicateToastBehaviour.PREVIOUS`) + * or with all visible messages (`NbDuplicateToastBehaviour.ALL`). + * + * @stacked-example(Prevent duplicates behaviour , toastr/toastr-prevent-duplicates-behaviour.component) + * * `hasIcon` - if true then render toast icon. * `icon` - you can pass icon class that will be applied into the toast. * diff --git a/src/playground/with-layout/toastr/toastr-prevent-duplicates-behaviour.component.ts b/src/playground/with-layout/toastr/toastr-prevent-duplicates-behaviour.component.ts new file mode 100644 index 0000000000..9f8e3fedca --- /dev/null +++ b/src/playground/with-layout/toastr/toastr-prevent-duplicates-behaviour.component.ts @@ -0,0 +1,48 @@ +import { Component, HostBinding } from '@angular/core'; +import { NbDuplicateToastBehaviour, NbToastrService } from '@nebular/theme'; + +@Component({ + selector: 'nb-toastr-prevent-duplicates-behaviour', + template: ` + + + + + + {{ option.label }} + + + `, + styles: [ + ` + ::ng-deep nb-layout-column { + height: 80vw; + } + `, + ], +}) + +export class ToastrPreventDuplicatesBehaviourComponent { + + @HostBinding('class') + classes = 'example-items-rows'; + + constructor(private toastrService: NbToastrService) { + } + + options = [ + { value: 'previous' , label: 'Duplicate previous', checked: true }, + { value: 'all' , label: 'Duplicate all' }, + ]; + + option: NbDuplicateToastBehaviour = 'previous'; + + showToast(message, status) { + this.toastrService.show( + message, + `This is toast title`, + { preventDuplicates: true, duplicatesBehaviour: this.option, status }); + } +} diff --git a/src/playground/with-layout/toastr/toastr-routing.module.ts b/src/playground/with-layout/toastr/toastr-routing.module.ts index 264bc1ff2d..54f1d3dbf0 100644 --- a/src/playground/with-layout/toastr/toastr-routing.module.ts +++ b/src/playground/with-layout/toastr/toastr-routing.module.ts @@ -13,6 +13,7 @@ import { ToastrPositionsComponent } from './toastr-positions.component'; import { ToastrPreventDuplicatesComponent } from './toastr-prevent-duplicates.component'; import { ToastrShowcaseComponent } from './toastr-showcase.component'; import { ToastrStatusesComponent } from './toastr-statuses.component'; +import { ToastrPreventDuplicatesBehaviourComponent } from './toastr-prevent-duplicates-behaviour.component'; const routes: Route[] = [ { @@ -35,6 +36,10 @@ const routes: Route[] = [ path: 'toastr-prevent-duplicates.component', component: ToastrPreventDuplicatesComponent, }, + { + path: 'toastr-prevent-duplicates-behaviour.component', + component: ToastrPreventDuplicatesBehaviourComponent, + }, { path: 'toastr-showcase.component', component: ToastrShowcaseComponent, diff --git a/src/playground/with-layout/toastr/toastr.module.ts b/src/playground/with-layout/toastr/toastr.module.ts index a374a7038c..7568df69bd 100644 --- a/src/playground/with-layout/toastr/toastr.module.ts +++ b/src/playground/with-layout/toastr/toastr.module.ts @@ -5,7 +5,7 @@ */ import { NgModule } from '@angular/core'; -import { NbButtonModule, NbToastrModule } from '@nebular/theme'; +import { NbButtonModule, NbRadioModule, NbToastrModule } from '@nebular/theme'; import { ToastrRoutingModule } from './toastr-routing.module'; import { ToastrDestroyByClickComponent } from './toastr-destroy-by-click.component'; import { ToastrDurationComponent } from './toastr-duration.component'; @@ -14,6 +14,9 @@ import { ToastrPositionsComponent } from './toastr-positions.component'; import { ToastrPreventDuplicatesComponent } from './toastr-prevent-duplicates.component'; import { ToastrShowcaseComponent } from './toastr-showcase.component'; import { ToastrStatusesComponent } from './toastr-statuses.component'; +import { ToastrPreventDuplicatesBehaviourComponent } from './toastr-prevent-duplicates-behaviour.component'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; @NgModule({ declarations: [ @@ -22,12 +25,16 @@ import { ToastrStatusesComponent } from './toastr-statuses.component'; ToastrIconComponent, ToastrPositionsComponent, ToastrPreventDuplicatesComponent, + ToastrPreventDuplicatesBehaviourComponent, ToastrShowcaseComponent, ToastrStatusesComponent, ], imports: [ + CommonModule, + FormsModule, NbToastrModule.forRoot(), NbButtonModule, + NbRadioModule, ToastrRoutingModule, ], })