4

I am trying to make a simple directive. When the image is loading, the img src will be set to an @Input() string field. On load, the image will be set to the original src value (or at least how I am trying to implement it).

I was using the answer here: https://stackoverflow.com/a/38837619/843443 but is isn't a directive, and thus would require a number of changes wherever I use images.

My first attempt:

loading-img.directive.ts:

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[tohLoadingImg]'
})
export class LoadingImgDirective {
  imgSrc: String;

  @Input()
  spinnerSrc: String;

  constructor(private el: ElementRef) {
    this.imgSrc = el.nativeElement.src;
    el.nativeElement.src = this.spinnerSrc;
  }

  @HostListener('load') onLoad() {
    this.el.nativeElement.src = this.imgSrc;
  }

}

from:

<img src="{{hero.imgUrl}}" alt="Random first slide">

to:

<img src="{{hero.imgUrl}}" alt="Random first slide" [tohLoadingImg]="'/assets/ring.svg'">

Error:

Can't bind to 'tohLoadingImg' since it isn't a known property of 'img'. (".imgUrl}}" alt="Random first slide">-->

What am I missing?

Jago
  • 2,751
  • 4
  • 27
  • 41
Jeff
  • 4,285
  • 15
  • 63
  • 115

3 Answers3

14

Try this

import {
  Directive,
  Attribute,
  Renderer2,
  ElementRef,
  HostListener } from '@angular/core';

@Directive({
  selector: '[uiImageLoader]'
})
export class UiImageLoaderDirective {
  constructor(
    @Attribute('loader') public loader: string,
    @Attribute('onErrorSrc') public onErrorSrc: string,
    private renderer: Renderer2,
    private el: ElementRef) {
      this.renderer.setAttribute(this.el.nativeElement, 'src', this.loader);
    }

  @HostListener('load') onLoad() {
    this.renderer.setAttribute(this.el.nativeElement, 'src', this.el.nativeElement.src);
  }
  @HostListener('error') onError() {
    this.renderer.setAttribute(this.el.nativeElement, 'src', this.onErrorSrc);
  }
}

USAGE

<img
  uiImageLoader
  onErrorSrc="/assets/images/no-available-800.png"
  loader="/assets/images/image-loader.svg"
  [src]="post.imagePath" [alt]="post.title">
Jago
  • 2,751
  • 4
  • 27
  • 41
Whisher
  • 31,320
  • 32
  • 120
  • 201
4

Thanks to @Aravind for the direction. This is how I solved it (by using a component rather than a directive):

spinner-img.component.ts:

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'toh-spinner-img',
  templateUrl: './spinner-img.component.html',
  styleUrls: ['./spinner-img.component.scss']
})
export class SpinnerImgComponent implements OnInit {

  @Input() imgSrc: String;
  @Input() spinnerSrc: String;
  @Input() imgContainerClass: String;

  loading: boolean = true

  onLoad() {
    this.loading = false;
  }

  constructor() { }

  ngOnInit() { }

}

spinner-img.component.html:

<div [class]="imgContainerClass">
  <img *ngIf="loading" src="{{spinnerSrc}}" alt="loading"/>
  <img [hidden]="loading" (load)="onLoad()" src="{{imgSrc}}" alt="Hero Pic"/>
</div>

And in use:

 <toh-spinner-img [imgSrc]="hero.imgUrl" [spinnerSrc]="'/assets/ring.svg'"></toh-spinner-img>
Jeff
  • 4,285
  • 15
  • 63
  • 115
0

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.