15

I have this angular project where I have a big background image that fills the page and a simple sidebar with links that when clicked, will change the url of the background with another image (from a cdn). Since these images are fairly big they take a second or two to load and it's noticeable, I want to add a preloader but I'm not sure how that would be done in angular 2.

In my html I have this:

<section class="fullsizebg image-bg" [ngStyle]="{'background-image': 'url(' + urlImage + ')'}"></section>

The variable urlImage is populated in the constructor of the component and the sidebar links changes the value of it on click with a simple function like so:

generateImage(data: any){
    this.urlImage = 'http://example.com/mycdn/'+this.data.url+'.jpg';
}

So the url is in fact changed instantly but the image takes a bit to load. I'd like to add a loading gif or something like that to keep the image change smooth to the user and not jumpy like it is now.

Supamiu
  • 8,501
  • 7
  • 42
  • 76
Elaine Marley
  • 2,143
  • 6
  • 50
  • 86

5 Answers5

19

One way to do it would be to use Blob to get the image and store it in an img component, this way you have your hands on the loading process and you can add your loading gif:

@Component({
   selector:'loading-image',
   template:'<img alt="foo" [src]="src"/>'
})
export class ExampleLoadingImage{

   public src:string = "http://example.com/yourInitialImage.png";

   constructor(private http:Http){}

   generateImage(data: any): void {
      this.src = 'http://www.downgraf.com/wp-content/uploads/2014/09/01-progress.gif'; //Just a random loading gif found on google.
      this.http.get('http://example.com/mycdn/'+this.data.url+'.jpg')
         .subscribe(response => {
            let urlCreator = window.URL;
            this.src = urlCreator.createObjectURL(response.blob());
         });
    }
}

N.B: You should type your data parameter, typing is a good way to ensure consistency over your code, any should only be used as a joker, like Object in Java.

slaesh
  • 16,659
  • 6
  • 50
  • 52
Supamiu
  • 8,501
  • 7
  • 42
  • 76
  • I'm going to try that but it will break my css to make the background size be cover, I'm not sure how to do that in angular, I've always used jquery for that. – Elaine Marley Sep 23 '16 at 13:12
  • you should look for css to make the img a fixed background, because the only way to achieve what you want is to do it this way, using blob. – Supamiu Sep 23 '16 at 14:42
  • Ok, this is more important than the background issue, I will see how I sort that, will try your approach – Elaine Marley Sep 23 '16 at 15:35
  • I get this: platform-browser.umd.js:1900 EXCEPTION: "blob()" method not implemented on Response superclass – Elaine Marley Sep 25 '16 at 09:01
  • 1
    oh seems like you're not using angular final release (2.0.X) – Supamiu Sep 25 '16 at 09:56
  • I used the version that was on the tutorial in the angular site a couple months ago when I started this project. I think it was 2.0.0-rc.4 – Elaine Marley Sep 25 '16 at 10:17
  • You should definetly upgrade to final release. check changelog for module migration and everything related to rc5+ – Supamiu Sep 25 '16 at 13:43
  • On rc5 I get this with your code: EXCEPTION: Error: The request body isn't either a blob or an array buffer – Elaine Marley Sep 25 '16 at 16:34
  • @ElaineMarley You need to add `responseType` to your http request, I wrote more about it here: http://stackoverflow.com/questions/37046133/pdf-blob-is-not-showing-content-angular-2/39657478#39657478 – Stefan Svrkota Sep 25 '16 at 20:08
  • how can i make it directive? i have n image box, that each load from cloud server, if it fail to load it should rollback to the server it self, if server fails too (it may be caching problem so i would load the original file from cloud/server) ... i'll provide an method for it within my model class, and the directive should use them for loading images and controll failover... – Hassan Faghihi Jul 18 '19 at 13:00
13

This solution leverages what angular and the browser already provide. The image loading is done by the browser and there's no need to mess with any data or the DOM yourself.

