Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A directive replacing loading content with spinner in Angular 2+

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

}
like image 305
ansorensen Avatar asked Jun 26 '17 11:06

ansorensen


1 Answers

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.

like image 118
Marty A Avatar answered Nov 09 '22 03:11

Marty A