- Template: app
- Template Type: Classic
- Created ABP Studio Version: 3.0.4
- Current ABP Studio Version: 3.0.4
- Tiered: Yes
- Multi-Tenancy: Yes
- UI Framework: angular
- Theme: leptonx
- Theme Style: dim
- Theme Menu Placement: side
- Progressive Web App: Yes
- Run Progressive Web App Support: Yes
- Database Provider: ef
- Database Management System: sqlserver
- Separate Tenant Schema: Yes
- Mobile Framework: react-native
- Public Website: Yes
- Social Login: Yes
- Include Tests: Yes
- Kubernetes Configuration: Yes
- Distributed Event Bus: none
- Use Local References: No
- Optional Modules:
- GDPR
- TextTemplateManagement
- LanguageManagement
- AuditLogging
- OpenIddictAdmin
- Selected Languages: English, Arabic, Chinese (Simplified), Chinese (Traditional), Czech, English (UK), Finnish, French, German (Germany), Hindi , Hungarian, Icelandic, Italian, Portuguese (Brazil), Romanian (Romania), Russian, Slovak, Spanish, Swedish, Turkish
- Default Language: English
- Create Command: abp new XXXX -t app --tiered --ui-framework angular --mobile react-native --database-provider ef --database-management-system sqlserver --theme leptonx --separate-tenant-schema --skip-migration --skip-migrator --public-website --without-cms-kit --dont-run-install-libs --dont-run-bundling -no-file-management
- Exception message and full stack trace:
- Steps to reproduce the issue: Add a [abpLoading]="false" to a <div>. The loading indicator is visible on the page.
Investigation showed that if we dont toggle the loading variable, the loading indicator will remain, once we start toggling the loading variable the indicator will start functioning correctly.
I looked at the source code, and it seems that when the LoadingComponent is created for the first time, it is automatically appended to the DOM during creation.
1 Answer(s)
-
0
Hi,
Yes, this is a regression in
LoadingDirectiveintroduced in 10.2.0 when it was switched fromComponentFactoryResolver.resolveComponentFactory(...).create()(which only creates the component) toViewContainerRef.createComponent()(which also auto-inserts the host element into the DOM right next to the directive anchor). When[abpLoading]="false"is bound from the start and never toggled, the directive still createsLoadingComponenton its first effect run, so the auto-inserted spinner sits in the DOM forever. As soon as you toggle once, the existing cleanup paths capturerootNodeand the directive works correctly again — exactly what you observed.The fix is already on the way in https://github.com/abpframework/abp/pull/25578 (targeting
rel-10.4). Until it ships, you can drop in this replacement directive — same selector[abpLoading], so usage in your templates stays unchanged. Import it in each standalone component that uses[abpLoading](instead of importingLoadingDirectivefrom@abp/ng.theme.shared):import { ComponentRef, Directive, ElementRef, EmbeddedViewRef, Injector, OnDestroy, OnInit, Renderer2, effect, inject, input, ViewContainerRef } from '@angular/core'; import { Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; import { LoadingComponent } from '@abp/ng.theme.shared'; @Directive({ selector: '[abpLoading]', host: { '[style.position]': '"relative"' } }) export class FixedLoadingDirective implements OnInit, OnDestroy { private elRef = inject<ElementRef<HTMLElement>>(ElementRef); private injector = inject(Injector); private renderer = inject(Renderer2); private viewContainerRef = inject(ViewContainerRef); readonly loading = input(false, { alias: 'abpLoading' }); readonly targetElementInput = input<HTMLElement | undefined>(undefined, { alias: 'abpLoadingTargetElement' }); readonly delay = input(0, { alias: 'abpLoadingDelay' }); private targetElement: HTMLElement | undefined; componentRef: ComponentRef<LoadingComponent> | null = null; rootNode: HTMLDivElement | null = null; timerSubscription: Subscription | null = null; constructor() { effect(() => this.handleLoadingChange(this.loading())); } private handleLoadingChange(newValue: boolean) { setTimeout(() => { if (!newValue) { this.clearLoading(); return; } if (this.timerSubscription) { this.timerSubscription.unsubscribe(); } this.timerSubscription = timer(this.delay()) .pipe(take(1)) .subscribe(() => { if (!this.loading()) return; if (!this.componentRef) { this.componentRef = this.viewContainerRef.createComponent(LoadingComponent, { injector: this.injector }); } if (!this.rootNode) { this.rootNode = (this.componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]; this.targetElement?.appendChild(this.rootNode as HTMLDivElement); } this.timerSubscription = null; }); }, 0); } private clearLoading() { if (this.timerSubscription) { this.timerSubscription.unsubscribe(); this.timerSubscription = null; } if (this.rootNode?.parentElement) { this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); this.rootNode = null; } } ngOnInit() { this.targetElement = this.targetElementInput(); if (!this.targetElement) { const { offsetHeight, offsetWidth } = this.elRef.nativeElement; if (!offsetHeight && !offsetWidth && this.elRef.nativeElement.children?.length) { this.targetElement = this.elRef.nativeElement.children[0] as HTMLElement; } else { this.targetElement = this.elRef.nativeElement; } } } ngOnDestroy() { this.clearLoading(); } }Usage in a standalone component:
@Component({ selector: 'my-page', imports: [FixedLoadingDirective], template: `<div [abpLoading]="loading()">...</div>` }) export class MyPageComponent { ... }Two things to note:
- Don't import both
LoadingDirective(from@abp/ng.theme.shared) andFixedLoadingDirectivein the same component — Angular will complain about the duplicate[abpLoading]selector. Pick one per component. - This only affects components where you apply the fix. ABP's own components that use
[abpLoading]internally always toggle the value, so they don't hit this issue — you don't need to patch them.
Once https://github.com/abpframework/abp/pull/25578 is merged and released, you can delete
FixedLoadingDirectiveand go back to importingLoadingDirectivefrom@abp/ng.theme.shared.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Don't import both