Best way to preload images with Angular.js
Asked Answered
A

9

26

Angular's ng-src keeps previous model until it preloads image internally. I am using different image for the banner on each page, when I switch routes, i change main view, leaving header view as it is, just changing bannerUrl model when I have it.

This is resulting in seeing previous banner image while new one is loading.

I was surprised that there's no directive for it yet, but I wanted to make a discussion before trying to build one.

What I want to do I think is have banner model on custom attribute. like:

<img preload-src="{{bannerUrl}}" ng-src="{{preloadedUrl}}">

Then $scope.watch for bannerUrl change, and as soon as it changes, replace ng-src with loader spinner first, and then create temproary img dom element, preload image from preload-src and then assing it to preloadUrl.

Need to think how to handle multiple images too for galleries for example.

Does anyone have any input on it? or maybe someone can point me to existing code?

I've seen existing code on github that uses background-image - but that doesn't work for me as I need dynamic height/width as my app is responsive, and I cannot do it with background-image.

Thank you

Amperehour answered 7/1, 2014 at 23:31 Comment(2)
The usual work-around is to use CSS to set the image to visibility: hidden until the final image is loaded when you change it to visibility: visible and thus you only see the final image.Confiding
@Confiding so set all images to visibility:hidden, then use javascript to see when all images in array are loaded, and set all images to visibility:visible then?Amperehour
I
48

Having the 2 urls on the directive seems a touch overcomplicated. What I think is better is to write a directive that works like:

<img ng-src="{{bannerUrl}}" spinner-on-load />

And the directive can watch ng-src and (for example) set visibility:false with a spinner until the image has loaded. So something like:

scope: {
  ngSrc: '='
},
link: function(scope, element) {
  element.on('load', function() {
    // Set visibility: true + remove spinner overlay
  });
  scope.$watch('ngSrc', function() {
    // Set visibility: false + inject temporary spinner overlay
  });
}

This way the element behaves very much like a standard img with an ng-src attribute, just with a bit of extra behaviour bolted on.

http://jsfiddle.net/2CsfZ/47/

Impeachable answered 8/1, 2014 at 8:6 Comment(5)
Makes total sense, thank you. Will try this afternoon.Amperehour
Thank you sir. I have expanded your idea a bit further and it worked perfectly.Amperehour
the image in the jsfiddle is not working. Even if i use a image that works, it never removes the spinner.Laszlo
If you're wondering, the fiddle works with Angular 1.2 and above, which introduced some breaking changes jsfiddle.net/janklimo/de8ntcdvEstrella
Yeah ! Thanx @JanKlimo!Provenience
A
24

If anyone is interested this is my final solution: I use twitter bootstrap. So added class of "fade" to all images and just toggling class "in" with directive to fade in and out when image is loaded

angular.module('myApp').directive('imgPreload', ['$rootScope', function($rootScope) {
    return {
      restrict: 'A',
      scope: {
        ngSrc: '@'
      },
      link: function(scope, element, attrs) {
        element.on('load', function() {
          element.addClass('in');
        }).on('error', function() {
          //
        });

        scope.$watch('ngSrc', function(newVal) {
          element.removeClass('in');
        });
      }
    };
}]);

<img img-preload class="fade" ng-src="{{imgSrc}}">

Working example: http://ishq.org

Amperehour answered 10/1, 2014 at 14:6 Comment(1)
Appreciate the link to a working real-world example but can you separate the goodies into a Plunker or some other online sandbox test environment? ThanksGregg
S
3

If you want you can pass the image fail and image loader as attributes for the directive....

myApp.directive("mySrc", function() {
    return {
      link: function(scope, element, attrs) {
        var img, loadImage;
        var IMAGE_LOAD="123.jpg";
        var IMAGE_FAIL="123.jpg";
        img = null;

        loadImage = function() {

          element[0].src = IMAGE_LOAD;

          img  = new Image();
          img.src = attrs.mySrc;

          img.onload = function() {
            element[0].src = attrs.mySrc;
          };
          img.onerror=function ()
          {
              element[0].src = IMAGE_FAIL;
          }
        };

        loadImage();


      }
    };
  });
Shelia answered 10/12, 2014 at 6:35 Comment(0)
W
2

