Cleaner way to hook into the window.onload of a web page? - javascript

I'm building a small iOS App in Swift and I use the WKWebView. What I am trying to achieve is to be notified when a web page has rendered completely. It is a known issue with WKWebView that none of its loading notifications work.
However, this approach seems to work and the idea is to hook into the window.onload function in Javascript and notify Swift from there. However, here too I found an issue. If I use JQuery, in some pages the callback doesn't happen apparently because the web pages already define window.onload. If I use the pure Javascript way, some web pages break i.e. they don't load at all apparently because I'm overriding the window.onload.
// using JQuery, this function never get called for web pages that do window.onload =
$(window).load(function() {
window.webkit.messageHandlers.callbackHandler.postMessage(
JSON.stringify({body: "window finished loading"}));
});
// works always but breaks web pages that use window.onload
window.onload = function() {
window.webkit.messageHandlers.callbackHandler.postMessage(
JSON.stringify({body: "window finished loading"}));
};
The question is how can I append this line notification to an existing window.onload and define one window.onload if it doesn't exist? other ideas also welcome e.g. queuing window.load implementations?
For completeness I have included below the two known ways to get such notifications using WKWebView natively.
(1) getting WKNavigationDelegate life-cycle notifications but the callback notification triggers too early when the web page has not yet rendered completely.
class ViewController: UIViewController {
// ...
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
NSLog("didFinishNavigation callback received ... too early!")
}
}
(2) Using the Key-Value Observer method (KVO) but here again callback notification triggers too early too:
webView.addObserver(viewController, forKeyPath: "estimatedProgress", options: .New, context: nil)
//
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<()>) {
guard let webView = object as? WKWebView else {return}
guard let change = change else {return}
guard let keyPath = keyPath else {return}
switch keyPath {
case "estimatedProgress":
if ((1.0 - webView.estimatedProgress) < 1e-10) {
NSLog("'estimatedProgress' callback received ... too early!")
}
break
default: break
}
}
it is the same issue for the "loading" KVO.
UPDATE: in addition to the accepted answer, the top ranked answer to this other question running-jquery-after-all-other-js-has-executed solves this OP too by polling the DOM for a specific change e.g. when all the Javascript has completed in addition to the loading of the page.

Instead of the load event you can listen to DOMContentLoaded.
Also you can perform the callback at the end of the execution stack by doing the following:
window.setTimeout(callback, 0);
Additionally you can try calling removeEventListener in your callback.
For Example:
if (window.addEventListener) {
var documentIsReady = function() {
window.removeEventListener("load", documentIsReady);
if (typeof window.isMyiOSAppAlreadyNotified === 'undefined') {
window.webkit.messageHandlers.callbackHandler.postMessage(JSON.stringify({body: "window onload"}));
}
window.isMyiOSAppAlreadyNotified = true;
};
window.addEventListener("load", function() { window.setTimeout(documentIsReady, 0); });
}
To avoid polluting the global(window) scope with variables like isMyiOSAppAlreadyNotified you can apply the module pattern.

I found a way that works. However despite my futile attempt to avoid duplicate notifications I still get two notifications for some web pages and haven't found a way to fix it yet ...
if (window.addEventListener) {
window.addEventListener("load",
function() {
// try to make sure it is called only once ...
if (typeof window.isMyiOSAppAlreadyNotified === 'undefined') {
// notify my iOS App that the page has finished loading
window.webkit.messageHandlers.callbackHandler.postMessage(
JSON.stringify({body: "window onload"}));
}
window.isMyiOSAppAlreadyNotified = true;
}
);
}

Related

Best way to solve this "ReferenceError: Function is not defined"?

