In the end, I made my own image loader to show a placeholder while the images are loading. I first used the code from @Whisher, but stumbled on some issues. For example, I was using Server Side Rendering (SSR) and thus had some small issues to work out.
Code
In the end, I came up with this:
import { isPlatformBrowser } from '@angular/common';
import {
AfterContentInit,
Directive,
ElementRef,
HostListener,
Inject,
Input,
OnInit,
PLATFORM_ID,
Renderer2,
} from '@angular/core';
@Directive({
selector: 'img[imageLoader]',
})
export class ImageLoaderDirective implements OnInit, AfterContentInit {
@Input()
public src!: string;
@Input()
public loaderSrc: string = '/assets/image-loading.png';
@Input()
public errorSrc: string = '/assets/image-not-found.png';
@Input()
public lazyLoad: boolean = true;
private alreadyTriedLoading: boolean = false;
private alreadyTriedError: boolean = false;
constructor(
private el: ElementRef<HTMLImageElement>,
private renderer: Renderer2,
@Inject(PLATFORM_ID) private platformId: Object
) {}
ngOnInit(): void {
this.renderer.setAttribute(this.el.nativeElement, 'src', this.src);
if (this.lazyLoad) {
this.renderer.setAttribute(this.el.nativeElement, 'loading', 'lazy');
}
}
ngAfterContentInit(): void {
if (this.shouldDisplayLoader()) {
this.renderer.setAttribute(this.el.nativeElement, 'src', this.loaderSrc);
} else {
this.renderer.setAttribute(this.el.nativeElement, 'src', this.src);
}
}
@HostListener('load')
public onLoad(): void {
if (!this.alreadyTriedLoading) {
this.renderer.setAttribute(this.el.nativeElement, 'src', this.src);
}
this.alreadyTriedLoading = true;
}
@HostListener('error')
public onError(): void {
if (!this.alreadyTriedError) {
this.renderer.setAttribute(this.el.nativeElement, 'src', this.errorSrc);
}
this.alreadyTriedError = true;
}
private shouldDisplayLoader(): boolean {
return (
isPlatformBrowser(this.platformId) && !this.el.nativeElement.complete
);
}
}
This should work with most use cases. When the server is trying to use the imageLoader
it will just set image and not do anything with it. When it's the browser trying to use the imageLoader
it will first check the state of the image. It might happen that the image has already been loaded, due to cache, so in that case it won't show the loader. Otherwise, it will display the loader and show the original image after it is done loading.
I also added lazy loading to it, but that was personal preference.
Example
If you were to use all fields, it might look something like this:
<img
[src]="product.image_path"
alt="An image of a product being displayed"
imageLoader
loaderSrc="/assets/loading.gif"
errorSrc="/assets/error.png"
[lazyLoad]="false"
/>
While the directive might work without using the OnInit
and AfterContentInit
, I used these two lifecycle hooks to make them work with property binding. When I tried @Whisher's code, I ran into the issue that the src would be empty. Also, because I use the src
as an input, angular changes it to something like ng-reflect-src
, so this would mean the browser doesn't load the image initially. To fix this, I set the src
again in the ngOnInit
hook.