9

I'd like to know from the html below, if link[rel=import], link[rel=stylesheet], img and script are pending/loaded/failed/aborted without the need to add listeners beforehand and at any point after the event has happened

<!DOCTYPE html>
<html>
<head>
    <title>App</title>
    <meta charset="utf-8">
    <link rel="import" href="template-bundle.html">
    <link rel="stylesheet" href="bundle.css">
</head>
<body>
    <header><img src="logo.png" alt="App logo"></header>
    <!-- Boilerplate... -->
    <script src="./app-bundle.js"></script>
</body>
</html>

In other words: Is there an interface which provides something similar to a Bluebird's isPending(), isResolved(), isRejected() methods or a regular ES6 Promise?


Bonus question: Is this something can be achieved using a Service Worker?

Since SW can intercept requests and know their status, I was wondering if I can implement an API which returns a Promise that

  • is pending if request is still pending
  • is resolved if load event fired
  • is rejected if error or aborted was fired.

Thanks for the help


Update & Solution:

Thanks to the answers of @pritishvaidya and @guest271314, I was able to come up with a viable solution using MutationObserver that involves watching DOM for additions of resource nodes (link,img,script) and adding a promise to them that will resolve as described above

This works great, with the only caviat that the script tag needs to be inlined in <head> before any other resource. Here's an example

var resourceNodeSelector = 'link[href],script[src],img[src]';
function watchResource (n) {
    var url = n.href || n.src;

    if (!n.matches || !n.matches(resourceNodeSelector)) {
        return;
    }

    if (n.status) {
        return;
    }

    n.status = resourceObserver.promises[url] = new Promise(function (resolve, reject) {
        n.addEventListener('load', resolve);
        n.addEventListener('error', reject);
        n.addEventListener('abort', reject);
        n.addEventListener('unload', function (l) { delete resourceObserver.promises[url]} );
    });
    n.status.catch(function noop () {}); //catch reject so that it doesn't cause an exception
}

var resourceObserver = new MutationObserver(function (mutations) {
    document.querySelectorAll(resourceNodeSelector).forEach(watchResource);
});
resourceObserver.promises = {};
resourceObserver.observe(window.document, {childList: true, subtree: true});

Once the observer is in place, any valid resource element, should have a status promise property that you can check at any point in time

document.querySelector('link').status.then(linkLoaded).catch(linkFailed)

A more elegant solution, that does not involve using the expensive querySelectorAll, should be possible with ServiceWorker, since it can be programmed to intercept and keep track of all resource requests and their status

Dogoku
  • 4,585
  • 3
  • 24
  • 34
  • I'm not interested in the styles specifically. Any resource (link,script, img) that fires [UI Events](https://www.w3.org/TR/DOM-Level-3-Events/#events-uievent-types) would benefit from an interface like this. I mention ServiceWorkers, since they can intercept requests, they might be able to handle the status and provide an API similar to what I'm suggesting – Dogoku Oct 03 '16 at 07:17
  • 1
    i think i'll remove the stylesheet link, cause it's distracting people – Dogoku Oct 03 '16 at 07:18
  • 1
    Instead of removing the stylesheet link from your question, add all different kinds of loaded resources in your example. – trincot Oct 03 '16 at 07:38
  • 1
    @trincot i restructured the question to hopefully make more sense – Dogoku Oct 03 '16 at 10:49
  • May be you can refer this link, http://www.phpied.com/when-is-a-stylesheet-really-loaded/ – Yoganand Oct 06 '16 at 13:53
  • @Dogoku _"without the need to add listeners beforehand"_ Not certain what you mean? Is requirement to not utilize `onload` event of `` element? – guest271314 Oct 07 '16 at 01:39
  • @guest271314 Ideally yes, I'd like to avoid having to have an inline script with a load handler that I attach to all static resources (especially since the [UI events do not bubble](https://www.w3.org/TR/DOM-Level-3-Events/#events-uievent-types)). Instead I'm asking if there's an API that the browser can provide (or can be implemented with ServiceWorker) that can give me the status of a resource asynchronously – Dogoku Oct 07 '16 at 03:19
  • Dogoku Well, had composed a prospective Answer using `onload`, `onerror` before reading your last comment about `ServiceWorker` being a requirement. If that Answer does not provide expected result will possible attempt to compose a `ServiceWorker` solution. – guest271314 Oct 07 '16 at 03:25

2 Answers2

3

You can utilize onload, onerror events of <link> element; see Browser CSS/JS loading capabilities at right column.

Create an object to store status of all <link> requests and resolved or rejected Promise corresponding to the <link> element.

Reject Promise at onerror event; use .catch() chained to Promise.reject() to handle error so that Promise.all() will not stop processing resolved promises within array passed as parameter. You can also throw error from .catch() at onerror handler to Promise.all() if any rejected Promise should stop processing of resolved promise within array of promises.

At window.onload event handler, use Promise.all() to process all resolved links, using same function called before window.onload event. To wait for results of Promise.all() to be available, set src of last <script> element to bundle.js at .then() chained to Promise.all()

