Working on the frontend, most of the time you want to display images loaded from a CDN. The request takes some time and while it is not yet completed a nasty placeholder is displayed until the image is loaded (or when it fails).
Making a Custom Placeholder
The following solution is done in Angular ('cause not all of us hate this framework) using a directive approach that can be attached to an img
directive and will result to a pulsing object until the image is not loaded, or will place a placeholder when the loading fails.
Here is the working result:
Implementing The Directive
The whole working example can be found in the following StackBlitz Example however here is the implementation:
import { isPlatformServer } from '@angular/common';
import {
Directive,
ElementRef,
Inject,
Input,
OnChanges,
PLATFORM_ID,
Renderer2,
SimpleChanges,
} from '@angular/core';
import { defaultImageLocation } from './image-locations.model';
type ImageSrc = string | null | undefined;
@Directive({
selector: '[appDefaultImg]',
standalone: true,
})
export class DefaultImgDirective implements OnChanges {
@Input({ required: true }) src: ImageSrc = null;
// url link to some default image
private defaultLocalImage = defaultImageLocation;
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
private imageRef: ElementRef,
private renderer: Renderer2
) {}
ngOnChanges(changes: SimpleChanges): void {
this.initImage();
}
private initImage() {
// do not evaluate on SSR
if (isPlatformServer(this.platformId)) {
return;
}
// show skeleton before image is loaded
this.renderer.addClass(this.imageRef.nativeElement, 'g-skeleton');
const img = new Image();
// return on no src
if (!this.src) {
return;
}
// if possible to load image, set it to img
img.onload = () => {
this.setImage(this.resolveImage(this.src));
this.renderer.removeClass(this.imageRef.nativeElement, 'g-skeleton');
};
img.onerror = () => {
// Set a placeholder image
this.setImage(this.defaultLocalImage);
this.renderer.removeClass(this.imageRef.nativeElement, 'g-skeleton');
};
// triggers http request to load image
img.src = this.resolveImage(this.src);
}
private setImage(src: ImageSrc) {
this.imageRef.nativeElement.setAttribute('src', src);
}
private resolveImage(src: ImageSrc): string {
if (!src) {
return this.defaultLocalImage;
}
return src;
}
}
Not going too much in-depth how the directive works, the main parts are the following:
1.) Every time the src
input changes, we will try to resolve the resource (url or image location)
2.) When using SSR, the initImage
method will not try to resolve the image
3.) Attaching the g-skeleton
css class we create some grey pulsing background, indicating to the user that the image is loading
4.) When the img.src
is resolved we display the image or a placeholder when it fails
Directive Usage
The directive can be attach on img
HTML tags and used in the following way:
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, DefaultImgDirective],
template: `
<h1>Testing different image</h1>
<h2>Working Images Directive vs Normal</h2>
<div class="image-wrapper">
<img appDefaultImg class="g-image-height" [src]="emoji1$ | async" />
<img class="g-image-height" [src]="emoji2$ | async" />
</div>
<h2>Not Working Image Directive vs Normal <h2>
<div class="image-wrapper">
<img appDefaultImg class="g-image-height" [src]="emojiNotWorkiing$ | async" />
<img class="g-image-height" [src]="emojiNotWorkiing$ | async" />
</div>
`,
})
export class App {
emoji1$ = of(emoji1).pipe(delay(3000));
emoji2$ = of(emoji1).pipe(delay(6000));
emojiNotWorkiing$ = of('https://...').pipe(delay(3000));
}
Just to have all the code, here is the used CSS
/* Add application styles & imports to this file! */
.g-skeleton {
border-radius: 10px;
background-color: rgb(224, 224, 224);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
transform: scale(1);
animation: pulse 2s infinite;
}
.g-image-height {
height: 200px;
width: 200px;
}
@keyframes pulse {
0% {
transform: scale(0.95);
border-radius: 2px;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
}
70% {
transform: scale(1);
border-radius: 10px;
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
100% {
transform: scale(0.95);
border-radius: 2px;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
}
}
.image-wrapper {
display: flex;
gap: 16px;
}
Summary
This post was a short tutorial on how I personally like to solve image loading in Angular. Hope this method will help you. If you have any questions, feel free to ask them below, share your solution or connect with me at:
Top comments (0)