0

I have an issue with a directive that I created in Angular. In this directive, I want to execute some code after the window.onload event to be sure that all the page resources have been loaded (because I want to know the page top offset of some elements in the directive and if the images are not loaded, this offset is not correct)

If I refresh the page, it works because of the window.onload event is fired but when using the angular navigation to reach my component, this event is not fired anymore. I tried to use the lifecycle of angular like AfterViewInit but most of the time the AfterViewInit method is executed before the images are loaded.

UPDATE This is the code of my directive :

export class ParallaxDirective implements OnInit, OnDestroy {
    @Input() coef = 1;

    start = 0;
    path = 0;
    initialTranslate = 0;
    intersectObserver: IntersectionObserver;
    inView: boolean;

    constructor(
      private element: ElementRef
    ) {

    }

    ngOnInit(): void {
      window.addEventListener('load', () => this.initParallax());
    }

    ngOnDestroy(): void {
      this.intersectObserver.unobserve(this.element.nativeElement);
      window.removeEventListener('scroll', () =>this.setTransform());
    }

    initParallax(): void {
      this.intersectObserver = new IntersectionObserver(this.intersect.bind(this));
      this.intersectObserver.observe(this.element.nativeElement);

      const initialY = new DOMMatrixReadOnly(window.getComputedStyle(this.element.nativeElement).getPropertyValue('transform')).m42;
      this.start = this.element.nativeElement.offsetTop + initialY;
      this.path = this.element.nativeElement.clientHeight;
      this.initialTranslate = (initialY /
this.element.nativeElement.clientHeight) * 100;
      if (window.pageYOffset > 0) {
        this.setTransform(true);
      }
      window.addEventListener('scroll', () => this.setTransform());
  }

  setTransform(force = false): void {
    if (!this.inView && !force) {
      return;
    }
    const offset = window.pageYOffset + window.innerHeight - this.start;
    const t = (offset * 10) / this.path;
    const i = t * this.coef + this.initialTranslate;
    this.element.nativeElement.style.setProperty('transform', `translate3d(0, ${i}%, 0)`);
  }

  intersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
    entries.forEach(entry => {
      this.inView = entry.isIntersecting || entry.intersectionRatio > 0;
    });
  }

}

As you can see, I need to retrieve some offsets and dom elements height. The problem is that these offsets and heights are not the same after resources in the page (like images) are completely loaded that's why I need to have an event like window.onload to be sure that everything is loaded. And I didn't find any Angular lifecycle that should be triggered after resources load.

I hope someone can help me with this.

Thanks.

Romain
  • 33
  • 9
  • Angular routing does not trigger window events. You have to use the life cycle methods as you are using. Please give more detail as to what you're trying and how it fails. – Aviad P. Jul 19 '21 at 19:51
  • In `ngOnInit` don't subscribe to `load` because it will never fire. Just do your init call directly. – Aviad P. Jul 20 '21 at 07:25
  • But in the ngOnInit, the resources are not loaded and the value of this.element.nativeElement.clientHeight is therefore not correct – Romain Jul 20 '21 at 07:34
  • You're right, use `AfterViewInit` – Aviad P. Jul 20 '21 at 07:35
  • Unfortunately AfterViewInit is triggered before the resources load too... So I still don't have the good value for the clientHeight of my element – Romain Jul 20 '21 at 07:36
  • Use `@HostListener('resize')` to detect host element size changes – Aviad P. Jul 20 '21 at 07:38
  • Yes but all resources are not loaded at once, so the event will be triggered many times.. – Romain Jul 20 '21 at 07:42
  • You can wait for it to stabilize by piping it through a debounce filter - do you know how to do that using rxjs? – Aviad P. Jul 20 '21 at 07:45
  • Yes and what if one of the picture takes too much time to load and therefore is not in the debounce time? it's not enough reliable in this case. Don't we have any equivalent of window.load on angular navigation? – Romain Jul 20 '21 at 07:49
  • Debounce simply prevents rapid changes. It will get the event even if it takes a while. – Aviad P. Jul 20 '21 at 08:44
  • Yes but as we will have a lot of resize event, if some of them a rapid they will be handled by the debounce, but if one of them is slower and not handled by the debounce, the resize event will be triggered multiple times – Romain Jul 20 '21 at 09:00
  • Thats right. Thats the best you can do – Aviad P. Jul 20 '21 at 09:03
  • Yes, thanks for your help and your time. I can't believe there is nothing on angular to catch the resources load event, like window.onload.. – Romain Jul 20 '21 at 09:23
  • If it exists outside of angular then you can catch it in angular - but the problem is it doesn't exist outside angular either. – Aviad P. Jul 20 '21 at 11:44
  • If we have window.onload being able to know if all the resources are loaded we should be able to have it on angular too, no? The problem comes from angular navigation in this case that does not trigger this event – Romain Jul 20 '21 at 13:16
  • That is because the resources are created dynamically by angular and therefore AfterViewInit should work. Can you specify exactly what doesn't work with that? – Aviad P. Jul 20 '21 at 13:33
  • As I say in my question and in the comment, Afterview init is triggered before the img is really loaded. Window.onload is triggered when all the resources of the page are completely loaded – Romain Jul 20 '21 at 14:07
  • See this: https://stackoverflow.com/a/39257902/235648 – Aviad P. Jul 20 '21 at 14:08
  • 1
    Yes unfortunately this does not help me, because the number of image is dynamic and that doesn't tell me if they are all loaded. I used document.querySelector('img') in the afterViewInit method to retrieve the imgs, and then count the number of img and the number of img loaded to ensure that they are all loaded, but the querySelector returns me the img of the previous component also... – Romain Jul 20 '21 at 14:16
  • See this: https://stackoverflow.com/a/11071687/235648 – Aviad P. Jul 20 '21 at 14:17
  • Can't you tell from your component logic how many images are added to the page? (this is starting to become personal for me :) ) – Aviad P. Jul 20 '21 at 14:19
  • I have component A with 5 images. I have component B with 8 images. If I navigate from A to B, document.images returns 13 images in afterviewInit. If I refresh only B, document.images returns 8 images in afterviewinit. – Romain Jul 20 '21 at 14:20
  • Can't you tell in the component logic how many images are used? And subscribe to their `load` events the angular way? - If you subsribe the angular way (`(load)="..."`) then it will not fire if the component is not active – Aviad P. Jul 20 '21 at 14:23
  • Yes I can count all the image I have static in my page, and all the dynamic images count all the loaded image using the angular (load) but I really don't want to hardcode all this values when I update my page or something, but that's a solution. I find it very strange that retrieving all the images in the afterViewInit method also returns me the images of the previous component after the navigation... – Romain Jul 20 '21 at 14:29
  • It's not strange, it's even expected. Angular hides components instead of removing them from the DOM so that when navigating back to them, it can simply bring them back into view without recreating them. - Also, I don't see this as hard coding, since your images are probably used in `*ngFor` so it should be concise – Aviad P. Jul 20 '21 at 14:52
  • I believe my answer addresses all your issues. – Aviad P. Jul 20 '21 at 15:41