I have an AEM page with multiple components, these components have a .js file with a function that encloses all the client side logic. We then call that function inside the component's HTML:
<script>
window.bootstrap_component(function() {
init_component_name();
});
</script>
As stated before init_component_name is the name of a function that encompasses all the logic we need:
function init_component_name() {
//DO STUFF
}
The wrapper bootstrap_component fuction is defined in the shared head.html of all our pages as:
<script>
window.bootstrap_component = function (handler) {
if (typeof handler === 'function') {
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
handler();
} else {
document.addEventListener("DOMContentLoaded", function() {
handler();
});
}
}
}
</script>
This works okay and we don't have any actual issues but we recently started using Bugsnag for error monitoring and reporting and we're getting reports for almost every component saying ReferenceError on page so/and/so init_component_name() is not defined.
The reason I think this is happening is because the init_component_name() function is not declared within the script tag and because this, function(init_component_name) has been attached to the window object it is executing fine and you don't see any console errors.
If I am correct would modifying those scripts tags to be like this work?
<script>
window.bootstrap_component(function() {
window.init_component_name();
})
</script>
A colleague of mine wants to add a timeout to the init_component_name functions of like 1ms but it rubs me the wrong way. Is there a more sensible approach?
If I am correct would modifying those scripts tags to be like this work?
window.bootstrap_component(function() {
window.init_component_name();
})
Yes, but you then have the problem that you're writing multiple data to the global namespace, window, which isn't ideal. What if another third-party script decides to override it?
Ideally you'd have a single namespace and put everything on there, and write only that namespace to window.
window.something = {};
something.bootstrap_component = { //...
and
something.init_component_name = () => {
//DO STUFF
}
Or better still, use modules (though that will need some light code refactoring).
Don't do the timeout hack. It's really, really horrible; what if scripts take longer than a second to load for some reason? You're also forcing your UI to wait a second, often unnecessarily. This hack tends to feature where chronology and scope has not been thought out properly.

Unload a Javascript from memory

I am thinking of a web application on browsers. I can dynamically add required js files as needed while traversing through different parts of application, but can I unload unnecessary js content from the current session's "memory" as the memory usage grows over time?
I know it is possible to remove the tag responsible for the content but it is not assured that it will eventually unload and unbind everything correspondent to that content.
Thanks
I know it is possible to remove the tag responsible for the content but it is not assured that it will eventually unload and unbind everything correspondent to that content.
In fact, it's assured that it will not. Once the JavaScript is loaded and executed, there is no link between it and the script element that loaded it at all.
You can dynamically load and unload modules, but you have to design it into your code by
Having a module have a single symbol that refers to the module as a whole
Having an "unload" function or similar that is responsible for removing all of a module's event listeners and such
Then unloading the module is a matter of calling its unload function and then setting the symbol that refers to it to undefined (or removing it entirely, if it's a property on an object).
Here's a very simple demonstration of concept (which you'd adapt if you were using some kind of Asynchronous Module Definition library):
// An app-wide object containing a property for each module.
// Each module can redefine it in this way without disturbing other modules.
var AppModules = AppModules || {};
// The module adds itself
AppModules.ThisModule = (function() {
var unloadCallbacks = [];
function doThis() {
// Perhaps it involves setting up an event handler
var element = document.querySelector(/*...*/);
element.addEventHandler("click", handler, false);
unloadCallbacks.push(function() {
element.removeEventHandler("click", handler, false);
element = handler = undefined;
});
function handler(e) {
// ...
}
}
function doThat() {
// ...
}
function unload() {
// Handle unloading any handlers, etc.
var callbacks = unloadCallbacks;
unloadCallbacks = undefined;
callbacks.forEach(function(callback) {
callback();
});
// Remove this module
delete AppModules.ThisModule;
}
return {
doThis: doThis,
unload: unload
};
})();
That callbacks mechanism is very crude, you'd want something better. But it demonstrates the concept.

Notify main page of variable changed by external js

I'm attempting to create a modular sign in script for some webpages I'm developing. In short, I load the script on the main page, fire the main signIn function from a button press, and an overlay div is created on the main page which is managed by the external signIn.js. The external js sets some sessionStorage variables that will be utilized in the main page.
The hope for modularity would be to have signIn.js handle the authentication from the database and have the main page do with the process of signing in as needed (in this specific instance, it gives users access to their projects). Ideally, the sign in will not force a refresh of the main page due to other project goals.
The problem I'm encountering, is how do I notify the main page that the user has signed in without destroying any sense of modularity?
On top of other efforts, the most hopeful was attempting to create a custom event on the main page's document using $(document).on('userSignedIn', function() {...}); but signIn.js apparently cannot trigger this event.
Any suggestions for how to accomplish this or am I just going about this entirely wrong?
EDIT:
So, this was definitely a scope related issue I was experiencing. To flesh out the process, if anyone finds it relevant, signIn.js adds an overlay div to mainPage.html. $("#signInContainerDiv").load("signIn.html") is used to load the sign in form into the page. It turns out, when I was trying to reference $(document), it was using signIn.html's document, and not mainPage.html's. Upon that realization, I just created a div (signInNotify) on the mainPage that I bind the event to ($("#signInNotify").on("userSignedIn", function() {...});) and trigger it in signIn.js.
My own inexperience has conquered me, yet again.
jQuery can help you out when it comes to this. Here's an example from the main page for trigger
$( "#foo" ).on( "custom", function( event, param1, param2 ) {
alert( param1 + "\n" + param2 );
});
$( "#foo").trigger( "custom", [ "Custom", "Event" ] );
jQuery Page Reference
Another solution is to use some library like amplify.js, it has publish/subscribe functionality which can be useful for implementing the "observer pattern". You could also implement your own library for that, the code could be something like this:
// the implementation
function Notify () {
this.listeners = {};
}
Notify.prototype.subscribe = function (event, callback, context) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push({ callback: callback, context: context || null});
};
Notify.prototype.publish = function (event/*, args...*/) {
var args = Array.prototype.slice.call(arguments, 1);
(this.listeners[event] || []).forEach(function (x) {
x.callback.apply(x.callback.context, args);
});
};
// usage:
// an instance, or can be implemented as a singleton
var global_events = new Notify();
// wherever you want to be notified of login events
global_events.subscribe('login_success', function () {
// do something with the arguments
}, myContext/*optional*/);
// after success login
global_events.publish('login_success', user_credentials, other_data);
// and all subscribers (listeners) will be called after this
I have used that code for similar purposes and also used amplifyjs a couple times, you can read more about Amplify Pub/Sub.

