4

Is it possible to bind an onload event to each image, declaring it once? I tried, but can't manage to get it working... (this error is thrown: Uncaught TypeError: Illegal invocation)

HTMLImageElement.prototype.onload = function()
{
        console.log(this, "loaded");
};

P.S: I also tried returning this, but doesn't seem to be the issue here... any suggestions / explanations on why my current code isn't working?

Bilal075_
  • 153
  • 7
  • 5
    No. Don't mess with builtin prototypes. – Bergi Aug 21 '16 at 16:14
  • http://www.ncyoung.com/?p=194 – mplungjan Aug 21 '16 at 16:15
  • @Bergi It's an empty built-in prototype. It's an event listener, it's made for custom boundations of custom functions. – Bilal075_ Aug 21 '16 at 16:17
  • @mplungjan That's interesting, thanks for sharing! – Bilal075_ Aug 21 '16 at 16:19
  • @Bilal075_ is does not matter if `onload` exists for `HTMLImageElement.prototype` or not. You should never add custom functionality by modifying the prototype of native objects, and should avoid to do so for objects that you don't own (e.g. of foreign libraries), except if they give you a defined why how you should do it. – t.niese Aug 21 '16 at 16:23

4 Answers4

3

You can't set a handler on the prototype, no.

In fact, I'm not aware of any way to get a proactive notification for image load if you haven't hooked load on the specific image element, since load doesn't bubble.

I only two know two ways to implement a general "some image somewhere has loaded" mechanism:

  1. Use a timer loop, which is obviously unsatisfying on multiple levels. But it does function. The actual query (document.getElementsByTagName("img")) isn't that bad as it returns a reference to the continually updated (live) HTMLCollection of img elements, rather than creating a snapshot like querySelectorAll does. Then you can use Array.prototype methods on it (directly, to avoid creating an intermediary array, if you like).

  2. Use a mutation observer to watch for new img elements being added or the src attribute on existing img elements changing, then hook up a load handler if their complete property isn't true. (You have to be careful with race conditions there; the property can be changed by the browser even while your JavaScript code is running, because your JavaScript code is running on a single UI thread, but the browser is multi-threaded.)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks for your answer. I tried setting up a mutation Observer, but doesn't seem to be picking up the 'static' elements (the one which are written in the document itself). I've read about the mutation Observer, and is actually designed to detect changes in the current DOM. – Bilal075_ Aug 22 '16 at 07:46
  • @Bilal075_: Markup in the HTML becomes elements in the DOM (that's its purpose). If you went with the mutation observer approach, you'd need to start with a check for elements that already exist, and then use the observer to watch for new `img` elements or `img` elements whose `src` changes. – T.J. Crowder Aug 22 '16 at 07:50
  • But how am I supposed to add onload listeners *(programmatically)* to these elements that have been in the HTML'S *'markup'* from the start? – Bilal075_ Aug 22 '16 at 08:04
  • @Bilal075_: `document.getElementsByTagName("img")` gives you an `HTMLCollection` of them. Call that from code included in a `script` tag at the end of `body`, just before the closing `

    ` tag and setup handlers on the ones that aren't complete yet (some of them already will be), allowing for the race condition I mentioned above. Or if you want to catch them as they're added by the parser, set up the mutation observer in a `script` tag at the **top** of `body`, before any `` tags are processed.

    – T.J. Crowder Aug 22 '16 at 08:13
  • Sorry for not mentioning your name, stackoverflow simply won't let me. But back to our discussion. My script is already set in the ** tag, and is called as soon as the document has a **readystate** *interactive*. This basically means *that whenever the document is ready to interact with the client, he can / will*. As soon as this ready state hits, this function is called, and selects (as you mentioned as well) all the elements with the **IMG** tag. But setting an event listener on this collection, simply won't work. I don't know what i'm doing wrong. Have the images been loaded already? – Bilal075_ Aug 22 '16 at 08:17
  • @Bilal075_: Can't help you with code I cannot see. :-) At this point we're well away from the original question. I suggest creating a [mcve] of the code you're having trouble with and posting it as a question. – T.J. Crowder Aug 22 '16 at 08:22
2

You get that error because onload is an accessor property defined in HTMLElement.prototype.

You are supposed to call the accessor only on HTML elements, but you are calling the setter on HTMLImageElement.prototype, which is not an HTML element.

If you want to define that function, use defineProperty instead.

Object.defineProperty(HTMLImageElement.prototype, 'onload', {
  configurable: true,
  enumerable: true,
  value: function () {
    console.log(this, "loaded");
  }
});
var img = new Image();
img.onload();

Warning: Messing with builtin prototypes is bad practice.

However, that only defines a function. The function won't be magically called when the image is loaded, even if the function is named onload.

That's because even listeners are internal things. It's not that, when an image is loaded, the browser calls the onload method. Instead, when you set the onload method, that function is internally stored as an event listener, and when the image is loaded the browser runs the load event listeners.

Instead, the proper way would be using Web Components to create a custom element:

var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
  var img = document.createElement('img');
  img.src = this.getAttribute('src');
  img.addEventListener('load', function() {
    console.log('loaded');
  });
  this.appendChild(img);  
};
document.registerElement('my-img', {prototype: proto});
<my-img src="/favicon.ico"></my-img>