2 Answers2

1

After a long long exchange of comments, I finally understood your problem, Romain, and here's what I came up with.

My solution involves adding a directive and a service. The directive will be attached to all <img> tags, and will subscribe to their load events, and the service will coordinate all the load events firing and maintain a running list of images that are still being loaded.

Here's the directive:

@Directive({
  selector: 'img'
})
export class MyImgDirective {
  constructor(private el: ElementRef, private imageService: ImageService) {
    imageService.imageLoading(el.nativeElement);
  }

  @HostListener('load')
  onLoad() {
    this.imageService.imageLoadedOrError(this.el.nativeElement);
  }

  @HostListener('error')
  onError() {
    this.imageService.imageLoadedOrError(this.el.nativeElement);
  }
}

Here's the service:

@Injectable({
  providedIn: 'root'
})
export class ImageService {
  private _imagesLoading = new Subject<number>();
  private images: Map<HTMLElement, boolean> = new Map();
  private imagesLoading = 0;

  imagesLoading$ = this._imagesLoading.asObservable();

  imageLoading(img: HTMLElement) {
    if (!this.images.has(img) || this.images.get(img)) {
      this.images.set(img, false);
      this.imagesLoading++;
      console.log('images loading', this.imagesLoading);
      this._imagesLoading.next(this.imagesLoading);
    }
  }

  imageLoadedOrError(img: HTMLElement) {
    if (this.images.has(img) && !this.images.get(img)) {
      this.images.set(img, true);
      this.imagesLoading--;
      console.log('images loading', this.imagesLoading);
      this._imagesLoading.next(this.imagesLoading);
    }
  }
}

And here's how I would use it in your parallax directive:

  constructor(private imageService: ImageService) {}

  ngOnInit() {
    this.sub = imageService.imagesLoading$.pipe(filter(r => r === 0)).subscribe(_ => {
      this.initParallax()
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

How does this work? The image directive gets attached to all images regardless of how deep they are in your component tree, registers the image with the service, and then tracks the loading progress by listening to the load event. Every time a new directive is created it means a new image is created, so a counter is incremented, and every time load fires, it means an image has finished loading, so the counter is decremented. We emit this counter in an observable, and so we can detect when all images are ready by waiting for the observable to emit the value 0.

Stackblitz demo

EDIT Added error handling in case an image points to a broken link

Aviad P.
  • 32,036
  • 14
  • 103
  • 124
  • If you decide to accept my answer, I think you should change the title of the question to "Detect when all images have done loading" or something like that – Aviad P. Jul 20 '21 at 15:30
  • thanks a lot for your help and your time. I will accept this as the right answer but I must say that I'm a bit that the resource loading event is not really handled by Angular. – Romain Jul 21 '21 at 09:43
  • You wouldn't have managed to do it any other way without a full page reload - don't you agree? – Aviad P. Jul 21 '21 at 18:48
  • Yes I agree, that's why I accept your answer – Romain Jul 22 '21 at 19:40
0

You can use HostListener

import { HostListener, Component } from "@angular/core";

@Component({
  selector: 'app',
  template: `<h1>{{message}}</h1>`
})
class AppComponent {
  message = "Window not loaded";

  @HostListener('load', ['$event'])
  windowLoadedEvent(event) {
    this.message = "Window loaded";
  }
}

Note: It will work only if the component is initialized before window is loaded. Otherwise it will not work

Jp Vinjamoori
  • 1,173
  • 5
  • 16
  • Thanks for your answer but as you say, it doesn't work if the component is loaded after window.load.. – Romain Jul 20 '21 at 14:27