I think this is perhaps the most elegant solution because the directive actually creates the spinner and removes it automatically:

app.directive('spinnerLoad', [function spinnerLoad() {
    return {
        restrict: 'A',
        link: function spinnerLoadLink(scope, elem, attrs) {
            scope.$watch('ngSrc', function watchNgSrc() {
                elem.hide();
                elem.after('<i class="fa fa-spinner fa-lg fa-spin"></i>');  // add spinner
            });
            elem.on('load', function onLoad() {
                elem.show();
                elem.next('i.fa-spinner').remove(); // remove spinner
            });
        }
    };
}]);

Here is the html:

<img ng-src='{{imgUrl}}' spinner-load />

Note: you'll need to be using font-awesome for this to work as described here

Wandy answered 11/4, 2016 at 22:46 Comment(2)
A dumb question but how can I use the directive in a HTML template that has its own controller? I added the directive.js file in index.html but it shows typerrror (elem.hide & elem.show are not functions). Also is the scope a type ($scope)? Thanks :)Dagley
This isn't working for me. The watch function is called once, and when the src is updated it doesn't trigger the watch function.Schacker
C
1

Just to share ^^

 //css
.media-box{
                position: relative;
                width:220px;
                height: 220px;
                overflow: hidden;
            }
            .media-box div{
                position: absolute;
                left: 0;
                top: 0;
            }
            .spinner{
                position: absolute;
                left: 0;
                top: 0;
                background: #CCC url(./spinner.gif) no-repeat center center;
                display: block;
                width:220px;
                height: 220px;
            }
            .feed img.spinner-show{
                visibility: visible;
            }
            .feed img.spinner-hide{
                visibility: hidden;
            }

//html
<div class="media-box">
  <div>
    <img data-ng-src="{{item.media}}" alt="" title="" data-spinner-on-load>
  </div>
</div>

//js
.directive('spinnerOnLoad', function() {
            return {
                restrict: 'A',
                link: function(scope,element){
                    element.on('load', function() {
                        element.removeClass('spinner-hide');
                        element.addClass('spinner-show');
                        element.parent().find('span').remove();
                    });
                    scope.$watch('ngSrc', function() {
                        element.addClass('spinner-hide');
                        element.parent().append('<span class="spinner"></span>');
                    });      
                }
            }
        });
Cannonball answered 13/2, 2014 at 16:8 Comment(0)
R
1

Instead of using

element.on('load', function() {});

use imagesLoaded plugin. It will speed up dramatically your images.

So the final code would be:

link: function(scope, element) {
  imagesLoaded(element, function() {

  });
  scope.$watch('ngSrc', function() {

  });
}
Roswald answered 9/4, 2014 at 11:27 Comment(0)
R
0

I have this directive which shows a spinner when img-src changes:

<img-with-loading
      img-src="{{src}}"
      spinner-class="{{spinnerClass}}"
/>

Code here: http://jsfiddle.net/ffabreti/yw74upyr/

Rosina answered 8/9, 2015 at 1:37 Comment(0)
A
0

images can be preloaded on route change by using image-preloader factory and resolve:

// call REST
                    return getContent.get().$promise.then(function(response) {

                                //return response;

                                // preload images from response
                                var imageLocations = [
                                  // put image(s) from response to array
                                  response.PostImage.big[0],
                                ];

                                // check do we have (all) image(s) in array
                                console.log(imageLocations);

                                // return when all images are preloaded
                                return preloader.preloadImages( imageLocations )
                                .then(function() {

                                    //if it was success 
                                    return response;
                                },
                                function() {

                                        //if it failed 
                                    return response;
                                });

                            });

complete tutorial here: https://www.coditty.com/code/angular-preload-images-on-route-change-by-using-resolve

Adiaphorous answered 15/9, 2017 at 4:6 Comment(0)
A
-1

A simple solution I've found is to change the url to '//:0' before assigning it's new value

$scope.bannerUrl = 'initial value'; 

// When we want to change it
$scope.bannerUrl = '//:0';  // remove the previous img so it's not visible while the new one loads
$scope.bannerUrl = scope.preloadedUrl
Azrael answered 8/5, 2015 at 13:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.