DEV Community

Eduard Krivanek
Eduard Krivanek

Posted on

Angular: Default Image Placeholder

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).

Image description

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:

Image description

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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:

dev.to | LinkedIn| Personal Website | Github

Top comments (0)