angularangular-directivedirective

How to overwrite data-bound-property of HTML-Element with Angular directive


I am trying to implement a fallback image on an img-element in angular. Therefor i have created a directive that listens for errors on the img and replaces its source with the fallback image whenever an error occurs.

Now the original src attribute of my image is set via data binding which results in a permanent loop of changing the src back and forth between the original and the fallback. I assume that is because the data-binding keeps on changing the src back.

My template:

<img [src]="sanitizer.bypassSecurityTrustResourceUrl(building + url + with + variables)"
  fallbackImage="path/to/fallback.svg">

My Directive:

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

@Directive({
    selector: 'img[fallbackImage]'
})
export class ImgFallbackDirective {

    constructor(private element: ElementRef) {}

    @Input('fallbackImage') fallbackImage: string;

    @HostListener('error') displayFallbackImage() {
        console.log('Before: ', this.element.nativeElement.src);
        this.element.nativeElement.src = this.fallbackImage || '';
        console.log('After: ', this.element.nativeElement.src);
    }
}

The logs document the src being changed back and forth.


Solution

  • We can insert an adjacent element( img ) with the fallback src set, Then we can delete the element on which the directive exists, this works great!

    @HostListener('error') displayFallbackImage() {
        if (this.isFirstLoad) {
          this.isFirstLoad = false;
          var img = document.createElement('img');
          img.src = this.fallbackImage;
          this.renderer.appendChild(this.element.nativeElement.parentNode, img);
          this.renderer.removeChild(
            this.element.nativeElement.parentNode,
            this.element.nativeElement,
            true
          );
        }
      }
    

    full code

    main.ts

    import { Component } from '@angular/core';
    import { bootstrapApplication, DomSanitizer } from '@angular/platform-browser';
    import 'zone.js';
    import { ImgFallbackDirective } from './test.directive';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [ImgFallbackDirective],
      template: `
        <img [src]="sanitizer.bypassSecurityTrustResourceUrl(failingUrl)"
      fallbackImage="https://placehold.co/600x400">
      `,
    })
    export class App {
      failingUrl = 'https://placeeeeeeeeeehold.co/600x400';
    
      constructor(public sanitizer: DomSanitizer) {}
    
      setFailingUrl(value: any) {
        console.log(value);
        this.failingUrl = value;
      }
    }
    
    bootstrapApplication(App);
    

    directive

    import {
      Directive,
      ElementRef,
      HostListener,
      Input,
      Renderer2,
    } from '@angular/core';
    
    @Directive({
      selector: 'img[fallbackImage]',
      standalone: true,
    })
    export class ImgFallbackDirective {
      isFirstLoad = true;
      constructor(private element: ElementRef, private renderer: Renderer2) {}
    
      @Input('fallbackImage') fallbackImage!: string;
    
      @HostListener('error') displayFallbackImage() {
        if (this.isFirstLoad) {
          this.isFirstLoad = false;
          var img = document.createElement('img');
          img.src = this.fallbackImage;
          this.renderer.appendChild(this.element.nativeElement.parentNode, img);
          this.renderer.removeChild(
            this.element.nativeElement.parentNode,
            this.element.nativeElement,
            true
          );
        }
      }
    }
    

    Stackblitz Demo