There is not much browser support yet, though.

Oriol
  • 274,082
  • 63
  • 437
  • 513
  • As T.J mentions, won't work either. I'm trying to setup a default onload behavior for every single image in the document. Any workarounds then? – Bilal075_ Aug 21 '16 at 16:28
  • @Oriol In your scenario, it does. But i'm trying to bind this event to static image-elements too (so these that are written in the **HTML** document itself, but doesn't have the onload event(s) attached). Any idea on how I can make your snippet work to my fulfilling? – Bilal075_ Aug 21 '16 at 16:31
  • @Oriol: You're directly *calling* `onload`. If the OP knew when to do that, they wouldn't need to do that. Try it just setting a `src` on that `img` and see if you get called (you won't). – T.J. Crowder Aug 21 '16 at 16:32
  • 1
    @T.J.Crowder Maybe I didn't explain well. I am only fixing OP's code, which throws an error. But of course setting a function won't magically call it when a resource is loaded, even if you name it `onload`. – Oriol Aug 21 '16 at 16:35
  • @Oriol Thanks for your answer. Creating custom elements is unfortunately no option for me at the moment... But.. do you mind giving me an explanation of why setting the **onload** *(statically)* works, but defining it as a 'default' behaviour, won't? – Bilal075_ Aug 22 '16 at 07:43
  • @Bilal075_ Do you mean in the HTML source? That works because then it's parsed as an event handler content attribute, which sets the event handler to an internal raw uncompiled handler, which appends the event handler processing algorithm to the list of event listeners. Something similar happens when you call the setter of the `onload` IDL attribute on a HTMLElement instance. But defining a function in `HTMLImageElement.prototype` won't add any event listener. – Oriol Aug 22 '16 at 16:50
  • @Oriol Yes, exactly. Too bad that there is no *(crossbrowser)* work-around to 'auto'-attach this event to static / dynamically created elements. – Bilal075_ Aug 22 '16 at 18:35
1

This provides a notification for any image loading, at least in Opera (Presto) and Firefox (haven't tried any other browser). The script tag is placed in the HEAD element so it is executed and the event listener installed before any of the body content is loaded.

document.addEventListener('load', function(e) {
    if ((!e.target.tagName) || (e.target.tagName.toLowerCase() != 'img')) return;
    // do stuff here
}, true);

Of course, by changing the filtering on tagName it will also serve to respond to the loading of any other element that fires a load event, such as a script tag.

Witnobfigo
  • 11
  • 1
0

I've written something similar some time ago to check if an image is loaded or not, and if not, show a default image. You can use the same approach.

$(document).ready(function() {
    // loop every image in the page
    $("img").each(function() {
        // naturalWidth is the actual width of the image
        // if 0, img is not loaded
        // or the loaded img's width is 0. if so, do further check
        if (this.naturalWidth === 0) { // not loaded
            this.dataset.src = this.src; // keep the original src
            this.src = "image404.jpg";
        } else {
            // loaded
        }
    });
});
akinuri
  • 10,690
  • 10
  • 65
  • 102