-
-
Save ebidel/1ba71473d687d0567bd3 to your computer and use it in GitHub Desktop.
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
body.loading #splash { | |
opacity: 1; | |
} | |
#splash { | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
transition: opacity 300ms cubic-bezier(0,0,0.2,1); | |
opacity: 0; | |
will-change: opacity; | |
z-index: 1; | |
background: url(...) no-repeat; | |
background-color: #E53935; | |
} | |
</style> | |
<!-- 1. Async HTML Imports do not block rending. Benefit of keeping it declarative | |
(instead of dynamically loading it later in JS) is that the parser can go | |
to town pre-fetching resources, etc. --> | |
<link rel="import" id="bundle" href="elements.html" async> | |
</head> | |
<!-- 2. Don't use <body unresolved>. It's a simple FOUC solution, but hides | |
the page until imports and Polymer are loaded. Intead, control FOUC manually with | |
a splash screen. --> | |
<body class="loading"> | |
<!-- 3. Light weight splash screen is outside of Polymer/imports and styled by | |
the main page. 1st paint is fast, even on polyfilled browsers. Alternatively, | |
one could create an "app shell" and style the page's un-upgraded elements | |
similar to their final upgraded version. --> | |
<div id="splash"></div> | |
<!-- Elements wait on the page and are upgraded when elements.html loads. --> | |
<paper-drawer-panel> | |
... | |
</paper-drawer-panel> | |
<script src="app.js" async></script> | |
</body> | |
</html> |
// 4. Conditionally load the webcomponents polyfill if needed by the browser. | |
// This feature detect will need to change over time as browsers implement | |
// different features. | |
var webComponentsSupported = ('registerElement' in document | |
&& 'import' in document.createElement('link') | |
&& 'content' in document.createElement('template')); | |
if (!webComponentsSupported) { | |
var script = document.createElement('script'); | |
script.async = true; | |
script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js'; | |
script.onload = finishLazyLoading; | |
document.head.appendChild(script); | |
} else { | |
finishLazyLoading(); | |
} | |
function finishLazyLoading() { | |
// (Optional) Use native Shadow DOM if it's available in the browser. | |
window.Polymer = window.Polymer || {dom: 'shadow'}; | |
// 6. Fade splash screen, then remove. | |
var onImportLoaded = function() { | |
var loadEl = document.getElementById('splash'); | |
loadEl.addEventListener('transitionend', loadEl.remove); | |
document.body.classList.remove('loading'); | |
// App is visible and ready to load some data! | |
}; | |
var link = document.querySelector('#bundle'); | |
// 5. Go if the async Import loaded quickly. Otherwise wait for it. | |
// crbug.com/504944 - readyState never goes to complete until Chrome 46. | |
// crbug.com/505279 - Resource Timing API is not available until Chrome 46. | |
if (link.import && link.import.readyState === 'complete') { | |
onImportLoaded(); | |
} else { | |
link.addEventListener('load', onImportLoaded); | |
} | |
} |
That's what this line is for: https://gist.github.com/ebidel/1ba71473d687d0567bd3#file-app-js-L38
Was var onImportLoaded = function() {} to avoid function hoisting?
Very useful, thanks!
An FYI to those who maybe ran into the same issue I did. If you're just testing this method out and you don't have anything in elements.html
yet, the script may fire the load
event before you set up the onImportLoaded
listener.
In this case, the splash screen will never go away! So, make sure you've at least got a couple of imports to load in elements.html
A way to figure out the transitionend
event name in various browser, the following transitionEndEventName
function can be used (I believe this is code from modernizr
):
// see http://stackoverflow.com/a/9090128/640539
function transitionEndEventName() {
var i;
var el = document.createElement('div');
var transitions = {
'transition': 'transitionend',
'OTransition': 'otransitionend',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd'
};
for (i in transitions) {
if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) {
return transitions[i];
}
}
}
I then use it as follows:
function finishLazyLoading() {
// 6. Fade splash screen, then remove.
var onImportLoaded = function() {
var loadEl = document.getElementById('splash');
var transitionEndEvt = transitionEndEventName();
loadEl.addEventListener(transitionEndEvt, loadEl.remove);
document.body.classList.remove('loading');
// App is visible and ready to load some data!
};
var link = document.querySelector('#bundle');
// 5. Go if the async Import loaded quickly. Otherwise wait for it.
// crbug.com/504944 - readyState never goes to complete until Chrome 46.
// crbug.com/505279 - Resource Timing API is not available until Chrome 46.
if (link.import && link.import.readyState === 'complete') {
onImportLoaded();
} else {
link.addEventListener('load', onImportLoaded);
}
}
Hope this helps someone.
@ebidel: I've played around with your original code as I had non-deterministic behaviour with regards to what external libraries, CSS files and/or other imports have been loaded at a certain stage. For example, sometimes jquery
was already present, sometimes it was not.
I believe the main issue is with this check and how this relates to the usage of the async
attribute. I'll try to explain:
- if the
elements.html
contains many files which they themselves include, for example, large javascript libraries, importing all those elements will take a while (let's say 5s) - however:
link.import.readyState === 'complete'
seems to be true even if the referenced scripts and other html imports have not yet been fully loaded, i.e. if you set a breakpoint at https://gist.github.com/ebidel/1ba71473d687d0567bd3#file-app-js-L38 and look in the network monitor in your debugging tool, many external files/CSS/javascripts etc. have not yet been loaded and are in statepending
(i.e. they're not yet200 OK
) - looking here, your own article and here, it appears that this is the expected behaviour
Given that, I figured an easy way to check (and make sure that all elements have successfully loaded using the .readyState
attribute) is to have another indirection of HTML import, i.e.:
- move all the needed imports in
elements.html
to a new file, e.g.imports.html
- import
imports.html
inelements.html
- check within
app.js
whether the single import element inelements.html
hasreadyState==='complete'
** this import, not beingasync
is only in statecomplete
when all dependent files have been loaded
index.html
<html lang="">
<head>
...
<!-- will be replaced with elements/elements.vulcanized.html -->
<link id="bundle" rel="import" href="elements/elements.html" async>
<!-- endreplace-->
</head>
<body class="loading fullbleed layout vertical">
...
<template is="dom-bind" id="app">
...
</template>
<!-- build:js scripts/app.js -->
<script type="text/javascript" src="scripts/app.js"></script>
<!-- endbuild-->
</body>
</html>
elements.html
<link id='allImports' rel="import" href="imports.html">
imports.html (example)
...
<link rel="import" href="../bower_components/external-deps/dependency-jquery.html">
<link rel="import" href="../bower_components/external-deps/dependency-materialize.html">
<link rel="import" href="../bower_components/external-deps/dependency-hammerjs.html">
<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html">
<link rel="import" href="../bower_components/iron-flex-layout/classes/iron-flex-layout.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
<link rel="import" href="../bower_components/iron-icons/av-icons.html">
<link rel="import" href="../bower_components/iron-pages/iron-pages.html">
<link rel="import" href="../bower_components/iron-selector/iron-selector.html">
...
app.js (only relevant snippets shown)
// Called when all components have been loaded.
// http://www.html5rocks.com/en/tutorials/webcomponents/imports/
// http://w3c.github.io/webcomponents/spec/imports/#fetching-import
// -> "Every import that is not marked as async delays the load event in the Document."
var onImportLoaded = function() {
console.log('All imports have been loaded, took ' + (new Date().getTime() - start) + 'ms');
initRouting();
installMaterializeCallbacks();
checkBrowser();
// remove the loading class so that app is now visible
document.body.classList.remove('loading');
};
// this method will only be called if webcomponents are supported natively by the browser
function waitUntilElementsFullyParsed() {
var link = document.querySelector('#bundle');
var allImportsDoc = link.import.querySelector('#allImports');
if (!allImportsDoc) {
console.log('Needed import element not yet in document, waiting 5ms.');
setTimeout(waitUntilElementsFullyParsed, 5);
} else {
console.log('Import document ready, continuing.');
// 5. Go if the async Import loaded quickly. Otherwise wait for it.
// crbug.com/504944 - readyState never goes to complete until Chrome 46.
// crbug.com/505279 - Resource Timing API is not available until Chrome 46.
if (allImportsDoc.import && allImportsDoc.import.readyState === 'complete') {
console.log('All components have already been loaded. Continuing initialization.')
onImportLoaded();
} else {
console.log('Not yet all components have been loaded. Waiting until done.');
allImportsDoc.addEventListener('load', onImportLoaded);
}
}
}
// See https://github.com/Polymer/polymer/issues/1381
window.addEventListener('WebComponentsReady', function() {
// imports are loaded and elements have been registered
console.log('WebComponentsReady event caught, took: ' + (new Date().getTime() - start) + 'ms');
onImportLoaded();
removeLoaderElement();
});
// make sure to install the event listener early enough that removes the splash div
var loadEl = document.getElementById('splash');
var transitionEndEvt = transitionEndEventName();
loadEl.addEventListener(transitionEndEvt, function() {
console.log('transitionend event caught, removing splash screen.');
removeLoaderElement();
});
// 4. Conditionally load the webcomponents polyfill if needed by the browser.
// This feature detect will need to change over time as browsers implement
// different features.
var webComponentsSupported = ('registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
console.log('Your browser does not support web components natively. Loading polyfill.');
var script = document.createElement('script');
script.async = true;
script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js';
document.head.appendChild(script);
} else {
console.log('Your browser supports web components natively, no polyfill needed.');
waitUntilElementsFullyParsed();
}
// see http://stackoverflow.com/a/9090128/640539
function transitionEndEventName() {
var i;
var el = document.createElement('div');
var transitions = {
'transition': 'transitionend',
'OTransition': 'otransitionend',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd'
};
for (i in transitions) {
if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) {
return transitions[i];
}
}
}
I've tested this code in Chrome, Firefox, Opera, Safari and I haven't had any more issues regarding components not being available. This way, onImportsLoaded
is only ever called if really all components have been loaded (and in case of java script libraries, executed).
Am I missing something or does this sound plausible?
@czellweg i still got onImportsLoaded called before all elements were loaded, infact it got called right away, the loading stays for just 2 ms...
Edit: The actual elements are imported, but i think after the imports polymer it`s self does more work, to order the elements on the page, and apply more styling to them, which it can be seen as the splash is already removed by then.
How about browser support on it? How to avoid this: crbug.com/504944 - readyState never goes to complete until Chrome 46.
on Mac Chrome 41 i have empty element (just tag) and its without async
!
<link rel="import" id="bundle" href="elements.html">
?
I need that because i need to debug my code.. and i got that i described in previous comment.
Tested on Mac Chromium 24 - works fine.
Fixed (i found fix there on gaming youtube) by adding this case with interactive on line37:
if ( link.import && (link.import.readyState === 'complete' || link.import.readyState === 'interactive')) {
onImportLoaded();
}
Hey Eric. Thanks for this gist. Super useful!
An issue that I have noticed and not sure how to resolve is that although we have
window.Polymer = window.Polymer || {dom: 'shadow'};
the html import may complete BEFORE the app.js runs, and therefore we miss the opportunity to configure polymer before it is to late. Any suggestions?