A directive replacing loading content with spinner in Angular 2+
Asked Answered
Q

2

13

In Angular 1, it was fairly easy to create a loading directive that replaced content with a spinner and was used like so:

<div isLoading="$scope.contentIsLoading"></div>

Where contentHasLoaded is a simple boolean value you set in your controller after a data call. The directive itself, was simple, most of the work being done in a template:

<div class="spinner" ng-if="$scope.isLoading"></div>
<div ng-transclude ng-if="!$scope.isLoading"></div>

Is there a "clean" way to do this in Angular 2+? By clean I mean 1) within Angular, not using vanilla JS to directly manipulate the DOM and 2) Can be implemented as a single attribute on an existing element?

I did see this article as fallback:Image Loading Directive. However, it's a little more verbose than I would like: using a regular component requires me to wrap all my async content in a new tag rather than just adding an attribute.

What I'm really looking for is something in a structural directive (which are supposed to be designed for "manipulating the DOM.") However, all the examples I've seen are recreations of something like *ngIf, which hides content but does not insert new content. Specifically, can a structural template 1) have a template, or 2) insert a component or 3) insert something as simple as <div class="spinner"></div>. Here's my best attempt so far:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[loading]',
  inputs: ['loading']
})
export class LoadingDirective {

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
    ) { }

  @Input() set loading (isLoading: boolean) {
    if (isLoading) {
      this.viewContainer.clear();
      // INSERT A COMPONENT, DIV, TEMPLATE, SOMETHING HERE FOR SPINNER
    } else {
      this.viewContainer.clear();
      // If not loading, insert the original content
      this.viewContainer.createEmbeddedView(this.templateRef);
    }
  }

}
Quamash answered 26/6, 2017 at 11:23 Comment(3)
Hi, if you want I have recently worked on a project where I used a directive to create some content and apply some classes... it may help you: github.com/damnko/angular2-django-movies/blob/master/…Bawd
how do you want that directive applied? show some htmlConsequential
Well, I did specify as an attribute, and I did give the example I knew from angular one. If I knew how such a thing was done in Angular 2, I wouldn't have asked the question, but I imagine it would be something like <div [isLoading]="myVar">Normal content goes here</div>Quamash
T
23

This can be done in Angular2+ the way which you have described, you are on the right track. Your structural directive will house the template of the host element and you can inject a component to house the loading image etc.

Directive This directive takes an input parameter to indicate the loading state. Each time this input is set we clear the viewcontainer and either inject the loading component or the host element's template depending on the loading value.

@Directive({
  selector: '[apploading]'
})
export class LoadingDirective {
  loadingFactory : ComponentFactory<LoadingComponent>;
  loadingComponent : ComponentRef<LoadingComponent>;

  @Input() 
  set apploading(loading: boolean) {
    this.vcRef.clear();

    if (loading)
    {
      // create and embed an instance of the loading component
      this.loadingComponent = this.vcRef.createComponent(this.loadingFactory);
    }
    else
    {
      // embed the contents of the host template
      this.vcRef.createEmbeddedView(this.templateRef);
    }    
  }

  constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef, private componentFactoryResolver: ComponentFactoryResolver) {
    // Create resolver for loading component
    this.loadingFactory = this.componentFactoryResolver.resolveComponentFactory(LoadingComponent);
  }
}

Component You can see this does nothing other than hold the template.

@Component({
  selector: 'app-loading',
  template: `<div class="loading">
              <img src="assets/loading.svg" alt="loading">
            </div>`
})
export class LoadingComponent {

  constructor() { }
}

Implementation Usage of the structural directive, bound to boolean

<div *apploading="isLoadingBoolean">
  <h3>My content</h3>
  <p>Blah.</p>
</div>

Note: You also need to include LoadingComponent in the entryComponents array in ngModule.

Trust answered 27/7, 2017 at 5:12 Comment(1)
this approach causes hydration to trip up for me with angular ssr ``` ERROR Error: NG0500: During hydration Angular expected a comment node but found <div>. Angular expected this DOM: <div _ngcontent-ng-c1691056="" class="mb-2"> … <!-- container --> <-- AT THIS LOCATION … </div> Actual DOM is: <div _ngcontent-ng-c16="" class="mb-2"> … <div _ngcontent-ng-c5="" class="loading-animation-containerw-100" style="max-width:355px;margin-right:auto;">…</div> <div _ngcontent-ng-c1693571056="" class="d-flexw-100">…</div> <-- AT THIS LOCATION … </div>Valetudinarian
P
0

I complete the very nice answer from @Marty with my version on Angular 16+ (some functions where deprecated)

@Directive({
  selector: '[appIsLoading]',
  standalone: true
})
export class LoadingDirective {
  private readonly templateRef = inject(TemplateRef<any>);
  private readonly vcRef = inject(ViewContainerRef);

  @Input()
  set appIsLoading(loading: boolean) {
    this.vcRef.clear();

    if (loading) {
      this.vcRef.createComponent(LoaderComponent);
    } else {
      this.vcRef.createEmbeddedView(this.templateRef);
    }
  }
}
Participle answered 2/2 at 17:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.