Open Closed

Angular abpLoading directive #10722


User avatar
0
erick@imperionsolutions.com created
  • 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.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

1 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Yes, this is a regression in LoadingDirective introduced in 10.2.0 when it was switched from ComponentFactoryResolver.resolveComponentFactory(...).create() (which only creates the component) to ViewContainerRef.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 creates LoadingComponent on its first effect run, so the auto-inserted spinner sits in the DOM forever. As soon as you toggle once, the existing cleanup paths capture rootNode and 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 importing LoadingDirective from @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: `&lt;div [abpLoading]=&quot;loading()&quot;&gt;...&lt;/div&gt;`
    })
    export class MyPageComponent { ... }
    

    Two things to note:

    • Don't import both LoadingDirective (from @abp/ng.theme.shared) and FixedLoadingDirective in 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 FixedLoadingDirective and go back to importing LoadingDirective from @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)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 16, 2026, 14:50
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.