link element onload
Asked Answered
H

11

53

Is there anyway to listen to the onload event for a <link> element?

F.ex:

var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';

link.onload = link.onreadystatechange = function(e) {
    console.log(e);
};

This works for <script> elements, but not <link>. Is there another way? I just need to know when the styles in the external stylesheet has applied to the DOM.

Update:

Would it be an idea to inject a hidden <iframe>, add the <link> to the head and listen for the window.onload event in the iframe? It should trigger when the css is loaded, but it might not guarantee that it's loaded in the top window...

Harappa answered 20/6, 2010 at 8:6 Comment(1)
Similar question here: #796330Voiceless
G
9

This is kind of a hack, but if you can edit the CSS, you could add a special style (with no visible effect) that you can listen for using the technique in this post: http://www.west-wind.com/weblog/posts/478985.aspx

You would need an element in the page that has a class or an id that the CSS will affect. When your code detects that its style has changed, the CSS has been loaded.

A hack, as I said :)

Grigri answered 20/6, 2010 at 8:29 Comment(0)
E
26

Today, all modern browsers support the onload event on link tags. So I would guard hacks, such as creating an img element and setting the onerror:

if !('onload' in document.createElement('link')) {
  imgTag = document.createElement(img);
  imgTag.onerror = function() {};
  imgTag.src = ...;
} 

This should provide a workaround for FF-8 and earlier and old Safari & Chrome versions.

minor update:

As Michael pointed out, there are some browser exceptions for which we always want to apply the hack. In Coffeescript:

isSafari5: ->
  !!navigator.userAgent.match(' Safari/') &&
      !navigator.userAgent.match(' Chrom') &&
      !!navigator.userAgent.match(' Version/5.')

# Webkit: 535.23 and above supports onload on link tags.
isWebkitNoOnloadSupport: ->
  [supportedMajor, supportedMinor] = [535, 23]
  if (match = navigator.userAgent.match(/\ AppleWebKit\/(\d+)\.(\d+)/))
    match.shift()
    [major, minor] = [+match[0], +match[1]]
    major < supportedMajor || major == supportedMajor && minor < supportedMinor
Excursive answered 28/11, 2012 at 16:45 Comment(5)
Can you explain how an img element helps? Btw the latest version of Chrome still doesn't have working onload for link elements.Dustan
The onload does work for me in latest Chrome & Safari. For browsers that do not support onload, you could create an IMG element, set the src to the stylesheet. The onerrer of the img will be triggered when the file is loaded. By the way, I just noticed that 'onload' in document.createElement('link') returns true even for Safari 5, which does not support the load event.Excursive
That won't work. The onerror will be fired as soon as a response is received from the server. That happens some time before the css file is fully received. The time difference is the receiving time which depends on the file size. In other words, you are just detecting when the server response is received and not when the css load finishes.Seeto
The guard does not seem to work: I am finding 'onload' in the link element of an older browser that does not fire the link onload event.Pretension
@Pretension correct, Safari 5 and Webkit below 535.23 are exceptions.Excursive
G
9

This is kind of a hack, but if you can edit the CSS, you could add a special style (with no visible effect) that you can listen for using the technique in this post: http://www.west-wind.com/weblog/posts/478985.aspx

You would need an element in the page that has a class or an id that the CSS will affect. When your code detects that its style has changed, the CSS has been loaded.

A hack, as I said :)

Grigri answered 20/6, 2010 at 8:29 Comment(0)
T
4

The way I did it on Chrome (not tested on other browsers) is to load the CSS using an Image object and catching its onerror event. The thing is that browser does not know is this resource an image or not, so it will try fetching it anyway. However, since it is not an actual image it will trigger onerror handlers.

var css = new Image();
css.onerror = function() {
    // method body
}
// Set the url of the CSS. In link case, link.href
// This will make the browser try to fetch the resource.
css.src = url_of_the_css;

Note that if the resource has already been fetched, this fetch request will hit the cache.

Tapestry answered 13/11, 2012 at 2:36 Comment(0)
B
4

E.g. Android browser doesn't support "onload" / "onreadystatechange" events for element: http://pieisgood.org/test/script-link-events/
But it returns:

"onload" in link === true

So, my solution is to detect Android browser from userAgent and then wait for some special css rule in your stylesheet (e.g., reset for "body" margins).
If it's not Android browser and it supports "onload" event- we will use it:

var userAgent = navigator.userAgent,
    iChromeBrowser = /CriOS|Chrome/.test(userAgent),
    isAndroidBrowser = /Mozilla\/5.0/.test(userAgent) && /Android/.test(userAgent) && /AppleWebKit/.test(userAgent) && !iChromeBrowser; 

addCssLink('PATH/NAME.css', function(){
    console.log('css is loaded');
});

