How to show a placeholder image before the actual image loads in an Ionic 2 or 3 app

Viewed 16517

Good day,

I'm working on an Ionic application and I'm loading images dynamically from a JSON feed. I would like that when the images are being pulled in from the external URL that a placeholder image shows in its place, thereafter, the placeholder image should then be replaced by the "real" image once its loaded.

An example of my current situation is:

<ion-card *ngFor="let item of items"...
 <img [src]="item.picture" />

Thanks & Regards.

5 Answers

Most of the answers are about adding the logic of the loaded check in the same view, but it seems not appropriate. Instead, you can create your own directive that will take care of it. You can take a look at this amazing article to see how it can be done.

First copy this GIF in your src/assets folder, and name it preloader.gif.

Then add this custom directive. The code is pretty much self-explanatory:

// 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 add the new directive to your app module, like this:

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

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

This is how you can use your new custom directive:

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

And this is how it will look like:

Result

You can then set the height/width of the container of the image, so both the preloader gif and the final image have the same dimensions.

As far as i know, there is no Ionic way to do it. But you still can do it by javascript and css.

CSS way:

Create a div cover your image and set the placeholder image is background-image of that div.

<div class="image">
    <img src="your_main_image"/>
</div>
.image{
    background-image: url('your_placeholder_image');
}

Javascript way:

There are several methods by javascript that you can easily find in stackoverflow so i will not re-answer here. This one is an example

Can you modify your JSON to have a loaded boolean property? If you can this will be easy, do as this:

<ion-card *ngFor="let item of items">
  <img [src]="item.picture" (load)="item.loaded = true" [hidden]="!item.loaded" />
  <img src="../path/to/your/placeholer/image.jpg" [hidden]="item.loaded">
</ion-card>

You'll have an loaded property always initalized as false, when the image loads the (load) event will be fired and change the image loaded property to true, hidding the placeholder image and showing the loaded imagem.

If you can't change this JSON (or if it's too big to edit) you can use the above code, but do the following in your page .ts

public functionWhoseIsLoadingYourJson(){
  // HTTP REQUEST FOR JSON
  .subscribe(response => {
    response.forEach(element => {
      element.loaded = false;
    });
    this.items = response;
  });
}

Just iterate your response and in every object set a loaded property to false. If this throw some error you can put your <ion-card> inside a div with a *ngIf

<div *ngIf="this.items != null">
  <!-- ion card -->
</div>

EDIT - Updating JSON

Since it's coming from Firebase there's three options to update your JSON. The first one is a function whose you would execute just once to update your database, the second still is the code i gave (iterating through results and pushing the boolean value) but itll be different since you're using Firebase.

First solution:

Somewhere in your code, it can be any page, you just need to execute it once:

firebase.database().ref('myDataNode').on('value', snapshot => {
  for(let key in snapshot.val()){
    firebase.database().ref('myDataNode/' + key + '/loaded').set(false);
  }
});

See that i'm looping through all resuts in your myDataNode and using their key to set a loaded property to false. But take care when using set since you can override all your nodes, before doing this method remember exporting your Firebase node to have a backup. And if you're pushing your images to Firebase from anywhere in your code remember to change it to set a loaded property together.

The second one, as mentioned, is what i've done up in this answer, but now using it inside your Firebase call and using the forEach method that comes along with snapshot:

firebase.database().ref('myDataNode').on('value', snapshot => {
  snapshot.forEach(element =>{
    this.myDataNode.push({
      picture: element.val(), // maybe it needs to be element.val().nameOfYourProperty
      loaded: false
    })
  });
});

This'll push a new element to your array with the picture url and the loaded property. If it can't recognize the .push() method just declare your variable typed as an array:

public myDataNode: any[];

The third one is just updating locally, itarate through your firebase snapshot, save the result in a local variable, create a new property and push it to your array:

firebase.database().ref('myDataNode').on('value', snapshot => {
  for (let key in snapshot.val()){
    snapshot.forEach(item => { // iterate snapshot
      let record = item.val(); // save the snapshot item value in a local variable
      record.loaded = true; // add the property
      this.newArray.push(record); // push it to your array that'll  be used in ng-for
      return true; // return true to foreach
    });
  }
});

Hope this helps.

Hope you can try as shown below. You can store your placeholder image inside the assets/images folder.

.html

<ion-card *ngFor="let item of items">
 <img [src]="item?.picture!= null ? item.picture : myImgUrl" />
</ion-card>

.ts

export class MyPage{
   myImgUrl:stirng='./assets/images/myimage.png;

   constructor(){}
}

Try this:

.ts

export class MyPage {

  placeHolder:string = './assets/img/placeholder.png';

  constructor(){}
}

.html

   <img [src]=item.image ? item.image : placeHolder>
Related