Browse Source

update: theme-shared package for the latest upgrade (state management and html properties)

pull/25690/head
sumeyye 2 months ago
parent
commit
d8328efee4
  1. 2
      npm/ng-packs/packages/theme-shared/project.json
  2. 26
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  3. 33
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  4. 10
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
  5. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.scss
  6. 33
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.ts
  7. 27
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.scss
  8. 14
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts
  9. 25
      npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts
  10. 46
      npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts
  11. 27
      npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts

2
npm/ng-packs/packages/theme-shared/project.json

@ -31,7 +31,7 @@
"executor": "@nx/vitest:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../coverage/packages/theme-shared"
"reportsDirectory": "{projectRoot}/../../coverage/packages/theme-shared"
}
}
}

26
npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

@ -40,23 +40,13 @@ export class ButtonComponent implements OnInit {
readonly buttonType = input('button');
readonly formName = input<string | undefined>(undefined);
readonly iconClass = input<string | undefined>(undefined);
readonly loadingInput = input(false, { alias: 'loading' });
readonly loading = input(false);
readonly disabled = input<boolean | undefined>(false);
readonly attributes = input<ABP.Dictionary<string> | undefined>(undefined);
// Internal writable signal for loading state - can be set programmatically
private readonly _loading = signal(false);
private readonly modalLoading = signal<boolean | null>(null);
// Computed that combines input and internal state
readonly isLoading = computed(() => this.loadingInput() || this._loading());
// Getter/setter for backward compatibility (used by ModalComponent)
get loading(): boolean {
return this._loading();
}
set loading(value: boolean) {
this._loading.set(value);
}
readonly isLoading = computed(() => this.modalLoading() ?? this.loading());
readonly click = output<MouseEvent>();
readonly focus = output<FocusEvent>();
@ -67,9 +57,13 @@ export class ButtonComponent implements OnInit {
readonly buttonRef = viewChild.required<ElementRef<HTMLButtonElement>>('button');
protected readonly icon = computed(() => {
return this.isLoading() ? 'fa fa-spinner fa-spin' : this.iconClass() || 'd-none';
});
protected readonly icon = computed(() =>
this.isLoading() ? 'fa fa-spinner fa-spin' : this.iconClass() || 'd-none',
);
setLoading(value: boolean): void {
this.modalLoading.set(value);
}
ngOnInit() {
const attributes = this.attributes();

33
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts

@ -45,14 +45,7 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
visible = model<boolean>(false);
busy = input(false, {
transform: (value: boolean) => {
if (this.abpSubmit() && this.abpSubmit() instanceof ButtonComponent) {
this.abpSubmit().loading = value;
}
return value;
},
});
busy = input(false);
options = input<NgbModalOptions>({ keyboard: true });
@ -92,6 +85,15 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
effect(() => {
this.toggle(this.visible());
});
effect(() => {
const submit = this.abpSubmit();
if (!(submit instanceof ButtonComponent)) {
return;
}
submit.setLoading(this.visible() && this.busy());
});
}
ngOnInit(): void {
@ -115,11 +117,20 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
this.visible.set(value);
if (!value) {
this.modalRef?.dismiss();
if (this.modalRef) {
const ref = this.modalRef;
this.modalRef = undefined!;
ref.dismiss();
}
this.disappear.emit();
return;
}
if (this.modalWindowRef) {
return;
}
setTimeout(() => this.listen(), 0);
this.modalRef = this.modal.open(this.modalContent(), {
size: 'md',
@ -136,6 +147,10 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
windowClass: `${this.options().windowClass || ''} ${this.modalIdentifier}`,
});
this.modalRef.result.finally(() => {
this.modalRef = undefined!;
});
this.appear.emit();
}

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

@ -4,13 +4,9 @@
[style.right]="right() || 'auto'"
[style.bottom]="bottom() || 'auto'"
[style.left]="left() || 'auto'"
[style.display]="toasts.length ? 'flex' : 'none'"
[@toastInOut]="toasts.length"
[style.display]="toasts().length ? 'flex' : 'none'"
>
@for (toast of toasts; track toast.options?.id) {
<abp-toast
[toast]="toast"
(remove)="remove($event)"
></abp-toast>
@for (toast of toasts(); track toast.options?.id) {
<abp-toast [toast]="toast" (remove)="remove($event)" />
}
</div>

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

@ -1,3 +1,9 @@
:host.abp-toast-host {
position: fixed;
z-index: 1900;
pointer-events: none;
}
.abp-toast-container {
position: fixed;
display: flex;
@ -6,7 +12,7 @@
justify-content: flex-end;
min-width: 350px;
min-height: 80px;
z-index: 1900;
pointer-events: auto;
&.new-on-top {
flex-direction: column-reverse;
}

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

@ -1,6 +1,4 @@
import { Component, OnInit, input, signal, effect } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { toastInOut } from '../../animations/toast.animations';
import { Component, input, OnInit, signal, effect } from '@angular/core';
import { Toaster } from '../../models/toaster';
import { ToastComponent } from '../toast/toast.component';
@ -8,18 +6,16 @@ import { ToastComponent } from '../toast/toast.component';
selector: 'abp-toast-container',
templateUrl: './toast-container.component.html',
styleUrls: ['./toast-container.component.scss'],
animations: [toastInOut],
imports: [ToastComponent],
host: {
'(window:resize)': 'onWindowResize()'
}
class: 'abp-toast-host',
'(window:resize)': 'onWindowResize()',
},
})
export class ToastContainerComponent implements OnInit {
toasts$!: ReplaySubject<Toaster.Toast[]>;
remove!: (toastId: number) => void;
toasts = [] as Toaster.Toast[];
readonly toasts = signal<Toaster.Toast[]>([]);
readonly top = input<string | undefined>(undefined);
readonly rightInput = input('30px', { alias: 'right' });
@ -39,13 +35,13 @@ export class ToastContainerComponent implements OnInit {
ngOnInit() {
this.setDefaultRight();
this.toasts$.subscribe(toasts => {
this.toasts = this.toastKey()
? toasts.filter(t => {
return t.options && t.options.containerKey !== this.toastKey();
})
: toasts;
});
}
setToasts(toasts: Toaster.Toast[]) {
const key = this.toastKey();
this.toasts.set(
key ? toasts.filter(t => t.options && t.options.containerKey !== key) : [...toasts],
);
}
onWindowResize() {
@ -58,9 +54,4 @@ export class ToastContainerComponent implements OnInit {
this.right.set(this.defaultMobileRight);
}
}
trackByFunc(index: number, toast: Toaster.Toast) {
if (!toast) return null;
return toast.options?.id;
}
}

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

@ -14,6 +14,33 @@
$toastClass: abp-toast;
:host {
display: block;
animation: abp-toast-in 350ms ease;
}
:host.abp-toast-leaving {
animation: abp-toast-out 450ms ease forwards;
}
@keyframes abp-toast-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes abp-toast-out {
to {
opacity: 0;
}
}
.#{$toastClass} {
display: grid;
grid-template-columns: 35px 1fr;

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

@ -6,6 +6,9 @@ import { LocalizationPipe } from '@abp/ng.core';
selector: 'abp-toast',
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.scss'],
host: {
'[class.abp-toast-leaving]': 'isLeaving',
},
imports: [LocalizationPipe],
})
export class ToastComponent implements OnInit {
@ -50,8 +53,17 @@ export class ToastComponent implements OnInit {
}, timeout);
}
isLeaving = false;
close() {
this.remove.emit(this.toast().options?.id);
if (this.isLeaving) {
return;
}
this.isLeaving = true;
setTimeout(() => {
this.remove.emit(this.toast().options?.id);
}, 450);
}
tap() {

25
npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts

@ -90,6 +90,31 @@ export default `
animation: fadeOutTop 0.2s ease-in-out;
}
.abp-collapse-y {
display: grid;
grid-template-rows: 1fr;
overflow: hidden;
transition: grid-template-rows 200ms linear;
}
.abp-collapse-y.abp-collapse-y-collapsed {
grid-template-rows: 0fr;
}
.abp-collapse-y-inner {
overflow: hidden;
}
.abp-collapse-margin {
margin-top: 0;
overflow: hidden;
transition: margin-top 400ms linear;
}
.abp-collapse-margin.abp-collapse-margin-collapsed {
margin-top: -100%;
}
.abp-collapsed-height {
-moz-transition: max-height linear 0.35s;
-ms-transition: max-height linear 0.35s;

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

@ -1,11 +1,10 @@
import { ApplicationRef, ComponentRef, inject, Injectable } from '@angular/core';
import {
ContentProjectionService,
LocalizationParam,
PROJECTION_STRATEGY,
Strict,
} from '@abp/ng.core';
import { ComponentRef, inject, Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { ToastContainerComponent } from '../components/toast-container/toast-container.component';
import { Toaster } from '../models';
@ -13,28 +12,33 @@ import { Toaster } from '../models';
providedIn: 'root',
})
export class ToasterService implements ToasterContract {
private toasts$ = new ReplaySubject<Toaster.Toast[]>(1);
private readonly appRef = inject(ApplicationRef);
private readonly contentProjectionService = inject(ContentProjectionService);
private lastId = -1;
private toasts = [] as Toaster.Toast[];
private containerComponentRef!: ComponentRef<ToastContainerComponent>;
private contentProjectionService: ContentProjectionService;
private setContainer() {
this.containerComponentRef = this.contentProjectionService.projectContent(
PROJECTION_STRATEGY.AppendComponentToBody(ToastContainerComponent, {
toasts$: this.toasts$,
remove: this.remove,
}),
);
this.containerComponentRef.changeDetectorRef.detectChanges();
this.syncContainer();
}
constructor() {
this.contentProjectionService = inject(ContentProjectionService);
private syncContainer() {
if (!this.containerComponentRef) {
return;
}
this.containerComponentRef.instance.setToasts(this.toasts);
this.containerComponentRef.changeDetectorRef.detectChanges();
this.appRef.tick();
}
/**
@ -100,23 +104,27 @@ export class ToasterService implements ToasterContract {
* @param severity Sets color of the toast. "success", "warning" etc.
* @param options Spesific style or structural options for individual toast
*/
show(
message: LocalizationParam,
title: LocalizationParam | undefined = undefined,
severity: Toaster.Severity = 'neutral',
options = {} as Partial<Toaster.ToastOptions>,
): Toaster.ToasterId {
if (!this.containerComponentRef) this.setContainer();
if (!this.containerComponentRef) {
this.setContainer();
}
const id = ++this.lastId;
this.toasts.push({
message,
title,
severity,
options: { closable: true, id, ...options },
});
this.toasts$.next(this.toasts);
this.toasts = [
...this.toasts,
{
message,
title,
severity,
options: { closable: true, id, ...options },
},
];
this.syncContainer();
return id;
}
@ -126,7 +134,7 @@ export class ToasterService implements ToasterContract {
*/
remove = (id: number) => {
this.toasts = this.toasts.filter(toast => toast.options?.id !== id);
this.toasts$.next(this.toasts);
this.syncContainer();
};
/**
@ -136,7 +144,7 @@ export class ToasterService implements ToasterContract {
this.toasts = !containerKey
? []
: this.toasts.filter(toast => toast.options?.containerKey !== containerKey);
this.toasts$.next(this.toasts);
this.syncContainer();
}
}

27
npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts

@ -42,13 +42,36 @@ describe('ButtonComponent', () => {
});
it('should display the spinner icon', () => {
spectator.component.loading = true;
spectator.component.setLoading(true);
spectator.detectComponentChanges();
expect(spectator.query('i')).toHaveClass('fa-spinner');
});
it('should display the spinner icon when loading input is true', () => {
spectator = createHost(
'<abp-button iconClass="fa fa-check" [loading]="loading">Button</abp-button>',
{ hostProps: { loading: true } },
);
spectator.detectComponentChanges();
expect(spectator.query('i')).toHaveClass('fa-spinner');
});
it('should clear the spinner icon when loading input becomes false', () => {
spectator = createHost(
'<abp-button iconClass="fa fa-check" [loading]="loading">Button</abp-button>',
{ hostProps: { loading: true } },
);
spectator.detectComponentChanges();
expect(spectator.query('i')).toHaveClass('fa-spinner');
spectator.setHostInput({ loading: false });
spectator.detectComponentChanges();
expect(spectator.query('i')).toHaveClass('fa-check');
expect(spectator.query('i')).not.toHaveClass('fa-spinner');
});
it('should disabled when the loading input is true', () => {
spectator.component.loading = true;
spectator.component.setLoading(true);
spectator.detectComponentChanges();
expect(spectator.query('[disabled]')).toBeTruthy();
});

Loading…
Cancel
Save