I want to lazy load a css file, and then have javascript modify a page after said css loaded. To that end, I'm using this approach, as illustrated here:
lazyLoadCSS = function (pathToStyleSheet, options, callback) {
var stylesheet = document.createElement('link');
stylesheet.href = pathToStyleSheet;
stylesheet.rel = 'stylesheet';
stylesheet.type = 'text/css';
// temporarily set media to something inapplicable to ensure
// it'll fetch without blocking render
stylesheet.media = 'only x';
// set the media back when the stylesheet loads
stylesheet.onload = function() {
stylesheet.media = 'all';
// here be callbacks...
callback();
};
document.getElementsByTagName('head')[0].appendChild(stylesheet);
};
So, the idea is, javascript fetches the CSS, but smuggles it in as 'only x' so the browser doesn't think it needs to wait for the file before rendering the rest of the page, which is the whole point. This approach seems to be pretty standard and the above code does work...mostly.
The only problem is the callback. It happens after the CSS is loaded, but before the styles have been applied to the document.
If I put a timer on the callback, like so:
window.setTimeout(function(){
callback();
}, 2000);
then everything works exactly the way it should (only slow).
Question is, how can I wait not only for the CSS to load, but also to be applied before I run the callback()?
As per some comments above, this is what I've come up with and it work great (though it's not thoroughly tested):
function waitForStylesToApply(cycleNumber, canary, callback){
if(cycleNumber < 100){
window.setTimeout(function(){
var computedValue = window
.getComputedStyle(window.document.body, null)
.getPropertyValue('max-width');
if(computedValue !== canary){
if(typeof callback === 'function'){
console.log('done in ' + cycleNumber + ' cycles');
callback();
}
}
else {
waitForStylesToApply(cycleNumber++, canary, callback);
}
}, 10);
}
}
Checks on the computed style for <body> over and over until it changes. Then it does the callback. So far, the code has never had to cycle through more than once, so the wait is infinitesimal.
Even a delay of 10 is too much. Code will work with a delay of 0 and still only cycle once.
EDIT: use this function in place of simply using callback() in the example above.
Related
I'm working with an "infinite scroll" page that calls up to 40 elements at a time when a user scrolls to the bottom of the page. How do I detect the moment at which all the content of the most recent set has been loaded?
This only works for initial page load:
$(window).load(function() {
// Do something
});
Is there something similar for when there's a "load" long after the page has already been done loading?
Sorry if this is a repeat of another question. I was unable to find a solution anywhere else.
After some more digging around, I've used a variation of this question/answer.
My end result looks something like this:
function ($container, previousCount, callback) {
var _imgs = $container.find('img'),
_newImages = _imgs.slice(previousCount), // 'start index' for new images
imgCount = _newImages.length, // count how many new ones exist
counter = 0;
_imgs.on('load', function() { // Count loaded
runCallback(20, callback);
}).on('error', function() { // Count errors, anyway, to reach total
runCallback(20, callback);
});
function runCallback(counterLimit, callback) {
// if counter meets new count or hits counter limit, run callback
counter++;
if (counter == imgCount || counter == counterLimit) {
callback();
}
}
}
Some quirks to why I needed it this way. previousCount can actually be greater than total number of images currently in the DOM, this is because people can jump throughout different portions of the infinite scroll. Because of this, there's also the check for counter == counterLimit.
Also, I need to run a script when the new set of images is done loading, regardless of whether they succeeded or failed. This is why I'm using, both, .on('load' and .on('error' to run up the count. I noticed that sometimes an image can error out (which is fine in this particular case) and it will throw off the count and never fire the callback.
==================
EDIT 04/27/16: I've since discovered a plugin that can handle this for you and seems to work well (with the exception of several subsequent loads before previous load is done loading). Check out imagesLoaded.
Can anyone explain why, in Internet Explorer, code sample 1 doesn't work while code sample 2 works?
Code 1 (non-functional)
Modernizr.load([
{
load: [
'../includes/css/foo.css',
'../includes/js/foo.js',
'../includes/js/bar.js'
],
complete: function() {
initBar();
}
}
]);
Code 2 (functional)
Modernizr.load([
{
load: [
'../includes/css/foo.css',
'../includes/js/foo.js',
'../includes/js/bar.js'
],
complete: function() {
window.initBar();
}
}
]);
bar.js
var initBar = function() {
// code here
};
It works fine in other browsers. I've tried moving the blocks to the head section as well as beneath the page. I've also tried wrapping the contents of the callback in $(document).ready(), but none have worked with code 1.
The error I am getting specifically is:
SCRIPT5009: « initBar » est indéfini
It is almost as if the callback is executed before the resources are finished loading, but if that was the case then why does code sample 2 work?
Also I will note that on refresh the page loads fine (most likely due to the fact that the resources are cached), but it also loads fine after clearing the cache. I have to restart my browser session after clearing the cache to reproduce the problem.
UPDATE:
This problem extends to more than just functions. Any global variable defined in a JS file that is loaded doesn't seem to be accessible directly. It also occurs if I load the CSS at the top of the page rather than with the other resources asynchronously. In fact I'm also noticing this problem with some jQuery plugins that are loaded in this manner.
UPDATE 2:
Here is the console.log() output as per debugging instructions below. I've changed bar to be an object instead of a function for the sake of illustrating this.
Internet Explorer:
HTML1300: Une navigation s’est produite.
Fichier : test18.php
before .load() called
before bar accessed
typeof bar = undefined
typeof window.bar = undefined
SCRIPT5009: « bar » est indéfini
Fichier : test18.js, ligne : 14, colonne : 13
before bar defined
So it appears that the complete function executes before bar is defined. I find it strange that window.bar is also undefined yet works...
Firefox
[02:10:46,448] "before .load() called"
[02:10:47,184] "before bar defined"
[02:10:47,184] "before bar accessed"
[02:10:47,184] "typeof bar = object"
[02:10:47,184] "typeof window.bar = object"
Chrome
before .load() called
before bar defined
before bar accessed
typeof bar = object
typeof window.bar = object
Both Firefox and Chrome appear to be loading and executing the resources in the correct order.
First off, you should know that .load() in modernizr comes from the yepnope library so that's where you find the detailed documentation for it.
Here are the possible things that could be different in different browsers that I can think of:
The exact timing of the loading of the scripts and thus the timing of when the complete() function gets called.
The caching in the browser (which can affect the load timing).
Because you are defining initBar by assigning it to a variable instead of a regular function initBar() definition, the function will not exist until that line of code executes whereas function initBar() will exist at script parse time.
Make sure you version 1.5 or higher of the yepnope loading library (I don't know what modernizr version that corresponds to. The yepnope doc for .load() says this: "In versions of yepnope prior to 1.5 this [when the complete function is called] could vary from time to time".
There are notes on this page that the yepnope library may not wait for .css files to load before calling the complete callback unless you have an add-in present. I don't know if that throws off the whole complete timing or what as I note that you do have .css files in your load list.
So, here's what I'd suggest to debug this:
1) Change your initBar definition to this:
function initBar() {
// code here
}
2) Make sure your initBar definition is in the proper scope and reachable from your other code. Look out for things like being inside another function (onload, document.ready, etc...) which might make it unreachable.
3) Insert some console.log() statements like this to do some timing debugging:
console.log("before .load() called");
Modernizr.load([
{
load: [
'../includes/css/foo.css',
'../includes/js/foo.js',
'../includes/js/bar.js'
],
complete: function() {
console.log("before initBar() called");
console.log("typeof initBar = " + typeof initBar);
console.log("typeof window.initBar = " + typeof window.initBar);
initBar();
console.log("after initBar() called");
}
}
]);
console.log("before initBar() defined");
function initBar() {
// code here
}
Then, see what order things come out in and what the typeof statements say. The idea here is to try to figure out if things are executing in the wrong order or if a scope is wrong.
4) Try loading the .css file separately so it won't be affecting the .js loading.
Here's a replacement script that can load multiple scripts dynamically to replace the modernizr buggy .load() code. This one loads them all in parallel. This works only for scripts files (though the same concept could be used for .css files.
function loadScriptsInParallel(scripts, completeCallback) {
var head = document.getElementsByTagName('head')[0];
var remaining = scripts.length, i, scriptTag;
function complete() {
// make sure it's not called again for this script
this.onreadystatechange = this.onload = function() {};
// decrement remaining count and check if all are done
--remaining;
if (remaining === 0) {
// all are done call the callback
completeCallback();
}
}
for (var i = 0; i < scripts.length; i++) {
scriptTag = document.createElement('script');
scriptTag.type = 'text/javascript';
scriptTag.src = scripts[i];
// most browsers
scriptTag.onload = complete;
// IE 6 & 7
scriptTag.onreadystatechange = function() {
if (this.readyState == 'complete') {
complete.apply(this, arguments);
}
}
head.appendChild(scriptTag);
}
}
Sample usage:
loadScriptsInParallel([
'../includes/js/foo.js',
'../includes/js/bar.js'
], function() {
// put code here for when all scripts are loaded
initBar();
});
Working demo: http://jsfiddle.net/jfriend00/qs44R/
If you need them loaded sequentially (one after the other because of dependencies between them), then you could use this:
function loadScriptsInSequence(scripts, completeCallback) {
var head = document.getElementsByTagName('head')[0];
var remaining = scripts.length, i = 0;
function loadNext() {
var scriptTag = document.createElement('script');
scriptTag.type = 'text/javascript';
scriptTag.src = scripts[i++];
// most browsers
scriptTag.onload = complete;
// IE 6 & 7
scriptTag.onreadystatechange = function() {
if (this.readyState == 'complete') {
complete.apply(this, arguments);
}
}
head.appendChild(scriptTag);
}
function complete() {
// make sure it's not called again for this script
this.onreadystatechange = this.onload = function() {};
// decrement remaining count and check if all are done
--remaining;
if (remaining === 0) {
// all are done call the callback
completeCallback();
} else {
loadNext();
}
}
loadNext();
}
Working demo: http://jsfiddle.net/jfriend00/9aVLW/
I have a js file that contains my closure, this file is loaded before jQuery, let's say it can't be moved. How can I pass in or check for jQuery with a view to use it in the closure?
This is what I've got so far:
(function MyClosure() {
var interval = setInterval(function() {
if (typeof jQuery !== 'undefined') {
doJqueryStuff();
clearInterval(interval);
}
}, 500);
function doJqueryStuff() {
// Some stuff with jQuery.
}
})();
It actually works, but is there a "better" way? I always think I'm doing something wrong whenever I use setInterval() for things like this, also the fact I am losing time in that 500ms.
You could wait and attach your execution to the window.onload event, assuming jQuery is loaded once the window is loaded...
window.onload = function() {
// do stuff with jQuery
};
Don't worry - while it does look hackish (at least to me and you) it isn't bad. Often times you need to wait until a complex object is initialized and you need to do the same thing. The best thing is to just ensure the order that your scripts load to solve any dependency issues - but as you requested let's assume the order can't be adjusted.
The only improvement I would suggest: adding an escape hatch to anonymous setInterval function. That way if jQuery never becomes available for some reason, the script can notify the user and stop checking.
var checkCount = 0;
var interval = setInterval(function() {
if (checkCount++ > 20) {
alert("jQuery could not be loaded - degrading user experience");
clearInterval(interval);
}
if (typeof jQuery !== 'undefined') {
doJqueryStuff();
clearInterval(interval);
}
}, 500);
Wait for the onload event on the script tag. In this case, the doJqueryStuff should be a global function.
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js" onload='doJqueryStuff()'></script>
I'm working on a do-dad that can be embedded in a page like a youtube video. The particular effect I want needs jQuery to work.
I want to load jQuery on the condition that something on the page hasn't already added jQuery.
I though of testing
if (typeof($)=='function'){...
but that only works if jQuery is loaded & running by the time the page gets to my script. Since best practices these days is to embed you scripts in the footer, my embed code probably will never see jQuery most of the time anyway.
I thought of doing the test onready instead of onload, but the onready function is inside of jQuery. (I suppose I could use a standalone script? is there a good one?)
Lastly, I though of testing for jQuery after a timeout delay, but this seems inelegant at best and unreliable at worst.
Any thoughts?
Given your constraints, I see only two options:
Use window.load event:
(function() {
if (window.addEventListener) {
// Standard
window.addEventListener('load', jQueryCheck, false);
}
else if (window.attachEvent) {
// Microsoft
window.attachEvent('onload', jQueryCheck);
}
function jQueryCheck() {
if (typeof jQuery === "undefined") {
// No one's loaded it; either load it or do without
}
}
})();
window.load happens very late in the loading cycle, though, after all images are and such loaded.
Use a timeout. The good news is that the timeout can probably be quite short.
(function() {
var counter = 0;
doDetect();
function doDetect() {
if (typeof jQuery !== "undefined") {
// ...jQuery has been loaded
}
else if (++counter < 5) { // 5 or whatever
setTimeout(doDetect, 10);
}
else {
// Time out (and either load it or don't)
}
}
})();
You'll have to tune to decide the best values for the counter and the interval. But if jQuery isn't loaded even on the first or second loop, my guess (untested) is that it isn't going to be loaded...unless someone else is doing what you're doing. :-)
You can use window.onload. This fires after domReady, so jQuery would surely be loaded by this point.
And check for jQuery, not $. Sometimes people use jQuery with other libraries and use $ for something different.
However, IMHO, I don't think it's a big deal if jQuery gets loaded twice.
I've been using this code for to do this very thing for a while now. It also checks for a minimum version of jQuery (in our case, we're still using 1.4.2) before loading:
/* Checks if JQuery is loaded... if not, load it. */
/* Remember to update minimum version number when updating the main jquery.min.js file. */
if (typeof jQuery != 'undefined') {
/* jQuery is already loaded... verify minimum version number of 1.4.2 and reload newer if needed */
if (/1\.(0|1|2|3|4)\.(0|1)/.test(jQuery.fn.jquery) || /^1.1/.test(jQuery.fn.jquery) || /^1.2/.test(jQuery.fn.jquery)|| /^1.3/.test(jQuery.fn.jquery)) {
loadJQ();
}
} else {
loadJQ();
}
/* loads jQuery if not already loaded, or if not a recent enough version */
function loadJQ() {
/* adds a link to jQuery to the head, instead of inline, so it validates */
var headElement = document.getElementsByTagName("head")[0];
linkElement=document.createElement("script");
linkElement.src="../scripts/lib/jquery.min.js";
linkElement.type="text/javascript";
headElement.appendChild(linkElement);
}
Is there an event that fires when JavaScript files are loaded? The problem came up because YSlow recommends to move JavaScript files to the bottom of the page. This means that
$(document).ready(function1) is fired before the js file that contains the code for function1 is loaded.
How to avoid this kind of situation?
I don't have a reference for it handy, but script tags are processed in order, and so if you put your $(document).ready(function1) in a script tag after the script tags that define function1, etc., you should be good to go.
<script type='text/javascript' src='...'></script>
<script type='text/javascript' src='...'></script>
<script type='text/javascript'>
$(document).ready(function1);
</script>
Of course, another approach would be to ensure that you're using only one script tag, in total, by combining files as part of your build process. (Unless you're loading the other ones from a CDN somewhere.) That will also help improve the perceived speed of your page.
EDIT: Just realized that I didn't actually answer your question: I don't think there's a cross-browser event that's fired, no. There is if you work hard enough, see below. You can test for symbols and use setTimeout to reschedule:
<script type='text/javascript'>
function fireWhenReady() {
if (typeof function1 != 'undefined') {
function1();
}
else {
setTimeout(fireWhenReady, 100);
}
}
$(document).ready(fireWhenReady);
</script>
...but you shouldn't have to do that if you get your script tag order correct.
Update: You can get load notifications for script elements you add to the page dynamically if you like. To get broad browser support, you have to do two different things, but as a combined technique this works:
function loadScript(path, callback) {
var done = false;
var scr = document.createElement('script');
scr.onload = handleLoad;
scr.onreadystatechange = handleReadyStateChange;
scr.onerror = handleError;
scr.src = path;
document.body.appendChild(scr);
function handleLoad() {
if (!done) {
done = true;
callback(path, "ok");
}
}
function handleReadyStateChange() {
var state;
if (!done) {
state = scr.readyState;
if (state === "complete") {
handleLoad();
}
}
}
function handleError() {
if (!done) {
done = true;
callback(path, "error");
}
}
}
In my experience, error notification (onerror) is not 100% cross-browser reliable. Also note that some browsers will do both mechanisms, hence the done variable to avoid duplicate notifications.
When they say "The bottom of the page" they don't literally mean the bottom: they mean just before the closing </body> tag. Place your scripts there and they will be loaded before the DOMReady event; place them afterwards and the DOM will be ready before they are loaded (because it's complete when the closing </html> tag is parsed), which as you have found will not work.
If you're wondering how I know that this is what they mean: I have worked at Yahoo! and we put our scripts just before the </body> tag :-)
EDIT: also, see T.J. Crowder's reply and make sure you have things in the correct order.
Take a look at jQuery's .load() http://api.jquery.com/load-event/
$('script').load(function () { });
Further to #T.J. Crowder 's answer, I've added a recursive outer loop that allows one to iterate through all the scripts in an array and then execute a function once all the scripts are loaded:
loadList([array of scripts], 0, function(){// do your post-scriptload stuff})
function loadList(list, i, callback)
{
{
loadScript(list[i], function()
{
if(i < list.length-1)
{
loadList(list, i+1, callback);
}
else
{
callback();
}
})
}
}
Of course you can make a wrapper to get rid of the '0' if you like:
function prettyLoadList(list, callback)
{
loadList(list, 0, callback);
}
Nice work #T.J. Crowder - I was cringing at the 'just add a couple seconds delay before running the callback' I saw in other threads.
I always make a call from the end of the JavaScript files for registering its loading and it used to work perfect for me for all the browsers.
Ex: I have an index.htm, Js1.js and Js2.js. I add the function IAmReady(Id) in index.htm header and call it with parameters 1 and 2 from the end of the files, Js1 and Js2 respectively. The IAmReady function will have a logic to run the boot code once it gets two calls (storing the the number of calls in a static/global variable) from the two js files.
Change the loading order of your scripts so that function1 was defined before using it in ready callback.
Plus I always found it better to define ready callback as an anonymous method then named one.
Like T.J. wrote: the order is defined (at least it's sequential when your browser is about to execute any JavaScript, even if it may download the scripts in parallel somehow). However, as apparently you're having trouble, maybe you're using third-party JavaScript libraries that yield some 404 Not Found or timeout? If so, then read Best way to use Google’s hosted jQuery, but fall back to my hosted library on Google fail.