Converting Treehouse’s Asynchronous Image Loading tutorial into an ionic 2 directive

In Custom Components, Ionic 2 by seme0 Comments

When working on a project recently I decided to take a crack at building an image preloader vs plunking an existing one, and I found the tutorial from Treehouse’s Asynchronous Image Loading with JavaScript to be straight forward and comprehensible. And I decided to share how its done.

You should head to their site for more detail, but this is the full code in plain JavaScript:

var image = document.images[0];
var downloadingImage = new Image();
downloadingImage.onload = function(){
    image.src = this.src;   
};
downloadingImage.src = "http://an.image/to/aynchrounously/download.jpg";

Our final code will enable us to use it like this:

<img img-preload="http://some_remote_image_url">

To give us this:

  • Without our preloader.

  • With our preloader

Project Setup

Lets start a new ionic 2 project. If you need help setting up ionic itself, the Ionic getting started guide is the best place to start.

ionic start imgpreload sidemenu --v2

Make our project folder the working folder in our terminal/command prompt

cd imgpreload

now lets create our directive img-preload

ionic g directive img-preload

Next, we add our directive into our app.module.ts to make sure it’s available app wide

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { Page1 } from '../pages/page1/page1';
import { Page2 } from '../pages/page2/page2';
import { ImagePreloader } from '../components/img-preload/img-preload';

@NgModule({
  declarations: [
    MyApp,
    Page1,
    Page2,
    ImagePreloader
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    Page1,
    Page2
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

now run ionic serve

lets open our default page1 at src/app/pages/page1.html. We’ll add a simple <img> tag that points to a really big picture.

<ion-content padding>
  <h3>Ionic Menu Starter</h3>
  <img src="https://images.unsplash.com/photo-1413781892741-08a142b23dfe" alt="">
  <p>
    If you get lost, the <a href="http://ionicframework.com/docs/v2">docs</a> will show you the way.
  </p>
</ion-content>

You internet speed may vary, but you may get something like this:

so create our preloader to fix this.

Building our Preloader

Open our src/components/img-preload/img-preload.ts:

import { Directive } from '@angular/core';

/*
  Generated class for the ImgPreload directive.

  See https://angular.io/docs/ts/latest/api/core/index/DirectiveMetadata-class.html
  for more info on Angular 2 Directives.
*/
@Directive({
  selector: '[img-preload]' // Attribute selector
})
export class ImgPreload {

  constructor() {
    console.log('Hello ImgPreload Directive');
  }

}

First off, we need our directive to pick up the src attribute of our image. To do this, we use the host meta of our directive to target the src of any image we attach this directive to:

import { Directive } from '@angular/core';

/*
  Generated class for the ImgPreload directive.

  See https://angular.io/docs/ts/latest/api/core/index/DirectiveMetadata-class.html
  for more info on Angular 2 Directives.
*/
@Directive({
  selector: '[img-preload]', // Attribute selector
  host: {
    '[attr.src]': 'finalImage'    //the attribute of the host element we want to update. in this case, <img 'src' />
  }
})
export class ImgPreload {

  constructor() {
    console.log('Hello ImgPreload Directive');
  }

}

the ‘finalImage’ assigned to our source attribute is a variable we can use in our class for further manipulation. basically, any value we assign the variable finalImage will be reflected in our img’s src.

Prepping our details and inputs

Just to itemize our preparations in our class:

  1. We specify an input that takes our large image url as parameter.
    @Input('img-preload') targetSource: string;
  2. Create a variable to hold the image from our Input():
    downloadingImage : any;
  3. We’ll also create an input to set a placeholder image for display while we download our image.
    @Input() defaultImage : string = 'assets/preloader.gif'; Note: You can use this preloader animated gif below. Copy to your src/assets folder.
  4. We’re also importing ngOnInit into our directive so our inputs a read once the directive is initiated.
Here’s the code so far:
import { Directive, Input, OnInit } from '@angular/core';

/*
  Generated class for the ImgPreload directive.

  See https://angular.io/docs/ts/latest/api/core/index/DirectiveMetadata-class.html
  for more info on Angular 2 Directives.
*/
@Directive({
  selector: '[img-preload]' // Attribute selector
})
export class ImgPreload implements OnInit {
  @Input('img-preload') targetSource: string;
  
  downloadingImage : any; // In class holder of remote image
  
  finalImage: any; //property bound to our host attribute.
  
  // Set an input so the directive can set a default image.
  @Input() defaultImage : string = 'assets/blank-default-feed-bg.png';
  constructor() {
    console.log('Hello ImgPreload Directive');
  }
  ngOnInit() {
      // our code starts/ends here.
  }

}

And now for the main event

in our ngOnInit(), lets take it line by line and show the full code further down:

  • We assign our default image placeholder to our <img>’s src. Remember our finalImage variable:
    this.finalImage = this.defaultImage;
  • We assign an Image() object to our downloadLoading image holder. The instant we set the src of this object, our image will be downloading in the background.
    this.downloadingImage = new Image();
  • Our image object gives us access to an onLoad() event, which will fire as soon as our image is downloaded. At this point we assign our new image to the the src attribute of our image tag, overriding the placeholder image we had before.

Here’s our final, usable code:

// Asynchronos ImagePreloader
// By Audacitus.com
// An image directive based on http://blog.teamtreehouse.com/learn-asynchronous-image-loading-javascript
import {Directive, Input, OnInit} from '@angular/core';

// Define the Directive meta data
@Directive({
  selector: '[img-preloader]', //E.g <img mg-img-preloader="http://some_remote_image_url"
  host: {
    '[attr.src]': 'finalImage'    //the attribute of the host element we want to update. in this case, <img 'src' />
  }
})

//Class must implement OnInit for @Input()
export class ImagePreloader implements OnInit {
  @Input('img-preloader') targetSource: string;

  downloadingImage : any; // In class holder of remote image
  finalImage: any; //property bound to our host attribute.

  // Set an input so the directive can set a default image.
  @Input() defaultImage : string = 'assets/preloader.gif';

  //ngOnInit is needed to access the @inputs() variables. these aren't available on constructor()
  ngOnInit() {
    //First set the final image to some default image while we prepare our preloader:
    this.finalImage = this.defaultImage;


    this.downloadingImage = new Image();  // create image object
    this.downloadingImage.onload = () => { //Once image is completed, console.log confirmation and switch our host attribute
      console.log('image downloaded');
      this.finalImage = this.targetSource;  //do the switch 😀
    }
    // Assign the src to that of some_remote_image_url. Since its an Image Object the
    // on assignment from this.targetSource download would start immediately in the background
    // and trigger the onload()
    this.downloadingImage.src = this.targetSource;
  }

}

Then we use it in our src/pages/page1/page1.html like this:

<img img-preloader="https://images.unsplash.com/photo-1413781892741-08a142b23dfe" alt="">

Restart Ionic Serve and you should the following effect now:

Sign-up for weekly newsletter
SUBSCRIBE