In my project, I have the pretty standard loadScript method to dynamically load JS scripts, and when done, the callback is fired:
app.utilities.loadScript = function loadScript(url, callback)
{
if (app.scripts[url]===true) {
callback();
} else {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = function() {
app.scripts[url] = true;
callback();
}
// Fire the loading
head.appendChild(script);
}
};
In my project situation, I'm working with a component-based system. To illustrate the problem, imagine two Google Maps components on the same page. Both are instances of a component. In their constructor, I'm calling the method, like so...
constructor(selector) {
super(selector);
// API load status
this._api = false;
// load the maps API
app.utilities.loadScript('https://maps.googleapis.com/maps/api/js?v=3.exp&key=' + app.config.mapsKey, this.initMap.bind(this));
}
I anticipated the situation that if two instances of a component load the same script, it should not get loaded twice. For that reason, you can see how inside the loadScript helper, I'm maintaining a global array of already loaded scripts: app.scripts[url] = true;
This strategy does not work, however, because multiple loadScript calls are started very close after each other. Since the first one did not finish yet (in progress), the second call starts another one.
I'm considering immediately setting a "loading" status on the script in a global array, but that would still leave me wondering how the second call can listen to the first one being ready, because it does need such a trigger otherwise the callback fires too soon.
I have a feeling I'm perhaps overthinking this?
Edit: including the code that solves this, based on an answer by @siam, with a little tweak added:
app.utilities.loadScriptEvent = function loadScript(url, eventName)
{
if (app.scripts[url] == 'loading') {
return true;
}
else {
app.scripts[url] = 'loading';
var event = new CustomEvent(eventName);
// Adding the script tag to the head as suggested before
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// Then bind the event to the callback function.
// There are several events for cross browser compatibility.
//script.onreadystatechange = callback;
script.onload = function() {
app.scripts[url] = true;
app.window.dispatchEvent(event);
}
// Fire the loading
head.appendChild(script);
}
};
Note that the method is now named loadScriptEvent, and that the 2nd parameter is an event name, not a callback. The idea is that even though two components could almost simultaneously make a call to this, they both listen to the same, single event. That's not enough though, the 2nd request must still be stopped if the first one is already busy loading the script. I've realized that with a loading state. You'd call this method like this:
app.utilities.loadScriptEvent('https://maps.googleapis.com/maps/api/js?v=3.exp&key=' + app.config.mapsKey, "gmloaded");
app.window.addEventListener('gmloaded',this.initMap.bind(this));
This works nicely. Component instances have no knowledge of each other, yet the script is still loaded only once, asynchronously, and we can immediately run code once it has done so.
setIntervalto check whether the first request is complete or not and if it is then initiate the second one and clear that intervaldataof the first request with that event.