Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazyloaded Image stays in "setup" state after URL change #473

Open
joe991 opened this issue Jul 30, 2020 · 4 comments
Open

Lazyloaded Image stays in "setup" state after URL change #473

joe991 opened this issue Jul 30, 2020 · 4 comments

Comments

@joe991
Copy link

joe991 commented Jul 30, 2020

I am using this very well made module for product images on a category page. Upon hover, the URL of the shown image changes to show other angles of said product.

Moving the mouse at exactly the right moment (exiting the hover, which changes the URL back to the original image) results in the lazyloaded image staying in the "setup" state - even when changing the URL again - which means that it turns back to the default image, without registering the fact that it is currently 'in view'.

Any ideas?

@tjoskar
Copy link
Owner

tjoskar commented Aug 2, 2020

Hey @joe991

Does your component look something like?

@Component({
  selector: 'async-image',
  template: `
    <img [defaultImage]="defaultImage" [lazyLoad]="image" (mouseEnter)="mouseEnter()" (mouseLeave)="mouseLeave()" />
  `
})
class MyComponent {
  defaultImage = 'https://www.placecage.com/300/300';
  image = 'https://picsum.photos/id/236/300/300';  

  mouseLeave() {
    this.image = 'https://picsum.photos/id/236/300/300';
  }

  mouseEnter() {
    this.image = 'https://picsum.photos/id/237/300/300';
  }
}

This will:

  1. Load the default image
  2. Load https://picsum.photos/id/236/300/300 when it appears in the viewport
  3. When the image is loaded it will take place in the document
  4. When the user hover the image, it will start loading https://picsum.photos/id/237/300/300
  5. Meanwhile, the image is loading we will show the default image.
  6. When https://picsum.photos/id/237/300/300 is loaded we will show that one instead of the default image.

It sounds like this is not the behavior you want?

It is possible to skip the default image when you change the image path if that is what you want.

@joe991
Copy link
Author

joe991 commented Aug 4, 2020

Hey @tjoskar

Not quite:

Template:

<picture class="{{ fitTo }}">

    <!-- xl -->
    <source
        *ngIf="!transparent"  
        media="(min-width: 993px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[4] + '.webp'"
    >
    <source
        *ngIf="transparent"  
        media="(min-width: 993px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[4] +' _transparent.webp'"
    >
    <source
        *ngIf="transparent"  
        media="(min-width: 993px)" 
        type="image/png" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[4] + '.png'"
    >
    <source
        *ngIf="!transparent" 
        media="(min-width: 993px)" 
        type="image/jpg"
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[4] + '.jpg'"
    >

    <!-- l -->
    <source
        *ngIf="!transparent"  
        media="(min-width: 800px) and (max-width: 992px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[3] + '.webp'"
    >
    <source
        *ngIf="transparent" 
        media="(min-width: 800px) and (max-width: 992px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]"  
        [attr.lazyLoad]="url + '_' + imageWidths[3] + '_transparent.webp'"
    >
    <source
        *ngIf="transparent"
        media="(min-width: 800px) and (max-width: 992px)" 
        type="image/png" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[3] + '.png'"
    >
    <source
        *ngIf="!transparent" 
        media="(min-width: 800px) and (max-width: 992px)"
        type="image/jpg" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[3] + '.jpg'"
    >

    <!-- m -->
    <source
        *ngIf="!transparent"  
        media="(min-width: 400px) and (max-width: 799px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[2] + '.webp'"
    >
    <source
        *ngIf="transparent" 
        media="(min-width: 400px) and (max-width: 799px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[2] + '_transparent.webp'"
    >
    <source
        *ngIf="transparent"
        media="(min-width: 400px) and (max-width: 799px)" 
        type="image/png" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[2] + '.png'"
    >
    <source
        *ngIf="!transparent"  
        media="(min-width: 400px) and (max-width: 799px)" 
        type="image/jpg"
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[2] + '.jpg'"
    >

    <!-- s -->
    <source
        *ngIf="!transparent"  
        media="(min-width: 250px) and (max-width: 399px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[1] + '.webp'"
    >
    <source
        *ngIf="transparent"
        media="(min-width: 250px) and (max-width: 399px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[1] + '_transparent.webp'"
    >
    <source
        *ngIf="!transparent"  
        media="(min-width: 250px) and (max-width: 399px)" 
        type="image/jpg" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[1] + '.jpg'"
    >
    <source
        *ngIf="transparent"
        media="(min-width: 250px) and (max-width: 399px)" 
        type="image/png" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[1] + '.png'"
    >

    <!-- xs -->
    <source
        *ngIf="!transparent"  
        media="(max-width: 249px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[0] + '.webp'"
    >
    <source
        *ngIf="transparent"
        media="(max-width: 249px)" 
        type="image/webp" 
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[0] + '_transparent.webp'"
    >
    <source
        *ngIf="!transparent" 
        media="(max-width: 249px)"
        type="image/jpg"
        [attr.defaultImage]="pImage[pNum]" 
        [attr.lazyLoad]="url + '_' + imageWidths[0] + '.jpg'"
    >
    <source 
        *ngIf="transparent"
        media="(max-width: 249px)" 
        type="image/png" 
        [attr.defaultImage]="pImage[pNum]"  
        [attr.lazyLoad]="url + '_' + imageWidths[0] + '.png'"
    >

    <img
        [errorImage]="'../assets/white_square.jpg'"
        [lazyLoad]="url + '_800w.jpg'"
        [defaultImage]="pImage[pNum]"
        (load)="onLoad()"
        alt="{{ alt }}"
    >
    <div class="overlay" *ngIf="loadingAnimation && pNum === 1">
        <app-loading class="loading-icon" *ngIf="loadingAnimation"></app-loading>
    </div>
