Browse Source

Merge pull request #5666 from abpframework/fix/5582

fix: add prefix to toaster css classes
pull/5759/head
Levent Arman Özak 5 years ago
committed by GitHub
parent
commit
8e4dd99693
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 96
      docs/en/UI/Angular/Toaster-Service.md
  2. 4
      npm/ng-packs/packages/core/src/lib/models/utility.ts
  3. 1
      npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts
  4. 1
      npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts
  5. 2
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
  6. 2
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.scss
  7. 12
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.html
  8. 22
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.scss
  9. 12
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts
  10. 32
      npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts
  11. 26
      npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts
  12. 34
      npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts
  13. 54
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

96
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<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

4
npm/ng-packs/packages/core/src/lib/models/utility.ts

@ -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;

1
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: `
<router-outlet></router-outlet>
<abp-confirmation></abp-confirmation>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`,
})
export class AccountLayoutComponent {

1
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: `
<router-outlet></router-outlet>
<abp-confirmation></abp-confirmation>
<abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`,
})
export class EmptyLayoutComponent {

2
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html

@ -1,5 +1,5 @@
<div
class="toast-container"
class="abp-toast-container"
[style.top]="top || 'auto'"
[style.right]="right || 'auto'"
[style.bottom]="bottom || 'auto'"

2
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.scss

@ -1,4 +1,4 @@
.toast-container {
.abp-toast-container {
position: fixed;
display: flex;
flex-direction: column;

12
npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.html

@ -1,15 +1,15 @@
<div class="toast" [ngClass]="severityClass" (click)="tap()">
<div class="toast-icon">
<div class="abp-toast" [ngClass]="severityClass" (click)="tap()">
<div class="abp-toast-icon">
<i class="fa icon" [ngClass]="iconClass"></i>
</div>
<div class="toast-content">
<button class="toast-close-button" (click)="close()" *ngIf="toast.options.closable">
<div class="abp-toast-content">
<button class="abp-toast-close-button" (click)="close()" *ngIf="toast.options.closable">
<i class="fa fa-times"></i>
</button>
<div class="toast-title">
<div class="abp-toast-title">
{{ toast.title | abpLocalization: toast.options?.titleLocalizationParams }}
</div>
<p class="toast-message">
<p class="abp-toast-message">
{{ toast.message | abpLocalization: toast.options?.messageLocalizationParams }}
</p>
</div>

22
npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.scss

@ -10,7 +10,9 @@
}
}
.toast {
$toastClass: abp-toast;
.#{$toastClass} {
display: grid;
grid-template-columns: 50px 1fr;
gap: 10px;
@ -23,19 +25,19 @@
z-index: 9999;
@include fillColor(#f0f0f0, #000);
opacity: 1;
&.toast-success {
&.#{$toastClass}-success {
@include fillColor(#51a351, #fff);
}
&.toast-info {
&.#{$toastClass}-info {
@include fillColor(#2f96b4, #fff);
}
&.toast-warning {
&.#{$toastClass}-warning {
@include fillColor(#f89406, #fff);
}
&.toast-error {
&.#{$toastClass}-error {
@include fillColor(#bd362f, #fff);
}
.toast-icon {
.#{$toastClass}-icon {
display: flex;
align-items: center;
justify-content: center;
@ -43,9 +45,9 @@
font-size: 36px;
}
}
.toast-content {
.#{$toastClass}-content {
position: relative;
.toast-close-button {
.#{$toastClass}-close-button {
position: absolute;
top: 0;
right: 0;
@ -64,13 +66,13 @@
outline: none;
}
}
.toast-title {
.#{$toastClass}-title {
margin: 0;
padding: 0;
font-size: 1rem;
font-weight: 600;
}
.toast-message {
.#{$toastClass}-message {
margin: 0;
padding: 0;
max-width: 240px;

12
npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts

@ -1,7 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { Toaster } from '../../models/toaster';
import { ToasterService } from '../../services/toaster.service';
import { LocalizationService } from '@abp/ng.core';
import snq from 'snq';
@Component({
@ -15,7 +14,7 @@ export class ToastComponent implements OnInit {
get severityClass(): string {
if (!this.toast || !this.toast.severity) return '';
return `toast-${this.toast.severity}`;
return `abp-toast-${this.toast.severity}`;
}
get iconClass(): string {
@ -33,10 +32,7 @@ export class ToastComponent implements OnInit {
}
}
constructor(
private toastService: ToasterService,
private localizationService: LocalizationService,
) {}
constructor(private toasterService: ToasterService) {}
ngOnInit() {
if (snq(() => this.toast.options.sticky)) return;
@ -47,10 +43,10 @@ export class ToastComponent implements OnInit {
}
close() {
this.toastService.remove(this.toast.options.id);
this.toasterService.remove(this.toast.options.id);
}
tap() {
if (this.toast.options && this.toast.options.tapToDismiss) this.close();
if (this.toast.options?.tapToDismiss) this.close();
}
}

32
npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts

@ -20,4 +20,36 @@ export namespace Toaster {
}
export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error';
export type ToasterId = string | number;
export interface Service {
show: (
message: Config.LocalizationParam,
title: Config.LocalizationParam,
severity: Toaster.Severity,
options: Partial<Toaster.ToastOptions>,
) => ToasterId;
remove: (id: number) => void;
clear: (containerKey?: string) => void;
info: (
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) => ToasterId;
success: (
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) => ToasterId;
warn: (
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) => ToasterId;
error: (
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) => ToasterId;
}
}

26
npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts

@ -1,15 +1,15 @@
import { Injectable, ComponentRef } from '@angular/core';
import { ComponentRef, Injectable } from '@angular/core';
import { Toaster } from '../models';
import { ReplaySubject } from 'rxjs';
import { Config, PROJECTION_STRATEGY, ContentProjectionService } 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 {
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 {
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 {
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 {
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 {
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 {
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 {
/**
* 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>;

34
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<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);
}

54
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -27,49 +27,37 @@ import { initLazyStyleHandler } from './handlers/lazy-style.handler';
import { RootParams } from './models/common';
import { THEME_SHARED_ROUTE_PROVIDERS } from './providers/route.provider';
import { THEME_SHARED_APPEND_CONTENT } from './tokens/append-content.token';
import { httpErrorConfigFactory, HTTP_ERROR_CONFIG } from './tokens/http-error.token';
import { HTTP_ERROR_CONFIG, httpErrorConfigFactory } from './tokens/http-error.token';
import { DateParserFormatter } from './utils/date-parser-formatter';
const declarationsWithExports = [
BreadcrumbComponent,
ButtonComponent,
ChartComponent,
ConfirmationComponent,
LoaderBarComponent,
LoadingComponent,
ModalComponent,
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
ToastContainerComponent,
SortOrderIconComponent,
NgxDatatableDefaultDirective,
NgxDatatableListDirective,
LoadingDirective,
TableSortDirective,
];
@NgModule({
imports: [CoreModule, NgxDatatableModule, NgxValidateCoreModule, NgbPaginationModule],
declarations: [
BreadcrumbComponent,
ButtonComponent,
ChartComponent,
ConfirmationComponent,
...declarationsWithExports,
HttpErrorWrapperComponent,
LoaderBarComponent,
LoadingComponent,
ModalComponent,
ModalContainerComponent,
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
ToastContainerComponent,
SortOrderIconComponent,
NgxDatatableDefaultDirective,
NgxDatatableListDirective,
LoadingDirective,
TableSortDirective,
],
exports: [
NgxDatatableModule,
BreadcrumbComponent,
ButtonComponent,
ChartComponent,
ConfirmationComponent,
LoaderBarComponent,
LoadingComponent,
ModalComponent,
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
ToastContainerComponent,
SortOrderIconComponent,
NgxDatatableDefaultDirective,
NgxDatatableListDirective,
LoadingDirective,
TableSortDirective,
...declarationsWithExports,
],
providers: [DatePipe],
entryComponents: [

Loading…
Cancel
Save