IE11 script onload not working (sometimes)

I am currently working on a data-intensive web application that frequently communicates with an external API and retrieves JSONP data when returned. The script depends upon a library called head.js v1.0.3. http://headjs.com/ to accomplish this. However, I noticed that in IE 11 for some reason, the onload event for the script sometimes, but not always, fires before the script has actually loaded into the browser. The behavior is demonstrable whether using head.js or not. Alternatively, I may create a script element with the onload event set to capture the returned data. Sometimes it works, and sometimes not. Even more weird is that once it happens the first time, it seems to keep happening for the duration of the browser session.
Any ideas for a workaround?
Here is some example code:
//somejson.js
/*
window["queryResult"] = {blah:'blah'}
*/
function loadScript() {
head.load("/somejson.js", afterScriptLoad)
}
function afterScriptLoad() {
var result = queryResult
//Throws error because window.queryResult is sometimes undefined
}
After a little bit of research, it seems the only way around this bug is to modify the API so that once the variable holding the JSONP is initialized, the script itself triggers the callback. Unfortunately, this would not work as a solution for others if they do not have access to modify whatever API is in use, but it does solve the problem for me.
//somejson.js
/*
window["queryResult"] = {blah:'blah'}; scriptCallback()
*/
function loadScript(callback) {
var c = new afterScriptLoad(callback)
window["scriptCallback"] = c
head.load("/somejson.js", c)
}
function afterScriptLoad(callback) {
var retrieved = false
return function () {
if (!retrieved) {
retrieved = true
callback(queryResult)
}
}
}
function myCallback(response) {
//do something
}

Vanilla JavaScript equivalent of jQuery's $.ready() - how to call a function when the page/DOM is ready for it [duplicate]