function addCssLink(href, onload) {
    var css = document.createElement("link");
    css.setAttribute("rel", "stylesheet");
    css.setAttribute("type", "text/css");
    css.setAttribute("href", href);
    document.head.appendChild(css);
    if (onload) {
        if (isAndroidBrowser || !("onload" in css)) {
            waitForCss({
                success: onload
            });
        } else {
            css.onload = onload;
        }
    }
}

// We will check for css reset for "body" element- if success-> than css is loaded
function waitForCss(params) {
    var maxWaitTime = 1000,
        stepTime = 50,
        alreadyWaitedTime = 0;

    function nextStep() {
        var startTime = +new Date(),
            endTime;

        setTimeout(function () {
            endTime = +new Date();
            alreadyWaitedTime += (endTime - startTime);
            if (alreadyWaitedTime >= maxWaitTime) {
                params.fail && params.fail();
            } else {
                // check for style- if no- revoke timer
                if (window.getComputedStyle(document.body).marginTop === '0px') {
                    params.success();
                } else {
                    nextStep();
                }
            }
        }, stepTime);
    }

    nextStep();
}

Demo: http://codepen.io/malyw/pen/AuCtH

Borneo answered 21/1, 2014 at 10:2 Comment(0)
G
2

Since you didn't like my hack :) I looked around for some other way and found one by brothercake.

Basically, what is suggested is to get the CSS using AJAX to make the browser cache it and then treat the link load as instantaneous, since the CSS is cached. This will probably not work every single time (since some browsers may have cache turned off, for example), but almost always.

Grigri answered 23/6, 2010 at 19:35 Comment(1)
That is not a bad idea, but I need to support local files (XMLHttpRequest does not support local files in FF 3+)Harappa
L
2
// this work in IE 10, 11 and Safari/Chrome/Firefox/Edge
// if you want to use Promise in an non-es6 browser, add an ES6 poly-fill (or rewrite to use a callback)

let fetchStyle = function(url) {
  return new Promise((resolve, reject) => {
    let link = document.createElement('link');
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.onload = resolve;
    link.href = url;

    let headScript = document.querySelector('script');
    headScript.parentNode.insertBefore(link, headScript);
  });
};
Leadsman answered 2/12, 2016 at 14:3 Comment(0)
S
1

The xLazyLoader plugin fails since the cssRules properties are hidden for stylesheets that belong to other domains (breaks the same origin policy). So what you have to do is compare the ownerNode and owningElements.

Here is a thorough explanation of what todo: http://yearofmoo.com/2011/03/cross-browser-stylesheet-preloading/

Sawyers answered 24/3, 2011 at 16:41 Comment(1)
I know this is old, but you have my vote, more useful than most other answers actually. Too bad you did't include part of the working code (because even if the blog posts still exists, the link is now dead, it leads to the home page, probably because of https).Ribaldry
B
1

Another way to do this is to check how many style sheets are loaded. For instance:

With "css_filename" the url or filename of the css file, and "callback" a callback function when the css is loaded:

var style_sheets_count=document.styleSheets.length;
var css = document.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('type', 'text/css');
css.setAttribute('href', css_filename);
document.getElementsByTagName('head').item(0).appendChild(css);
include_javascript_wait_for_css(style_sheets_count, callback, new Date().getTime());

function include_javascript_wait_for_css(style_sheets_count, callback, starttime)
/* Wait some time for a style sheet to load.  If the time expires or we succeed
 *  in loading it, call a callback function.
 * Enter: style_sheet_count: the original number of style sheets in the
 *                           document.  If this changes, we think we finished
 *                           loading the style sheet.
 *        callback: a function to call when we finish loading.
 *        starttime: epoch when we started.  Used for a timeout. 12/7/11-DWM */
{  
   var timeout = 10000; // 10 seconds
   if (document.styleSheets.length!=style_sheets_count || (new Date().getTime())-starttime>timeout)
      callback();
   else
      window.setTimeout(function(){include_javascript_wait_for_css(style_sheets_count, callback, starttime)}, 50);
}
Barchan answered 7/12, 2011 at 16:39 Comment(0)
H
0

You either need a specific element which style you know, or if you control the CSS file, you can insert a dummy element for this purpose. This code will exactly make your callback run when the css file's content is applied to the DOM.

// dummy element in the html
<div id="cssloaded"></div>

// dummy element in the css
#cssloaded { height:1px; }

// event handler function
function cssOnload(id, callback) {
  setTimeout(function listener(){
    var el = document.getElementById(id),
        comp = el.currentStyle || getComputedStyle(el, null);
    if ( comp.height === "1px" )
      callback();
    else
      setTimeout(listener, 50);
  }, 50)
}

