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);
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With