This question already has answers here:
$(document).ready equivalent without jQuery
(39 answers)
Closed 6 years ago.
With jQuery, we all know the wonderful .ready() function:
$('document').ready(function(){});
However, let's say I want to run a function that is written in standard JavaScript with no library backing it, and that I want to launch a function as soon as the page is ready to handle it. What's the proper way to approach this?
I know I can do:
window.onload="myFunction()";
Or I can use the body tag:
<body onload="myFunction()">
Or I can even try at the bottom of the page after everything, but the end body or html tag like:
<script type="text/javascript">
myFunction();
</script>
What is a cross-browser(old/new)-compliant method of issuing one or more functions in a manner like jQuery's $.ready()?
The simplest thing to do in the absence of a framework that does all the cross-browser compatibility for you is to just put a call to your code at the end of the body. This is faster to execute than an onload handler because this waits only for the DOM to be ready, not for all images to load. And, this works in every browser.
<!doctype html>
<html>
<head>
</head>
<body>
Your HTML here
<script>
// self executing function here
(function() {
// your page initialization code here
// the DOM will be available here
})();
</script>
</body>
</html>
For modern browsers (anything from IE9 and newer and any version of Chrome, Firefox or Safari), if you want to be able to implement a jQuery like $(document).ready() method that you can call from anywhere (without worrying about where the calling script is positioned), you can just use something like this:
function docReady(fn) {
// see if DOM is already available
if (document.readyState === "complete" || document.readyState === "interactive") {
// call on next available tick
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
Usage:
docReady(function() {
// DOM is loaded and ready for manipulation here
});
If you need full cross browser compatibility (including old versions of IE) and you don't want to wait for window.onload, then you probably should go look at how a framework like jQuery implements its $(document).ready() method. It's fairly involved depending upon the capabilities of the browser.
To give you a little idea what jQuery does (which will work wherever the script tag is placed).
If supported, it tries the standard:
document.addEventListener('DOMContentLoaded', fn, false);
with a fallback to:
window.addEventListener('load', fn, false )
or for older versions of IE, it uses:
document.attachEvent("onreadystatechange", fn);
with a fallback to:
window.attachEvent("onload", fn);
And, there are some work-arounds in the IE code path that I don't quite follow, but it looks like it has something to do with frames.
Here is a full substitute for jQuery's .ready() written in plain javascript:
(function(funcName, baseObj) {
// The public function name defaults to window.docReady
// but you can pass in your own object and own function name and those will be used
// if you want to put them in a different namespace
funcName = funcName || "docReady";
baseObj = baseObj || window;
var readyList = [];
var readyFired = false;
var readyEventHandlersInstalled = false;
// call this when the document is ready
// this function protects itself against being called more than once
function ready() {
if (!readyFired) {
// this must be set to true before we start calling callbacks
readyFired = true;
for (var i = 0; i < readyList.length; i++) {
// if a callback here happens to add new ready handlers,
// the docReady() function will see that it already fired
// and will schedule the callback to run right after
// this event loop finishes so all handlers will still execute
// in order and no new ones will be added to the readyList
// while we are processing the list
readyList[i].fn.call(window, readyList[i].ctx);
}
// allow any closures held by these functions to free
readyList = [];
}
}
function readyStateChange() {
if ( document.readyState === "complete" ) {
ready();
}
}
// This is the one public interface
// docReady(fn, context);
// the context argument is optional - if present, it will be passed
// as an argument to the callback
baseObj[funcName] = function(callback, context) {
if (typeof callback !== "function") {
throw new TypeError("callback for docReady(fn) must be a function");
}
// if ready has already fired, then just schedule the callback
// to fire asynchronously, but right away
if (readyFired) {
setTimeout(function() {callback(context);}, 1);
return;
} else {
// add the function and context to the list
readyList.push({fn: callback, ctx: context});
}
// if document already ready to go, schedule the ready function to run
if (document.readyState === "complete") {
setTimeout(ready, 1);
} else if (!readyEventHandlersInstalled) {
// otherwise if we don't have event handlers installed, install them
if (document.addEventListener) {
// first choice is DOMContentLoaded event
document.addEventListener("DOMContentLoaded", ready, false);
// backup is window load event
window.addEventListener("load", ready, false);
} else {
// must be IE
document.attachEvent("onreadystatechange", readyStateChange);
window.attachEvent("onload", ready);
}
readyEventHandlersInstalled = true;
}
}
})("docReady", window);
The latest version of the code is shared publicly on GitHub at https://github.com/jfriend00/docReady
Usage:
// pass a function reference
docReady(fn);
// use an anonymous function
docReady(function() {
// code here
});
// pass a function reference and a context
// the context will be passed to the function as the first argument
docReady(fn, context);
// use an anonymous function with a context
docReady(function(context) {
// code here that can use the context argument that was passed to docReady
}, ctx);
This has been tested in:
IE6 and up
Firefox 3.6 and up
Chrome 14 and up
Safari 5.1 and up
Opera 11.6 and up
Multiple iOS devices
Multiple Android devices
Working implementation and test bed: http://jsfiddle.net/jfriend00/YfD3C/
Here's a summary of how it works:
Create an IIFE (immediately invoked function expression) so we can have non-public state variables.
Declare a public function docReady(fn, context)
When docReady(fn, context) is called, check if the ready handler has already fired. If so, just schedule the newly added callback to fire right after this thread of JS finishes with setTimeout(fn, 1).
If the ready handler has not already fired, then add this new callback to the list of callbacks to be called later.
Check if the document is already ready. If so, execute all ready handlers.
If we haven't installed event listeners yet to know when the document becomes ready, then install them now.
If document.addEventListener exists, then install event handlers using .addEventListener() for both "DOMContentLoaded" and "load" events. The "load" is a backup event for safety and should not be needed.
If document.addEventListener doesn't exist, then install event handlers using .attachEvent() for "onreadystatechange" and "onload" events.
In the onreadystatechange event, check to see if the document.readyState === "complete" and if so, call a function to fire all the ready handlers.
In all the other event handlers, call a function to fire all the ready handlers.
In the function to call all the ready handlers, check a state variable to see if we've already fired. If we have, do nothing. If we haven't yet been called, then loop through the array of ready functions and call each one in the order they were added. Set a flag to indicate these have all been called so they are never executed more than once.
Clear the function array so any closures they might be using can be freed.
Handlers registered with docReady() are guaranteed to be fired in the order they were registered.
If you call docReady(fn) after the document is already ready, the callback will be scheduled to execute as soon as the current thread of execution completes using setTimeout(fn, 1). This allows the calling code to always assume they are async callbacks that will be called later, even if later is as soon as the current thread of JS finishes and it preserves calling order.
If you are doing VANILLA plain JavaScript without jQuery, then you must use (Internet Explorer 9 or later):
document.addEventListener("DOMContentLoaded", function(event) {
// Your code to run since DOM is loaded and ready
});
Above is the equivalent of jQuery .ready:
$(document).ready(function() {
console.log("Ready!");
});
Which ALSO could be written SHORTHAND like this, which jQuery will run after the ready even occurs.
$(function() {
console.log("ready!");
});
NOT TO BE CONFUSED with BELOW (which is not meant to be DOM ready):
DO NOT use an IIFE like this that is self executing:
Example:
(function() {
// Your page initialization code here - WRONG
// The DOM will be available here - WRONG
})();
This IIFE will NOT wait for your DOM to load. (I'm even talking about latest version of Chrome browser!)
I would like to mention some of the possible ways here together with a pure javascript trick which works across all browsers:
// with jQuery
$(document).ready(function(){ /* ... */ });
// shorter jQuery version
$(function(){ /* ... */ });
// without jQuery (doesn't work in older IEs)
document.addEventListener('DOMContentLoaded', function(){
// your code goes here
}, false);
// and here's the trick (works everywhere)
function r(f){/in/.test(document.readyState)?setTimeout('r('+f+')',9):f()}
// use like
r(function(){
alert('DOM Ready!');
});
The trick here, as explained by the original author, is that we are checking the document.readyState property. If it contains the string in (as in uninitialized and loading, the first two DOM ready states out of 5) we set a timeout and check again. Otherwise, we execute the passed function.
And here's the jsFiddle for the trick which works across all browsers.
Thanks to Tutorialzine for including this in their book.
Tested in IE9, and latest Firefox and Chrome and also supported in IE8.
document.onreadystatechange = function () {
var state = document.readyState;
if (state == 'interactive') {
init();
} else if (state == 'complete') {
initOnCompleteLoad();
}
}​;
Example: http://jsfiddle.net/electricvisions/Jacck/
UPDATE - reusable version
I have just developed the following. It's a rather simplistic equivalent to jQuery or Dom ready without backwards compatibility. It probably needs further refinement. Tested in latest versions of Chrome, Firefox and IE (10/11) and should work in older browsers as commented on. I'll update if I find any issues.
window.readyHandlers = [];
window.ready = function ready(handler) {
window.readyHandlers.push(handler);
handleState();
};
window.handleState = function handleState () {
if (['interactive', 'complete'].indexOf(document.readyState) > -1) {
while(window.readyHandlers.length > 0) {
(window.readyHandlers.shift())();
}
}
};
document.onreadystatechange = window.handleState;
Usage:
ready(function () {
// your code here
});
It's written to handle async loading of JS but you might want to sync load this script first unless you're minifying. I've found it useful in development.
Modern browsers also support async loading of scripts which further enhances the experience. Support for async means multiple scripts can be downloaded simultaneously all while still rendering the page. Just watch out when depending on other scripts loaded asynchronously or use a minifier or something like browserify to handle dependencies.
The good folks at HubSpot have a resource where you can find pure Javascript methodologies for achieving a lot of jQuery goodness - including ready
http://youmightnotneedjquery.com/#ready
function ready(fn) {
if (document.readyState != 'loading'){
fn();
} else if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', fn);
} else {
document.attachEvent('onreadystatechange', function() {
if (document.readyState != 'loading')
fn();
});
}
}
example inline usage:
ready(function() { alert('hello'); });
I'm not quite sure what you're asking, but maybe this can help:
window.onload = function(){
// Code. . .
}
Or:
window.onload = main;
function main(){
// Code. . .
}
Your method (placing script before the closing body tag)
<script>
myFunction()
</script>
</body>
</html>
is a reliable way to support old and new browsers.
Ready
function ready(fn){var d=document;(d.readyState=='loading')?d.addEventListener('DOMContentLoaded',fn):fn();}
Use like
ready(function(){
//some code
});
For self invoking code
(function(fn){var d=document;(d.readyState=='loading')?d.addEventListener('DOMContentLoaded',fn):fn();})(function(){
//Some Code here
//DOM is avaliable
//var h1s = document.querySelector("h1");
});
Support: IE9+
Here's a cleaned-up, non-eval-using version of Ram-swaroop's "works in all browsers" variety--works in all browsers!
function onReady(yourMethod) {
var readyStateCheckInterval = setInterval(function() {
if (document && document.readyState === 'complete') { // Or 'interactive'
clearInterval(readyStateCheckInterval);
yourMethod();
}
}, 10);
}
// use like
onReady(function() { alert('hello'); } );
It does wait an extra 10 ms to run, however, so here's a more complicated way that shouldn't:
function onReady(yourMethod) {
if (document.readyState === 'complete') { // Or also compare to 'interactive'
setTimeout(yourMethod, 1); // Schedule to run immediately
}
else {
readyStateCheckInterval = setInterval(function() {
if (document.readyState === 'complete') { // Or also compare to 'interactive'
clearInterval(readyStateCheckInterval);
yourMethod();
}
}, 10);
}
}
// Use like
onReady(function() { alert('hello'); } );
// Or
onReady(functionName);
See also How to check if DOM is ready without a framework?.
document.ondomcontentready=function(){} should do the trick, but it doesn't have full browser compatibility.
Seems like you should just use jQuery min

Categories

Resources