<!DOCTYPE html>
<html>

<head>
  <title>App</title>
  <meta charset="utf-8">
  <script>
    var handleLinks = {
      links: [],
      isPending: true
    };

    function handleBeforeLoad() {
      if (document.querySelectorAll("link").length === 0) {   
        console.log("links loading state is pending..", handleLinks.isPending);
      } else {
        handleLinks.isPending = false;
        Promise.all(handleLinks.links)
          .then(function(linksContent) {
            console.log("links resolved:", linksContent
                       , "links loading state is pending.."
                       , handleLinks.isPending);
            linksContent.filter(Boolean).forEach(function(link) {
              // `content` property : html `document`,  `CSSStyleSheet` 
              // requested at `<link>` element
              console.log(link); 
            });
            // load `bundle.js`
            document.getElementById("bundle")
            .src = "bundle.js"

          })
          .catch(function(err) {
            console.log("link error:", err.message)
          })
      }
    }
    handleBeforeLoad();
    window.onload = handleBeforeLoad;

    function handleLink(el) {
      handleLinks.links.push(Promise.resolve({
        content: el.import || el.sheet,
        type: el.type,
        rel: el.rel,
        href: el.href,
        integrity: el.integrity,
        isResolved: true
      }));

    }

    function handleLinkError(el) {
      handleLinks.links.push(Promise.reject(new Error(JSON.stringify({
        error: "error loading link",
        type: el.type,
        rel: el.rel,
        href: el.href,
        integrity: el.integrity,
        isRejected: true
      }))).catch(function(err) {
        // handle error
        console.log(err);
        // this will return a resolved Promise
        return "error requesting link " + el.href;
        // `throw err` here if any rejected Promise should
        // stop `Promise.all()` from handling resolved Promise
      }));

    }
  </script>
  <link onload="handleLink(this)" 
        onerror="handleLinkError(this)" 
        rel="import" 
        href="template-bundle.html" 
        type="text/html">
  <link onload="handleLink(this)" 
        onerror="handleLinkError(this)" 
        rel="stylesheet" 
        href="bundle.css" 
        type="text/css">
  <!-- this should throw error, file does not exist -->
  <link onload="handleLink(this)" 
        onerror="handleLinkError(this)" 
        rel="stylesheet" 
        href="bundles.css" 
        type="text/css">

  <body>
    <header><img src="" alt="App logo"></header>
    <!-- Boilerplate... -->
    <script id="bundle"></script>
  </body>

</html>

plnkr http://plnkr.co/edit/DQj9yTDcoQJj3h7rGp95?p=preview

guest271314
  • 1
  • 15
  • 104
  • 177
  • 1
    I like your thinking with the exception of the inline handlers (onload/onerror attributes). That gave me an idea of using a MutationObserver to detect link/img/script tag being added to the DOM and attaching the handlers that way. Also the document.querySelectorAll("link") in your beforeLoad function, will never return anything, since that part of the HTML is not yet parsed – Dogoku Oct 07 '16 at 03:30
  • _"Also the document.querySelectorAll("link") in your beforeLoad function, will never return anything, since that part of the HTML is not yet parsed"_ Yes, that is the expectation at that point in `document`; that is, using same function twice for different purposes. Event attributes can be substituted for `.addEventListener()`, where same result should occur. What is purpose of using `ServiceWorker` or `MutationObserver`? What exactly are you trying to achieve? – guest271314 Oct 07 '16 at 03:34
  • 1
    I'm trying to know if a link failed to load, without me having to explicitly add an onload/onerror attribute to the HTML. I would also like to be able to get this information after this event has happened. You gave me an idea of using MutationObserver to add those handlers on the fly, which should work, but perhaps ServiceWorkers would remove the need of handlers all together, since a SW can intercept all requests and can be programmed to provide a nice Promise-like API – Dogoku Oct 07 '16 at 03:40
  • @Dogoku Not certain how `MutationObserver` could assist in what you are trying to achieve? Though `ServiceWorker` should return expected result, without using event handlers. – guest271314 Oct 07 '16 at 03:48
  • A MutationObserver can inform me when a new link/img/script node has been added to the DOM ([example here](http://stackoverflow.com/a/13278380/424052)). Then I can add the listeners to the node using `addEventListener` and the rest is similar to what you are doing – Dogoku Oct 07 '16 at 03:56
  • Yes, am familiar with `MutationObserver`. You linked to an Answer which uses jQuery, where instead of `MutationObserver` you can delegate the event to the parent element using `$(parentElement).on("load", "link", function() {// do stuff})` – guest271314 Oct 07 '16 at 04:06
  • The jquery usage is irrelevant, i was just showing an example of MutationObserver. Anyway thanks for your help. I'm working on a POC now. I'll accept your answer once I have something working – Dogoku Oct 07 '16 at 04:08
0

yes you can check this in developer tool.Right click on web page and select inspect then window will open seleck network option and there you can check the status.

krpra
  • 466
  • 1
  • 4
  • 19