NG2 Image Loading

November 13, 2016 by Stuart Kuentzel

We’ll be using the Angular CLI to build out a small project to demonstrate this, so you’ll need it installed on your machine. If you don’t have the CLI, you can get it on their GitHub page. You can check the final code for this project on GitHub.

NG Build a project

Let’s start by building out a small project to see this in action. In your terminal, navigate to where you want your project to live and run ng new async_img.

Once that’s all installed, we can also create a service to fetch image data. In the terminal, run cd async_img then ng generate service img. For this tutorial, we’ll be using Unsplash.

Now, if you run ng serve and open localhost:4200 in your browser, you’ll see “App Works”.

Async Load Single Image

Open up the project in a text editor of your choice, and open the src/app folder. Let’s first add the ImgService to our app. Inside of app.module

// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { ImgService } from './img.service';

import { AppComponent } from './app.component';

@NgModule({
declarations: [
  AppComponent
],
imports: [
  BrowserModule,
  FormsModule,
  HttpModule
],
providers: [ImgService],
bootstrap: [AppComponent]
})
export class AppModule { }

We’ll quickly build out the service to get a list of images from UnSplash.

// img.service.ts

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Injectable()
export class ImgService {

 constructor(private http: Http) { }

 getImgList(): Observable<Object[]> {
   return this.http
     .get('https://unsplash.it/list')
     .map( (res) =>
       res.json()
     );
 }

}

When we call the getImgList() function, it’s going to return a big list from UnSplash.

Now we’ll open up app.component.ts to make the call to ImgService. At the top, we import everything we need, and set the ImgService as a provider for the component. Make sure to add implements OnInit in our class export.

// app.component.ts

import { Component, OnInit } from '@angular/core';
import { ImgService } from './img.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [ ImgService ]
})
export class AppComponent implements OnInit { }

Next, we’ll write some logic in our component so we can make a call to the service and set the response as a variable.

// app.component.ts
// ...

single_img: Object;

constructor( private imgService: ImgService) {}

ngOnInit() {

  this.imgService
    .getImgList()
    .subscribe( (res: Object[]) => {
      let randomImg = Math.floor(Math.random() * 1000) + 1;
      this.single_img = res[randomImg];
      this.single_img['loading'] = true;
      this.imgPreload(this.single_img);
    });

}

imgPreload(new_image) {
  let c = new Image();
  c.src = new_image['post_url'] + '/download';
  c.onload = () => {
    this.single_img['loading'] = false;
  }
}

Let’s walk through this. At the top, we’re declaring a new variable single_img, which we will use in our view. In the constructor, we create a private instance of the service, which we can now use in the component. Inside of ngOnInit, we call the function we wrote in our image service above, and assign the response to the single_img variable.

We the data, so let’s bind it to app.component.html.

<div *ngIf="single_img">
  <div class="featured-image img"
    [ngStyle]="{'background-image': 'url(' + single_img.post_url + '/download)'}"
    [class.loading]="single_img.loading"></div>
</div>

We’re doing a few things here. First, we’re checking if there is single_img. This way our app doesn’t break if there’s an error. In ngStyle, we’re binding single_img.post_url as the background image. Finally, we’re adding the .loading class to the element. Remember, in our component in ngOnInit, we initially set the value to true, then after the image is loaded we set the value to false. This way, the element will have the loading class as it loads, then is removed. This is key to transition the element in.

All that’s left is changing the css. We’ll do this in two places. First, open app.component.css

app.component.css

.featured-image{
  width: 100%;
  height: 100vh;
  background-size: cover;
}

.img.loading{
  opacity: 0;
}

.img{
  opacity: 1;
  transition: opacity 300ms ease;
}

On initial load, when the .img element also has the class .loading, we set the opacity as 0. When the image has been loaded, and the loading class removed, we transition into opacity 1. Next, outside of the app folder, still inside of the src folder, open styles.css.

styles.css

/* You can add global styles to this file, and also import other style files */
body{ margin: 0; }

As the note here suggests, we put css here that is site-wide. We’re just getting rid of those pesky margins, since we’re styling the image as a full bleed image.

If we refresh our app at localhost:4200, we’ll see a random image that is full bleed.

Async Load Image Collection

Sometime we might have a collection of images we have to fade in on load. Rather than fading in the entire collection after all the images have loaded, we can fade in each image as it loads.

We could alter our function to be more robust, but in this case we’ll create another function for the sake of simplicity. Inside of app.component we’ll add some code.

// app.component.ts

img_collection: Object[] = [];

constructor( private imgService: ImgService) {}

ngOnInit() {

  this.imgService
    .getImgList()
    .subscribe( (res: Object[]) => {

      this.handleFeaturedImg(res);
      this.handleImgCollection(res);

    });

}

handleImgCollection(res) {

  for ( let i = 0; i < 9; i++ ) {
    let randomImg = Math.floor(Math.random() * 1000) + 1;

    this.img_collection.push(res[randomImg]);
    this.img_collection[i]['loading'] = true;

    this.collectionImgPreload( this.img_collection[i], i );

  }

}

collectionImgPreload(new_image, i) {
  let c = new Image();
  c.src = new_image['post_url'] + '/download';
  c['index'] = i;
  c.onload = (c) => {
    let loadedI = c['target']['index'];
    this.img_collection[loadedI]['loading'] = false;
  }
}

Let’s break all this down. Inside of ngOnInit, we’re calling a new function handleImgCollection(res) and passing it res.

Below that, we write a loop. In this case, since we’re dealing with random images, I just am looping through the first 9 of the collection. In a real instance of this, you’d loop through all of res. Just like in the handleFeaturedImg function, we’re picking a random image to our image collection by creating a random number less than 1000 and passing res[randomImg] to the img_collection array we declared at the top. We also create a new property for our object, loading, and set that to true. Next, we pass our newly created object and its index to the newly created collectionImgPreload(this.img_collection[i], i ) function.

Inside of the collectionImgPreload(new_image, i), our function is similar to our previous imgPreload function, but there are subtle differences. We add a property to the new Image, c['index'] and assign it to the index we pass down into the function, i. Then, when we load the new image, we are retrieving that index value let loadedI = c['target']['index'];. That way, we can check which image is ready inside of the img_collection, and fade it in.

We have to set up a place to render these images inside of the HTML.

// app.component.html

<div *ngIf="single_img">
<div class="featured-image img"
[ngStyle]="{'background-image': 'url(' + single_img.post_url + '/download)'}"
[class.loading]="single_img.loading"></div>
</div>

<div *ngIf="img_collection">
  <div *ngFor="let img of img_collection">
    <div class="single-img-collection img"
    [ngStyle]="{'background-image': 'url(' + img.post_url + '/download)'}"
    [class.loading]="img.loading">
    </div>
  </div>
</div>

You can see we’re setting it up the same way as the featured image, but we’re looping with *ngFor="let img of img_collection". In the component, after the image has been loaded, we set the loading property to false, which removes the loading class in the HTML.

And that’s it! You can check the final code on GitHub. Get out there and make your app look less janky!

© 2018