Search code examples
htmlangularperformanceimage-loading

Content in ngOnInit or ngAfterViewInit should not load until alll images in <IMG> tags are loaded


I'm using Angular 8 for a blog based web-app. Data is stored in json file for now, even images to be loaded along with the path.

JSON data

[
    {
        "imgSrc": "./assets/images/dalai-hills-1.jpg",
        "destination": "Dalai Hills",
        "introTitle": "through happy valley, to a picturesque place",
        "place": "mussoorie, uttarakhand",
        "description": "Lorem ipsum dolor sit amet, no elitr tation delicata cum, mei in causae deseruisse.",
    }
]

The imgSrc decides which image to load. All the images are already optimized and places inside assets folder.

Template

<article class="blog-card" style="border-top: 0;" *ngFor="let blog of blogsList">
    <div class="blog-img-wrap" style="min-height: 200px;">
        <a href="#"">
            <img loading="lazy" class="img-fluid blog-img" src="{{ blog.imgSrc }}" alt="blog-image-1">
        </a>
    </div>
</article>

Let's say, in the blogs page, on load there are 12 images loading because of , I want to ensure page should load only after all the images are loaded.

I am not getting any concrete answer on stackoverflow. Currently, there is a very small fraction of seconds difference between the text and images getting loaded but it looks weird.

Any solutions on the same?

P.S: I want to avoid jQuery.


Solution

  • You can;

    1. Create image elements programatically. (use HTMLImageElement)
    2. Track their loading state. (use ReplaySubject and forkJoin)
    3. When all images are loaded show them in page. (use async pipe and Renderer2)

    Here is a sample implementation (explanations are in comments);

    @Component({
      selector: "my-app",
      templateUrl: "./app.component.html",
      styleUrls: ["./app.component.css"]
    })
    export class AppComponent implements OnInit {
      data = [
        {
          imgSrc: "https://img.jakpost.net/c/2017/10/27/2017_10_27_34794_1509067747._large.jpg",
          destination: "destination-01",
          introTitle: "introTitle-01",
          place: "place-01",
          description: "description-01"
        },
        {
          imgSrc: "https://lakecomofoodtours.com/wp-content/uploads/gravedona-celiaa-img-0282_orig-800x600.jpg",
          destination: "destination-02",
          introTitle: "introTitle-02",
          place: "place-02",
          description: "description-02"
        },
        {
          imgSrc: "https://italicsmag.com/wp-content/uploads/2020/05/Lake-Como-5-770x550.jpg",
          destination: "destination-03",
          introTitle: "introTitle-03",
          place: "place-03",
          description: "description-03"
        }
      ];
    
      /* This array holds Observables for images' loading status */
      private tmp: ReplaySubject<Event>[] = [];
    
      blogData: BlogDataType[] = this.data.map(d => {
        const img = new Image();
        img.height = 200;
        img.width = 300;
        const evt = new ReplaySubject<Event>(1);
        img.onload = e => {
          evt.next(e);
          evt.complete();
        };
        this.tmp.push(evt);
        img.src = d.imgSrc 
        return { ...d, imgElem: img };
      });
    
      /* 
       * Convert images' loading status observables to a higher-order observable .
       * When all observables complete, forkJoin emits the last emitted value from each.
       * since we emit only one value and complete in img.load callback, forkJoin suits our needs.
       * 
       * when forkJoin emits (i.e. all images are loaded) we emit this.blogData so that we use it
       * in template with async pipe
       */
      blogsList: Observable<BlogDataType[]> = forkJoin(this.tmp).pipe(
        map(() => this.blogData)
      );
    
      constructor(private renderer: Renderer2) {}
    
      /* manually append image elements to DOM, used in template */
      appendImg(anchor: HTMLAnchorElement, img: HTMLImageElement) {
        this.renderer.appendChild(anchor, img);
        return "";
      }
    }
    
    interface BlogDataType {
      imgElem: HTMLImageElement;
      imgSrc: string;
      destination: string;
      introTitle: string;
      place: string;
      description: string;
    }
    
    <div *ngFor="let blog of blogsList | async">
      <div>{{blog.description}}</div>
      <a #aEl href="#">
        {{ appendImg(aEl, blog.imgElem) }}
      </a>
    </div>
    

    Here is a working demo: https://stackblitz.com/edit/angular-ivy-ymmfz6

    Please note that this implementation is not error-proof. img.onerror should be also handled for use in production, i skipped it for the sake of simplicity.