14

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.

Sampath
  • 63,341
  • 64
  • 307
  • 441
TC Roberts
  • 185
  • 1
  • 2
  • 13
  • you can just give a default value to `item.picture` (your placeholder image src), and replace it with the actual value when the image is ready. – Kaddath Sep 29 '17 at 10:38
  • @Kaddath easier said than done :) Can you demonstrate your method please, so I can fully understand? Thanks! – TC Roberts Sep 29 '17 at 10:54
  • you can look at sampath's answer, i did almost the same thing in a previous project, except that i had a controller function as the src value (the function doing the same as him, checking if picture has a value and returning a default src if not). In my case i was downloading the pictures, and the async response just sets the value when the file path is known. The display should update automatically when value changes – Kaddath Sep 29 '17 at 12:01
  • Were you able to resolve this @TCRoberts? – sebaferreras Oct 04 '17 at 09:42
  • 1
    @sebaferreras not yet. I haven’t tested the methods. I’m getting to this portion shortly in my project. I had to take a few steps back in my project – TC Roberts Oct 04 '17 at 10:01

5 Answers5

29

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.

sebaferreras
  • 44,206
  • 11
  • 116
  • 134
  • 1
    How do you pass a second parameter to this directive? For example something like below, where you want to add another image in case of onError event: `` – Ari Jan 05 '18 at 04:23
  • 1
    @Ari You just need to add another `@Input('onErrorImage') errorImgSource: string;` in the directive, and then use it when there's an error: `this.downloadingImage.onerror = () => { this.finalImage = this.errorImgSource; }` – sebaferreras Jan 05 '18 at 07:24
  • 1
    dude you are amazing! – Jay Aug 19 '18 at 22:47
  • 1
    Thanks a million! This is neat and way too useful! – Gregordy Sep 12 '18 at 10:00
  • Can we do this for Ionic 1 as well? – Siyah Sep 12 '18 at 10:10
  • Cannot find module '../components/img-preload/img-preload'. – core114 Jan 25 '19 at 10:05
  • @sebaferreras Can't get this to work in Angular 9. Can't bind to 'img-preloader' since it isn't a known property of 'img'. I have done some fiddling with the selector but it still throws it out. Any help would be greatly appreciated as this looks like the best solution. Cheers. – fromage9747 Jun 16 '20 at 04:53
  • Great. But link to gif file is dead. Kindly update – Ojonugwa Jude Ochalifu Jan 24 '21 at 15:49
4

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

Duannx
  • 7,501
  • 1
  • 26
  • 59
2

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.

Gabriel Barreto
  • 6,411
  • 2
  • 24
  • 47
  • Yes, I can modify my JSON file. Here is my current JSON structure, how can I amend it to have the `loaded` boolean property? `"picture" : "my/picture/url"` – TC Roberts Oct 02 '17 at 11:33
  • @TCRoberts You have two options: You can either modify every value of your JSON in your API/Database, or you can do as in my code. When you get the response from your http call you can iterate it with a forEach loop and insert in each element the loaded property seting it to false. – Gabriel Barreto Oct 02 '17 at 12:30
  • @TCRoberts how do you get your JSON? Is it an http call? It's in a service? In an assets file? If you post how you're getting your JSON i can update my answer and show you how to modify. – Gabriel Barreto Oct 02 '17 at 12:31
  • Hi there, I'm getting my JSON data via Firebase. In my `ts file` I have my import `import firebase from 'firebase';` then in my `constructor` I have: `firebase.database().ref('myDataNode').on('value', snapshot => { this.myDataNode = snapshot.val(); });` and I've got the property `myDataNode` as `myDataNode = [];` under my page `export class` - you helped me with retreiving data from Firebase a while back (if you recall), so I'm still following you method with regards to retrieving the JSON data. Everything is fine there. – TC Roberts Oct 02 '17 at 12:45
  • Oh, sorry i don't recall helping you, but i'm gald i've helped. Updated my answer. – Gabriel Barreto Oct 02 '17 at 13:14
  • Thanks!!! I got it to work with your 1st solution: `firebase.database().ref('myDataNode').on('value', snapshot => { for(let key in snapshot.val()){ firebase.database().ref('myDataNode/' + key + '/loaded').set(false); } });` I have different firebase database nodes that I call on different pages in the app, but all from the same database. Should I then run this function on all the different pages BUT then just changing the node name as per your example, OR is there a way to just do it once for all the nodes in my database? PS: I'm declaring the code in my `.ts files` – TC Roberts Oct 16 '17 at 10:53
  • @TCRoberts glad it worked :D This first code you used is to be used only once, so execute it one time and then you can remove it from your code. If you have more than one node that has images you just need to change the `.ref()` in the code, execute it, see if it changed and do the same to other ref. If you have any code that saves a new image on your database you need to update it to also save a `loaded` property too. – Gabriel Barreto Oct 16 '17 at 10:59
  • I noticed that you updated your answer, in your own opinion, which one is the best method out of the 3? Secondly, I just want to make sure that I'm hearing you right, you are saying I should only run the code once and the code shouldn't sit in the app when I build it?? Images in the app will managed via a Firebase CRUD admin panel, so they'll constantly be changed and new images will be added. What am I to do re: setting the loaded property for this or its not necessary? Thanks. – TC Roberts Oct 16 '17 at 11:23
  • @TCRoberts in my opinion the first one is the best for you. Yes the code doesn't need to be on your app hen building, you just use it once so you don't need to update your database manually, so that's why you can use it to update all your nodes that have images as needed and then delete it. And in your Admin panel when saving a new image you just need to guarantee it'll also save the loaded property, so in your `.push()` or `.set()` method add `loaded` property to the object you're saving. – Gabriel Barreto Oct 16 '17 at 11:40
0

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(){}
}
Sampath
  • 63,341
  • 64
  • 307
  • 441
  • Will this work as expected? Since (i think) there'll be always an image the `item?.picture` will always be != null. Then it'll get the image url and start loading and the placeholer image will never be shown. – Gabriel Barreto Sep 29 '17 at 11:43
-2

Try this:

.ts

export class MyPage {

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

  constructor(){}
}

.html

   <img [src]=item.image ? item.image : placeHolder>
Ivan
  • 32
  • 2