// attach an onload handler
cssOnload("cssloaded", function(){ 
  alert("ok"); 
});

If you use this code in the bottom of the document, you can move the el and comp variables outside of the timer in order to get the element once. But if you want to attach the handler somewhere up in the document (like the head), you should leave the code as is.

Note: tested on FF 3+, IE 5.5+, Chrome

Hammonds answered 28/6, 2010 at 23:44 Comment(5)
Why were you down/up/down/up voting everytime? To remove a vote, just click the same orange arrow again.Gambetta
Peter said: "Sounds like this is exactly what the OP needs". The OP said, no it's not what I need. +1 from Peter, -1 from me...Hammonds
I don't care about that, just trying to be helpful, but you were first downvoting it and then upvoting it and then downvoting it again within the same minute (and maybe once more thereafter). I just had a "wtf"? :)Gambetta
Fine, but why did you downvote Peter's post as well which basically answers the same as you? Is it gaming/gambling? I find it honestly said unfair and egocentric ;)Gambetta
because he offered an overcomplicated jQuery solution, which is bloated for a simple problem and may not be cross browser. It's all about quality. The reason there is an up/down vote is to promote preferred solutions. I'm only trying to help the OP, to get the best one.Hammonds
A
0

This trick is borrowed from the xLazyLoader jQuery plugin:

var count = 0;

(function(){
  try {
    link.sheet.cssRules;
  } catch (e) {
    if(count++ < 100)
      cssTimeout = setTimeout(arguments.callee, 20);
    else
      console.log('load failed (FF)');
    return;
  };
  if(link.sheet.cssRules && link.sheet.cssRules.length == 0) // fail in chrome?
    console.log('load failed (Webkit)');
  else
    console.log('loaded');
})();

Tested and working locally in FF (3.6.3) and Chrome (linux - 6.0.408.1 dev)

Demo here (note that this won't work for cross-site css loading, as is done in the demo, under FF)

Airdrome answered 29/6, 2010 at 7:58 Comment(11)
older browsers don't have try-catch. arguments.callee is no longer part of the language. No test in IE???Hammonds
Nope. IE doesn't run on my operating system, thank god (or takes more effort than I'm willing to put in). For the record tho I think the onload/onreadystatechange events do work in IE. And I didn't see any requirement that it work in browsers that old.Airdrome
And callee was deprecated only as a property of Function.arguments in js 1.4, not as a property of the function's local arguments variable.Airdrome
your link is a browser vendor's site about an older version, not the current language spcification. let me help you out. The new version (since 2009 December): ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdfHammonds
Yes. It's one of many links that say the same thing. The document you linked to says accessing callee will throw an error in struct mode only. That is very different to it 'not being part of the language'.Airdrome
"strict mode is the future of the language", as Douglas Crockford (memeber of the ecma committee) stated. it's not forced because it would break most of the sites right now. But the new version called Harmony will be based on the strict mode, and will definitely break your site. Consider the future. The version you are writing in is from 1999... Just to be clear.Hammonds
In fact that document states, 'The "callee" property has a more specific meaning for non-strict mode functions...' You'd have to read and understand section 10.6 to know what that meaning is.Airdrome
arguments.callee was removed for good reason, because it's not safe. also there is a simple solution for replacing arguments.callee, so...Hammonds
Ok. So to be clear, it is still part of the language, and some people think it might not be one day. Cool.Airdrome
you should care, who you call "some people"... papa corckford will kick your ass.. crockfordfacts.com :))Hammonds
@galambalazs - 5 years later, and strict mode is still optional :-PWailoo
A
0

This is a cross-browser solution

// Your css loader
var d = document,
    css = d.head.appendChild(d.createElement('link'))

css.rel = 'stylesheet';
css.type = 'text/css';
css.href = "https://unpkg.com/[email protected]/css/tachyons.css"

// Add this  
if (typeof s.onload != 'undefined') s.onload = myFun;
} else {
    var img = d.createElement("img");
    img.onerror = function() {
      myFun();
      d.body.removeChild(img);
    }
    d.body.appendChild(img);
    img.src = src;
}

function myFun() {
    /* ..... PUT YOUR CODE HERE ..... */
}

The answer is based on this link that say:

What happens behind the scenes is that the browser tries to load the CSS in the img element and, because a stylesheet is not a type of image, the img element throws the onerror event and executes our function. Thankfully, browsers load the entire CSS file before determining its not an image and firing the onerror event.

In modern browsers you can do css.onload and add that code as a fallback to cover old browsers back to 2011 when only Opera and Internet Explorer supported the onload event and onreadystatechange respectively.

Note: I have answered here too and it is my duplicate and deserves to be punished for my honesteness :P

Aldosterone answered 26/5, 2019 at 3:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.