Walter Luszczyk
Walter Luszczyk

Reputation: 1522

How to gently change img src in Angular2+

When I dynamically change img's src attribute, old image is displayed while loading a new one.

I have a component which displays some data: text and image. On click the underlying data is changed (i.e. new data from server). Once click, text is changed immediately, but component displays old image while new one is loaded. When new image is loaded, then it is visually displayed which can take noticeable amount of time.

In real application one can have product details and changing products on button click. All data is replaced immediately but not image.

Problem exists when the component is not destroyed (reused).

I've already tried clear image src after click, but it not worked.

I have simple binding in template

img [src]="img.url" style="width: 300px; height: 300px">
<p>{{ img.text }}</p>

and image change on click

this.img = this.images[1];

You can see sample app here https://stackblitz.com/edit/angular-cojqnf

Is this possible to take more control of this image change process? It would be great to clear image on click and wait for new one with empty background for example.

Upvotes: 4

Views: 7457

Answers (2)

jo_va
jo_va

Reputation: 13993

I hacked around a little with your stackblitz demo, I basically wrapped your code in an ImageGhostDirective to make it reusable. The directive listens to any changes on the src attribute using a MutationObserver to change the style. Using a HostListener on the 'load' event, it reverts the styles back to normal. I start with an opacity of 0 for the first load, followed by an opacity of 0.2 between successive image changes, but this is completely arbitrary and could be replaced by a spinner or any kind of placeholder...

Here is the link to the stackblitz: https://stackblitz.com/edit/angular-image-ghost-directive

<img [src]="'https://loremflickr.com/300/300?random=' + index"
     style="width: 300px; height: 300px" imgGhost>
@Directive({
  selector: 'img[imgGhost]'
})
export class ImageGhostDirective implements OnDestroy {
  private changes: MutationObserver;

  constructor(private elementRef: ElementRef) {
    this.changes = new MutationObserver((mutations: MutationRecord[]) =>
      mutations.filter(m => m.attributeName === 'src').forEach(() => this.opacity = 0.2)
    );

    this.changes.observe(this.elementRef.nativeElement, {
      attributes: true,
      childList: false,
      characterData: false
    });
  }

  ngOnDestroy(): void {
    this.changes.disconnect();
  }

  @HostBinding('style.display') display = 'block';
  @HostBinding('style.opacity') opacity = 0;

  @HostListener('load')
  onLoad(): void {
    this.opacity = 1;
  }
}

It is also possible to tell Angular to automatically attach this directive to every img element by using the img:not([imgGhost]) selector in the directive decorator. That way, you don't have to manually place the directive on every image in your app.

Hope this is useful.

Upvotes: 7

Walter Luszczyk
Walter Luszczyk

Reputation: 1522

Finally I achieved what I want by leveraging (load) event on img and [ngStyle].

In template I added load handler and style:

<img [src]="img.url" style="width: 300px; height: 300px" (load)="loaded()"
 [ngStyle]="{'display': imgVisible ? 'block' : 'none'}">

In back-end:

imgVisible = true;

and when changing data, also hide image:

this.imgVisible = false;

next, when image is loaded, show the image (be careful! when old and new images have the same URL, this event is not raised; if it is the case you need to conditionally hide image)

loaded(): void {
  this.imgVisible = true;
}

Complete code for solution: https://stackblitz.com/edit/angular-ewptj7

I'm not a big fan of this kind solutions. It could be difficult to apply when you have more images.

All better solution are welcome.

Upvotes: 2

Related Questions