</picture>

Typescript:

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

@Component({
  selector: 'app-image-tag',
  templateUrl: './image-tag.component.html',
  styleUrls: ['./image-tag.component.less']
})
export class ImageTagComponent implements OnInit {

  @Input()
  url = '';

  @Input()
  columns = [];

  @Input()
  alt = '';

  @Input()
  pNum = 1;

  @Input()
  transparent = false;

  @Input()
  fitTo = 'width';

  pImage = [
    '../assets/placeholder_team.jpg',
    '../assets/white_square.jpg'
  ];

  loadingAnimation = true;

  // our different image sizes
  sizes = [250, 400, 800, 1000, 1500];

  // the to implement widths
  imageWidths = [];

  constructor() { }

  ngOnInit() {
    this.sizes.forEach((size, index) => {

      // calculate necessary size
      const tempSizeValue = size / this.columns[index];

      // get image sizing
      if (tempSizeValue > 1001) {
        this.imageWidths.push('1500w');
      } else if (tempSizeValue > 801) {
        this.imageWidths.push('1000w');
      } else if (tempSizeValue > 401) {
        this.imageWidths.push('800w');
      } else if (tempSizeValue > 251) {
        this.imageWidths.push('400w');
      } else {
        this.imageWidths.push('250w');
      }
    });
  }

  onLoad() {
    this.loadingAnimation = false;
  }

}

The URL is changed by a parent component which works the way that you suggested. However, during debugging, the lazyloading image just has the "ready" state if the URL is changed too quickly.

@ClemensSchneider
Copy link

I'm observing a pretty similar behavior, where the loading of images is stuck in "setup" state and cannot be recovered even if the underlying img-src changes.

For me this can be reproduced as follows:

  1. scroll image into viewport so it is lazyloaded
  2. scroll image out of viewport
  3. url bound via [lazyload] is updated at least twice!
  4. scroll image back into viewport
  5. -> nothing happens and future updates of the bound url have no effect

See a minimal reproduction of the issue at https://stackblitz.com/edit/angular-ivy-pca22x?file=src/app/app.component.html

For me, providing a custom getObservable implementation fixed the issue, so it seems that the intersection-observer caching somehow breaks things. Is it even necessary to cache them at all?

class LazyLoadImageHooks extends IntersectionObserverHooks {
    getObservable(
        attributes: Attributes<{
            isIntersecting: boolean;
        }>
    ) {
        if (this.skipLazyLoading(attributes)) {
            return of({ isIntersecting: true });
        }
        if (attributes.customObservable) {
            return attributes.customObservable;
        }
        const options: IntersectionObserverInit = {
            root: attributes.scrollContainer || null,
            threshold: [0]
        };
        if (attributes.offset) {
            options.rootMargin = `${attributes.offset}px`;
        }

        return new Observable<{ isIntersecting: boolean }>((subscriber) => {
            const observer = new IntersectionObserver((entrys) => {
                entrys.forEach((entry) => subscriber.next(entry));
            }, options);

            observer.observe(attributes.element);

            return () => {
                observer.disconnect();
            };
        });
    }
}

@ekrapfl
Copy link

ekrapfl commented May 12, 2021

I am also experiencing a similar issue. In my case, there is cell reuse involved (through cdk-virtual-scroll-viewport), but I think the problem is happening for the same reason, though. In the end, I am trying to change the [lazyLoad] on an image that was previously not in the viewport. I have modified @ClemensSchneider's StackBlitz example slightly to add a bit of logging that also highlights the issue.

https://stackblitz.com/edit/angular-ivy-qgdkdb

I have also verified that @ClemensSchneider's modified getObservable also works to work around the issue for me.

Any thoughts on a permanent fix for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants