131

The application I'm building requires my user to set 4 pieces of information before this image even has a chance of loading. This image is the center-piece of the application, so the broken image link makes it look like the whole thing is borked. I'd like to have another image take its place on a 404.

Any ideas? I'd like to avoid writing a custom directive for this.

I was surprised that I couldn't find a similar question, especially when the first question in the docs is the same one!

http://docs.angularjs.org/api/ng.directive:ngSrc

TylerH
  • 20,799
  • 66
  • 75
  • 101
will_hardin
  • 1,319
  • 2
  • 9
  • 5

13 Answers13

252

It's a pretty simple directive to watch for an error loading an image and to replace the src. (Plunker)

Html:

<img ng-src="smiley.png" err-src="http://google.com/favicon.ico" />

Javascript:

var app = angular.module("MyApp", []);

app.directive('errSrc', function() {
  return {
    link: function(scope, element, attrs) {
      element.bind('error', function() {
        if (attrs.src != attrs.errSrc) {
          attrs.$set('src', attrs.errSrc);
        }
      });
    }
  }
});

If you want to display the error image when ngSrc is blank you can add this (Plunker):

attrs.$observe('ngSrc', function(value) {
  if (!value && attrs.errSrc) {
    attrs.$set('src', attrs.errSrc);
  }
});

The problem is that ngSrc doesn't update the src attribute if the value is blank.

Jason Goemaat
  • 28,692
  • 15
  • 86
  • 113
  • Works well if the url is broken (404), but if it's an empty string ng-src silently swallows the error. – Stephen Patten Sep 18 '13 at 18:43
  • 2
    If an empty string is not valid for your model, then make it so that the expression you are binding to with ng-src does not return an empty string. – Justin Lovero Feb 21 '14 at 03:41
  • Updated script to support use of `src` attribute for the `err-src` image: http://plnkr.co/edit/b05WtghBOHkxKdttZ8q5 – Ryan Schumacher Feb 27 '14 at 19:26
  • @JustinLovero I consider it a bug in angular and reported it. The problem is that ngSrc will not set the src attribute if the value is blank. That could actually be a problem if for instance you are displaying a phone and load another phone with ajax that is missing an image url. The old phone's image would continue being displayed, but I think an error or placeholder would be more appropriate. Yes you could handle it in your model but the behavior of leaving an old image is, I think, more wrong than displaying an error. – Jason Goemaat Nov 27 '14 at 23:57
  • @JasonGoemaat, how would I do to replace the broken image with text? – simeg Jan 04 '15 at 14:47
  • Also works fine putting err-src value empty: `` – mggSoft May 15 '15 at 16:58
  • [Darren](http://stackoverflow.com/users/1678148/darren) made a good point: **"handle EVERY image img tag globally"**. In my opinion the ideal solution will handle this without need `err-src` at all. I would like a mix with both solutions. What do you think? – aUXcoder Jul 22 '15 at 20:06
  • Anyway I can get this to stop logging the 404s to the console? – Simon H Aug 02 '15 at 13:17
  • You can actually do this without watching anything, or without Angular or javascript... just wrap the image in an object tag first and it works as far back as IE8 (before Angular)... http://stackoverflow.com/questions/22051573/how-to-hide-image-broken-icon-using-only-css-html-without-js/29111371#29111371 – Nick Steele Apr 07 '16 at 19:55
48

Little late to the party, though I came up with a solution to more or less the same issue in a system I'm building.

My idea was, though, to handle EVERY image img tag globally.

I didn't want to have to pepper my HTML with unnecessary directives, such as the err-src ones shown here. Quite often, especially with dynamic images, you won't know if it's missing until its too late. Adding extra directives on the off-chance an image is missing seems overkill to me.

Instead, I extend the existing img tag - which, really, is what Angular directives are all about.

So - this is what I came up with.

Note: This requires the full JQuery library to be present and not just the JQlite Angular ships with because we're using .error()

You can see it in action at this Plunker

The directive looks pretty much like this:

app.directive('img', function () {
    return {
        restrict: 'E',        
        link: function (scope, element, attrs) {     
            // show an image-missing image
            element.error(function () {
                var w = element.width();
                var h = element.height();
                // using 20 here because it seems even a missing image will have ~18px width 
                // after this error function has been called
                if (w <= 20) { w = 100; }
                if (h <= 20) { h = 100; }
                var url = 'http://placehold.it/' + w + 'x' + h + '/cccccc/ffffff&text=Oh No!';
                element.prop('src', url);
                element.css('border', 'double 3px #cccccc');
            });
        }
    }
});

When an error occurs (which will be because the image doesn't exist or is unreachable etc) we capture and react. You can attempt to get the image sizes too - if they were present on the image/style in the first place. If not, then set yourself a default.

This example is using placehold.it for an image to show instead.

Now EVERY image, regardless of using src or ng-src has itself covered in case nothing loads up...

Darren Wainwright
  • 30,247
  • 21
  • 76
  • 127
  • 2
    very nice solution ! let's vote this one to the top ! – Matthijs Nov 12 '14 at 09:10
  • 1
    This is quite beautiful I have to say. I actually don't want any image to show at all if it doesn't resolve so I used: element.css("display", "none"); to just hide it totally – Pompey Feb 19 '15 at 19:30
  • 1
    nice :) or you could simply remove it from the DOM altogether, with element.remove(); :) – Darren Wainwright Feb 19 '15 at 20:40
  • Works great, Thanks! – ecorvo Jan 21 '16 at 14:00
  • Edited a bit your solution, so it works also when path is empty. http://stackoverflow.com/a/35884288/2014288 – andrfas Mar 09 '16 at 06:15
  • @Darren How can I handle the same in ng-srcset? I can see that in ng-src it will have my default image url only. However I've couple of different copies of the same image so I want to check if the srcset candidate picked up by browser is 404 then I want to load image myself with some centralize approach. – Jay Shukla Jul 14 '16 at 08:37
  • @JayShukla - not sure off the top of my head; don't fully understand what you're hoping to achieve. Perhaps best to write a new question. – Darren Wainwright Jul 14 '16 at 12:06
22

To expand Jason solution to catch both cases of a loading error or an empty source string, we can just add a watch.

Html:

<img ng-src="smiley.png" err-src="http://google.com/favicon.ico" />

Javascript:

var app = angular.module("MyApp", []);

app.directive('errSrc', function() {
  return {
    link: function(scope, element, attrs) {

      var watcher = scope.$watch(function() {
          return attrs['ngSrc'];
        }, function (value) {
          if (!value) {
            element.attr('src', attrs.errSrc);  
          }
      });

      element.bind('error', function() {
        element.attr('src', attrs.errSrc);
      });

      //unsubscribe on success
      element.bind('load', watcher);

    }
  }
});
user2899845
  • 1,181
  • 9
  • 13
  • 2
    Nice code, but seems an overkill to add a watch for what can be easily checked with ```ng-if``` in the template (the empty src string) – alonisser Dec 18 '13 at 09:35
  • Seems like you could just replace ngSrc with your own directive and have it set up the bind on error to use errSrc instead of having a separate errSrc directive if that's what you want. You could use `attrs.$observe('ngSrc', function(value) {...});`, that's what ngSrc uses internally instead of $watch – Jason Goemaat Jul 22 '14 at 22:08
  • The whole problem with blanks is because ngSrc doesn't update the src attribute if the value is blank, so if you had your own directive replacing ngSrc it wouldn't need a blank check. – Jason Goemaat Jul 22 '14 at 22:32
  • If the err-src return a 404, the code just keep cycling causing thousands of error in console.. Any idea? – Ivan Carosati Jan 20 '15 at 20:46
  • @Pokono - it may keep cycling if the link to the substitute image you provided isn't available. Does it help? – user2899845 Jan 22 '15 at 17:27
  • Yes, exactly. I believe that should stop though. Does anyone can think of a fix? – Ivan Carosati Jan 22 '15 at 19:43
  • 1
    @Pokono - you can check inside the error function whether the current 'src' attribute is the same as 'errSrc'. – user2899845 Jan 23 '15 at 13:04
  • If I am adding this to lot of images in a loop, isn't this expensive? – Biswas Khayargoli Jun 16 '17 at 06:30
  • 1
    @BiswasKhayargoli - added a call to unsubscribe, so it will not have any processing cost after the initial loading – user2899845 Jun 22 '17 at 21:56
11
App.directive('checkImage', function ($q) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            attrs.$observe('ngSrc', function (ngSrc) {
                var deferred = $q.defer();
                var image = new Image();
                image.onerror = function () {
                    deferred.resolve(false);
                    element.attr('src', BASE_URL + '/assets/images/default_photo.png'); // set default image
                };
                image.onload = function () {
                    deferred.resolve(true);
                };
                image.src = ngSrc;
                return deferred.promise;
            });
        }
    };
});

in HTML :

<img class="img-circle" check-image ng-src="{{item.profileImg}}" />
Zharf
  • 2,638
  • 25
  • 26
Mehul
  • 161
  • 2
  • 4
  • I feel this should be the accepted answer since it doesn't use jQuery and solves the problem – Gregra Jul 27 '16 at 12:48
10

If image is 404 or image is null empty whatever there is no need for directives you can simply use ng-src filter like this :)

<img ng-src="{{ p.image || 'img/no-image.png' }}" />
NeoNe
  • 1,429
  • 1
  • 11
  • 12
  • 2
    No, if p.image returns a 404 on an AJAX call, it will treat that as 'true' and not move on to the second img/no-image.png. – James Gentes Feb 12 '15 at 20:05
  • 2
    @JamesGentes - That doesn't seem to be the case. This use of ng-src works for me exactly as shown. And it's so much simpler. – shanemgrey Apr 16 '15 at 14:52
  • @shaneveeg thanks, that's interesting - I wonder if the back end is what's different. I am running Node with Express, perhaps it handles it differently. – James Gentes Apr 16 '15 at 18:50
  • 2
    @JamesGentes - Correction of my previous comment. The OP is looking for a solution when there is a valid URI returned by the model, but a 404 when it's followed. In that case, this solution does not work, it results in broken image. If angular returns nothing for the image value, this correctly chooses the fallback. – shanemgrey Apr 21 '15 at 14:28
  • Im using Laravel and it works perfect for me if 404, null or empty is the case. but maybe Express returns some kind of error instead of code response like 404 so yes probably depends of server framework – NeoNe May 11 '15 at 10:59
  • This may be correct when you have the image data directly (as it looks like it's doing: p.img and not p.imgUrl). But if you set the URL and server returns code 404 then this doesn't seem to work, not to me at least. I'm on IIS7 and it's returning an HTTP 404 standard response -> HTTP/1.1 404 Not Found – Ruben.Canton Aug 12 '15 at 11:04
  • This should be the accepted answer because it puts all the code into a single line. +1 because google searchers are looking for this answer, not the more complex overkill approaches. – CodeMed Oct 27 '15 at 18:10
9

I use something like this, but it assumes that team.logo is valid. It forces default if "team.logo" isn't set or is empty.

<img ng-if="team.logo" ng-src="https://api.example.com/images/{{team.logo}}">
<img ng-hide="team.logo" ng-src="img/default.png">
  • 2
    This is correct, as it evaluates "team.logo" as a string (true/false) whereas ng-src will see {{team.logo}} as true even if it returns a 404. – James Gentes Feb 12 '15 at 20:34
2

You don't need angular for that, or even CSS or JS. If you want, you can wrap this answer (linked) in a simple directive to make it simpler, like or something, but it's a pretty simple process... just wrap it in an object tag...

How to hide image broken Icon using only CSS/HTML (without js)

Community
  • 1
  • 1
Nick Steele
  • 7,419
  • 4
  • 36
  • 33
1

Is there a specific reason you can't declare the fallback image in your code?

As I understand, you have two possible cases for your image source:

  1. Correctly set pieces of information < 4 = Fallback image.
  2. Correctly set pieces of information == 4 = Generated URL.

I think this should be handled by your app - if the correct URL cannot currently be determined, instead pass a loading/fallback/placeholder image URL.

The reasoning is that you never have a 'missing' image, because you have explicitly declared the correct URL to display at any point in time.

Alex Osborn
  • 9,831
  • 3
  • 33
  • 44
  • Sorry, I should've clarified that even if all 4 values are set, the URL can still 404 (the user can add folders along the way.) In the meantime I've added a function that checks the URL's response headers when all values are set. It still would be nice to handle this without special treatment, I bet I'll come across this again :) – will_hardin May 01 '13 at 00:46
  • 1
    Cool, you should show that as an answer and mark it as accepted for anyone searching in future. – Alex Osborn May 01 '13 at 01:53
1

I suggest that you might like to use the Angular UI Utils 'if statement' directive to solve your problem, as found at http://angular-ui.github.io/. I have just used it to do exactly the same thing.

This is untested, but you could do something like:

Controller code:

$scope.showImage = function () {
    if (value1 && value2 && value3 && value4) { 
        return true;
    } else {
        return false;
    }
};

(or simpler)

$scope.showImage = function () {
    return value1 && value2 && value3 && value4;
};

HTML in View: <img ui-if="showImage()" ng-src="images/{{data.value}}.jpg" />

Or even simpler, you could just use a scope property:

Controller code:

$scope.showImage = value1 && value2 && value3 && value4;

HTML in View: <img ui-if="showImage" ng-src="images/{{data.value}}.jpg" />

For a placeholder image, just add another similar <img> tag but prepend your ui-if parameter with an exclamation (!) mark, and either make ngSrc have the path to the placeholder image, or just use a src tag as per normal ol' HTML.

eg. <img ui-if="!showImage" src="images/placeholder.jpg" />

Obviously, all of the above code samples are assuming that each of value1, value2, value3 and value4 will equate to null / false when each of your 4 pieces of information are incomplete (and thus also to a boolean value of true when they are complete).

PS. The AngularUI project has recently been broken in to sub-projects, and the documentation for ui-if seems to be missing currently (it's probably in the package somewhere though). However, it is pretty straightforward to use as you can see, and I have logged a Github 'issue' on the Angular UI project to point it out to the team too.

UPDATE: 'ui-if' is missing from the AngularUI project because it's been integrated in to the core AngularJS code! Only as of v1.1.x though, which is currently marked as 'unstable'.

Matty J
  • 3,116
  • 32
  • 31
1

Here's a solution I came up with using native javascript. I'm checking if the image is broken then adding a class to the image just in case and changing the source.

I got part of my answer from a Quora answer http://www.quora.com/How-do-I-use-JavaScript-to-find-if-an-image-is-broken

app.directive('imageErrorDirective', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            element[0].onerror = function () {
                element[0].className = element[0].className + " image-error";
                element[0].src = 'http://img3.wikia.nocookie.net/__cb20140329055736/pokemon/images/c/c9/702Dedenne.png';
            };
        }
    }
});
0

Came up with my own solution. It replaces image both if src or ngSrc is empty, and if img returns 404.

(fork of @Darren solution)

directive('img', function () {
return {
    restrict: 'E',        
    link: function (scope, element, attrs) {   
        if((('ngSrc' in attrs) && typeof(attrs['ngSrc'])==='undefined') || (('src' in attrs) && typeof(attrs['src'])==='undefined')) {
            (function () {
                replaceImg();
            })();
        };
        element.error(function () {
            replaceImg();
        });

        function replaceImg() {
            var w = element.width();
            var h = element.height();
            // using 20 here because it seems even a missing image will have ~18px width 
            // after this error function has been called
            if (w <= 20) { w = 100; }
            if (h <= 20) { h = 100; }
            var url = 'http://placehold.it/' + w + 'x' + h + '/cccccc/ffffff&text=No image';
            element.prop('src', url);
        }
    }
}
});
andrfas
  • 1,320
  • 2
  • 10
  • 16
0

This will allow only to loop twice, to check if the ng-src doesn't exist else use the err-src, this prevents the continues looping.

(function () {
    'use strict';
    angular.module('pilierApp').directive('errSrc', errSrc);

    function errSrc() {
        return {
            link: function(scope, element, attrs) {
             element.error(function () {
                // to prevent looping error check if src == ngSrc
                if (element.prop('src')==attrs.ngSrc){
                     //stop loop here
                     element.prop('src', attrs.errSrc);
                }              
            });
            }
        }
    }
})();
Pang
  • 9,564
  • 146
  • 81
  • 122
0

This is a solution to your problem Do this in your component.html

<img [src]="imageUrl" (error)="handleError()">

in your component.ts file

defaultImageUrl = '/assets/default-image.png';
imageUrl = 'https://example.com/actual-image.png';

create a function inside the component.ts file

handleError() {
   this.imageUrl = this.defaultImageUrl;
}

this will solve your issues for you