I have tested this on Chrome 53 and it's working flawlessly.

This is your element, that's getting its background changed:

<div class="yourBackgroundClass" [style.background-image]="'url(' + imgUrl + ')'"></div>

To prefetch the image, we use an image tag, that is not shown. It'd be good to additionally make its position: absolute and move it out of the view or make it really tiny, so that it can't interfere with your actual content.

<img [src]="imgPreloadUrl" (load)="imgUrl = imgPreloadUrl" hidden>

Through setting imgPreloadUrl, the img's src is updated by angular and the browser loads the image into the invisible img tag. Once it's done, onload fires and we set imgUrl = imgPreloadUrl. Angular now updates style.background-image of the actual background and the background image switches immediately, because it's already loaded in the hidden image.

While imgUrl !== imgPreloadUrl we can show a spinner to indicate loading:

<div class="spinner" *ngIf="imgUrl !== imgPreloadUrl"></div>

test with:

<button (click)="imgPreloadUrl = 'https://upload.wikimedia.org/wikipedia/commons/2/24/Willaerts_Adam_The_Embarkation_of_the_Elector_Palantine_Oil_Canvas-huge.jpg'">test</button>
j2L4e
  • 6,914
  • 34
  • 40
8

Using Image Object ( Plunker Demo &neArr; )

tmpImg: HTMLImageElement; // will be used to load the actual image before showing it

generateImage(data: any){
 this.urlImage = 'http://example.com/mycdn/'+ 'loading_GIF_url';  // show loading gif

 let loaded = () => { // wait for image to load then replace it with loadingGIF
   this.urlImage = 'http://example.com/mycdn/' + this.data.url+'.jpg';
 }

 // background loading logic
 if(this.tmpImg){
   this.tmpImg.onload = null; // remove the previous onload event, if registered
 }
 this.tmpImg = new Image();
 this.tmpImg.onload = loaded;  // register the onload event
 this.tmpImg.src = 'http://example.com/mycdn/'+this.data.url+'.jpg';
}
Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
1

There are few things that you needed like a http, resolver and a sanitizer. here's a link to that explaining how to implement it from scratch.

So for example you have a request then transforming the returned blob into a safe style for us to able to use it in our style directive

this.http.get('assets/img/bg.jpg', { responseType: 'blob' }).pipe(
  map( image => {
    const blob: Blob = new Blob([image], { type: 'image/jpeg' });
    const imageStyle = `url(${window.URL.createObjectURL(blob)})`;
    return this.sanitizer.bypassSecurityTrustStyle(imageStyle);
  })
)
Riyenz
  • 2,498
  • 2
  • 9
  • 23
  • Would you be able to explain how this can be used in context to the question? – user3836415 Apr 04 '19 at 01:33
  • @user3836415 It was a suggestion that we could load the image first using a resolver so that we can avoid late load of the image most likely if its a background image of a landing page. – Riyenz Apr 04 '19 at 01:46
0

I recently implemented this as a structural directive:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appPreloadImage]'})
export class PreloadImageDirective {

  @Input("appPreloadImage") imageUrl : string;

  constructor( private templateRef : TemplateRef<any>,
               private viewContainer : ViewContainerRef) {
  }

  private showView() {
    this.viewContainer.createEmbeddedView(this.templateRef);
  }

  ngOnInit() {
    var self = this;
    self.viewContainer.clear();
    var tmpImg = new Image();
    tmpImg.src = self.imageUrl;
    tmpImg.onload = function() {
        self.showView();
    }
    tmpImg.onerror = function() {
        self.showView();
    }
  }
}

You can use it like this:

<div *appPreloadImage="'/url/of/preloaded/image.jpg'">
  <!-- Nothing in here will be displayed until the 
       request to the URL above has been completed 
      (even if that request fails) -->
</div>

(Note the single quotes nested inside the double quotes - it's because we're passing a string literal.)

Chris HG
  • 1,412
  • 16
  • 20