Javascript using DOMContentLoaded as multiple html files needed - javascript

I have some javascript that runs nicely when added to the end of a HTML file. However, I need the same script on multiple html files so attempting to add it to js file. All the links are fine as I already have other functions added. The documentation recommends using DOMContentLoaded along with a readystate function. It prints the first console.log and 'print loading' from if statement, and then stops. Can someone please help?
function addbutton(){
let cartbutton = document.getElementsByName('ropebutton');
console.log(cartbutton) // prints node []
const cart = [];
for(var i = 0; i < cartbutton.length; i++) {
let button = cartbutton[i];
console.log(button);
button.addEventListener('click', (event) => {
console.clear();
console.log(event.target);
console.log(event.target.dataset.test);
cart.push(event.target.dataset.test);
console.log(cart)
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addbutton);
console.log("print loading");
} else {
addbutton();
console.log("print loaded");
}

Simply add the defer attribute to the style tag <script src="URL" defer></script> then you still load the script async'ed but the script will wait to be executed after all DOMContent is loaded.

Related

Html script src files that are linked to each other

(simplification) I have two javascript files I want to include. They inter-link each other.
Problem: If I just include them the following there is an error because source1.js needs something from source2.js and vice-versa.
How can I include inter-linking source files properly in HTML, without merging them? (Imaging various already-large files)
<head>
<script src="source1.js"></script>
<script src="source2.js"></script>
source1.js
function filtersomething() {
...
othersource.somefunction();
}
source2.js
var columns = {
text: filtersomething()
}
Added more to a working snippet. I'd use that code over some of the examples here. it deals more with arguments, promises, etc. Have to run, hope this helps.
You can place an event listener within each JS file, which would not call any functions until the dom is loaded. Doing this allows both JS files to load in and see the others functions available.
// script.js
window.addEventListener('DOMContentLoaded', (event) => {
filtersomething();
});
function filtersomething() {
...
othersource.somefunction();
}
Because these are loaded after script.js, script2.js always sees what script.JS has available. So, script.JS does not see what script2.JS has until after it is loaded
// script2.js
window.addEventListener('DOMContentLoaded', (event) => {
var columns = {
text: filtersomething()
}
});
We can also watch for a pointer, as suggested. This is useful when waiting for jQuery to load as well. So within your script files, watch for a property to be set, then execute.
<head>
<script>
function deferRun(thisMethod, scriptNum) {
if (window[scriptNum])
return thisMethod();
// No property found, set timeout of 50ms and try again
setTimeout(function() { defer(thisMethod, scriptNum) }, 50);
}
</script>
<script src="source1.js"></script>
<script src="source2.js"></script>
</head>
// script2.JS
// wait until script.js is available, then return result
var columns = {
text: deferRun(filtersomething, 'script1')
}
// Set our window property saying script loaded
window.script2 = true;
//script.js
function filtersomething() {
...
deferRun(othersource.somefunction, 'script2');
}
// Set our window property saying script loaded
window.script1 = true;
// Think of script1/script2 as <script> tags.
window.script1 = {
// script.js
filtersomething: () => {
return deferRun('somefunction', 'script2', 'message to use');
}
};
// Now "load" script2.js
window.script2 = {
somefunction: (msg) => {
return `msg response is ${msg}`
},
columns: {
// wait until script.js is available, then return result
text: deferRun('filtersomething', 'script1')
},
render: async() => {
console.log(await window.script2.columns.text);
}
};
(async() => {
await window.script2.render();
})();
<head>
<script>
// this is available to all and before either script 1 or 2 loads
function deferRun(thisMethod, property, argument = null) {
if (window[property])
return window[property][thisMethod](argument);
// No property found, set timeout of 50ms and try again
return setTimeout(function() {
deferRun(thisMethod, property, argument)
}, 50);
}
</script>
</head>
You can place an event listener within each JS file, which would not call any functions until the dom is loaded. Doing this allows both JS files to load in and see the others functions available.
<pre>
// script.js
window.addEventListener('DOMContentLoaded', (event) => {
filtersomething();
});
function filtersomething() {
...
othersource.somefunction();
}
```
Because these are loaded after script.js, script2.js always sees what script.JS has available. So, script.JS does not see what script2.JS has until after it is loaded
```js
// script2.js
window.addEventListener('DOMContentLoaded', (event) => {
var columns = {
text: filtersomething()
}
});
```
We can also watch for a pointer, as suggested. This is useful when waiting for jQuery to load as well. So within your script files, watch for a property to be set, then execute.
</pre>

JS DOM issue: Uncaught TypeError: Cannot read property 'addEventListener' of null becomes Uncaught Reference Error

This is code that i've downloaded from a tutorial here: (https://codepen.io/Web_Cifar/pen/PoNNEYY) in hopes of adapting it to something that i am working on. I've pieced it together and while it works in the demo (and there's a nice YT video where he goes thru it), it doesn't work for me in a live situation. I am attempting to build a Gravity Forms-like multi-page data entry form (GF has some quirks that don't work for me).
The first Javascript error that i got was: "Uncaught TypeError: Cannot read property 'addEventListener' of null". Researching that here on StackO, i got the idea that the general advice is that perhaps the DOM is not loaded for my document yet and we are calling JS before that has taken place, and two suggestions seem popular:
1). Move your JS file to the bottom of your HTML document, just before the close of your body tag (it WAS originally in my tag... moving it changed nothing).
2. Wrap your JS function in question in the following code: "window.addEventListener('DOMContentLoaded', (event) => {"
to force the DOM to load before calling the function.
Doing #2 changed the error to: ReferenceError: "changeStep" is not defined. There IS a function called 'changeStep' in the JS code. If you reference the tutorial above, you'll see that it was originally the last function defined in the JS file, so i thought that moving it to the top would change it. No dice. I've done some checks to see if some simple JQuery tests work in my environment and they do. I am relatively new to JS but i don't see why the DOM would not be loaded nor do i see why the function in question cannot be referenced.
Here is a skeleton of my HTML code:
<html>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script> /* A recommended test to see if jQuery is working, and it is. */
$(document).ready(function(){
$("button").click(function(){
alert("jQuery is working perfectly.");
});
});
</script>
<link rel="stylesheet" href="/Users/Me/Documents/multistep.css">
<body>
etc.....(moving to end of html file....)
<script type = "text/javascript" src = "/Users/Me/Documents/multistep.js" ></script>
</body>
</html>
And here is my entire JS script:
const steps = Array.from(document.querySelectorAll("form .step"));
const nextBtn = document.querySelectorAll("form .next-btn");
const prevBtn = document.querySelectorAll("form .previous-btn");
const form = document.querySelector("form");
window.addEventListener('DOMContentLoaded', (event) => {
function changeStep(btn) {
let index = 0;
const active = document.querySelector(".active");
index = steps.indexOf(active);
steps[index].classList.remove("active");
if (btn === "next") {
index++;
} else if (btn === "prev") {
index--;
}
steps[index].classList.add("active");
}
});
window.addEventListener('DOMContentLoaded', (event) => {
nextBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("next");
});
});
});
window.addEventListener('DOMContentLoaded', (event) => {
prevBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("prev");
});
});
});
window.addEventListener('DOMContentLoaded', (event) => {
form.addEventListener("submit", (e) => {
e.preventDefault();
const inputs = [];
form.querySelectorAll("input").forEach((input) => {
const { name, value } = input;
inputs.push({ name, value });
});
console.log(inputs);
form.reset();
});
});
Please note that i wrapped basically everything in the "window.addEventListener..." tag (as per advice Googled above) and the only other change i made from the referenced demo (that works in codePen) was to move the changeStep function to the first function in the file.
I'm at a loss as to what is going on. Can an experienced JS guy help me get un-stuck here?
changeStep is defined inside the anonymous callback function of the first event listener, so it is only visible inside that function. If you want to access it outside that function's scope, you have to define it outside of the function, i.e. before calling addEventListener. But a better solution is to put everything in one event listener. I do not see a reason why there have to be three of them.
Besides, since you are still calling querySelector outside the event listener, it does not solve the original problem. That is exactly the thing you want to do after the DOM context has been loaded, so you have to put it inside the event listener, too.
So the solution is to put basically everything in one event listener:
window.addEventListener('DOMContentLoaded', (event) => {
const steps = Array.from(document.querySelectorAll("form .step"));
const nextBtn = document.querySelectorAll("form .next-btn");
const prevBtn = document.querySelectorAll("form .previous-btn");
const form = document.querySelector("form");
function changeStep(btn) {
let index = 0;
const active = document.querySelector(".active");
index = steps.indexOf(active);
steps[index].classList.remove("active");
if (btn === "next") {
index++;
} else if (btn === "prev") {
index--;
}
steps[index].classList.add("active");
}
nextBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("next");
});
});
prevBtn.forEach((button) => {
button.addEventListener("click", () => {
changeStep("prev");
});
});
});
This should answer your question about why you get the second error, but actually putting the script at the end of the body tag should have solved the problem in the first place, so the first error probably has a different reason. Are you sure that your script tag was at the very end of the body tag, and that you actually have all the elements you are querying in your DOM tree?

External JavaScript with jQuery dependence being loaded before jQuery

Externally loading a script, but my script was placed by the client above jQuery (which is a requirement), as such, my script does not work.
I am trying to make my code wait until jQuery has loaded before executing, but I am having difficulty with nested functions within my code; specifically $(x).hover, or $(x).click etc.
I can separate my functions without much trouble, which include jQuery selectors (but they won't be called unless 'x y or z' is done (i.e. until after jQuery is loaded).
I don't know how to have the hover, click etc implemented as they don't work within my $(document).ready(function(){... which is located within the onload yourFunctionName described below - with thanks to user #chaos
Link to onload hook: https://stackoverflow.com/a/807997/1173155
and a quote of the above link:
if(window.attachEvent) {
window.attachEvent('onload', yourFunctionName);
} else {
if(window.onload) {
var curronload = window.onload;
var newonload = function() {
curronload();
yourFunctionName();
};
window.onload = newonload;
} else {
window.onload = yourFunctionName;
}
}
Thanks in advance for any help with this.
I have also looked into a loop that checks if jQuery is activated before continueing, but did not implement it as I found that JavaScript does not have a sufficient sleep method that sleeps that specific script.
Solution:
if(typeof jQuery === "undefined"){
if(window.attachEvent) {
window.attachEvent('onload', myLoadFunction);
} else {
if(window.onload) {
var curronload = window.onload;
var newonload = function() {
curronload();
myLoadFunction();
};
window.onload = newonload;
} else {
window.onload = myLoadFunction;
}
}
}
else {
myLoadFunction();
}

Verify External Script Is Loaded

I'm creating a jquery plugin and I want to verify an external script is loaded. This is for an internal web app and I can keep the script name/location consistent(mysscript.js). This is also an ajaxy plugin that can be called on many times on the page.
If I can verify the script is not loaded I'll load it using:
jQuery.getScript()
How can I verify the script is loaded because I don't want the same script loaded on the page more than once? Is this something that I shouldn't need to worry about due to caching of the script?
Update:
I may not have control over who uses this plugin in our organization and may not be able to enforce that the script is not already on the page with or without a specific ID, but the script name will always be in the same place with the same name. I'm hoping I can use the name of the script to verify it's actually loaded.
If the script creates any variables or functions in the global space you can check for their existance:
External JS (in global scope) --
var myCustomFlag = true;
And to check if this has run:
if (typeof window.myCustomFlag == 'undefined') {
//the flag was not found, so the code has not run
$.getScript('<external JS>');
}
Update
You can check for the existence of the <script> tag in question by selecting all of the <script> elements and checking their src attributes:
//get the number of `<script>` elements that have the correct `src` attribute
var len = $('script').filter(function () {
return ($(this).attr('src') == '<external JS>');
}).length;
//if there are no scripts that match, the load it
if (len === 0) {
$.getScript('<external JS>');
}
Or you can just bake this .filter() functionality right into the selector:
var len = $('script[src="<external JS>"]').length;
Few too many answers on this one, but I feel it's worth adding this solution. It combines a few different answers.
Key points for me were
add an #id tag, so it's easy to find, and not duplicate
Use .onload() to wait until the script has finished loading before using it
mounted() {
// First check if the script already exists on the dom
// by searching for an id
let id = 'googleMaps'
if(document.getElementById(id) === null) {
let script = document.createElement('script')
script.setAttribute('src', 'https://maps.googleapis.com/maps/api/js?key=' + apiKey)
script.setAttribute('id', id)
document.body.appendChild(script)
// now wait for it to load...
script.onload = () => {
// script has loaded, you can now use it safely
alert('thank me later')
// ... do something with the newly loaded script
}
}
}
#jasper's answer is totally correct but with modern browsers, a standard Javascript solution could be:
function isScriptLoaded(src)
{
return Boolean(document.querySelector('script[src="' + src + '"]'));
}
UPDATE July 2021:
The accepted solutions above have changed & improved much over time. The scope of my previous answer above was only to detect if the script was inserted in the document to load (and not whether the script has actually finished loading).
To detect if the script has already loaded, I use the following method (in general):
Create a common library function to dynamically load all scripts.
Before loading, it uses the isScriptLoaded(src) function above to check whether the script has already been added (say, by another module).
I use something like the following loadScript() function to load the script that uses callback functions to inform the calling modules if the script finished loading successfully.
I also use additional logic to retry when script loading fails (in case of temporary network issues).
Retry is done by removing the <script> tag from the body and adding it again.
If it still fails to load after configured number of retries, the <script> tag is removed from the body.
I have removed that logic from the following code for simplicity. It should be easy to add.
/**
* Mark/store the script as fully loaded in a global variable.
* #param src URL of the script
*/
function markScriptFullyLoaded(src) {
window.scriptLoadMap[src] = true;
}
/**
* Returns true if the script has been added to the page
* #param src URL of the script
*/
function isScriptAdded(src) {
return Boolean(document.querySelector('script[src="' + src + '"]'));
}
/**
* Returns true if the script has been fully loaded
* #param src URL of the script
*/
function isScriptFullyLoaded(src) {
return src in window.scriptLoadMap && window.scriptLoadMap[src];
}
/**
* Load a script.
* #param src URL of the script
* #param onLoadCallback Callback function when the script is fully loaded
* #param onLoadErrorCallback Callback function when the script fails to load
* #param retryCount How many times retry laoding the script? (Not implimented here. Logic goes into js.onerror function)
*/
function loadScript(src, onLoadCallback, onLoadErrorCallback, retryCount) {
if (!src) return;
// Check if the script is already loaded
if ( isScriptAdded(src) )
{
// If script already loaded successfully, trigger the callback function
if (isScriptFullyLoaded(src)) onLoadCallback();
console.warn("Script already loaded. Skipping: ", src);
return;
}
// Loading the script...
const js = document.createElement('script');
js.setAttribute("async", "");
js.src = src;
js.onload = () => {
markScriptFullyLoaded(src)
// Optional callback on script load
if (onLoadCallback) onLoadCallback();
};
js.onerror = () => {
// Remove the script node (to be able to try again later)
const js2 = document.querySelector('script[src="' + src +'"]');
js2.parentNode.removeChild(js2);
// Optional callback on script load failure
if (onLoadErrorCallback) onLoadErrorCallback();
};
document.head.appendChild(js);
}
This was very simple now that I realize how to do it, thanks to all the answers for leading me to the solution. I had to abandon $.getScript() in order to specify the source of the script...sometimes doing things manually is best.
Solution
//great suggestion #Jasper
var len = $('script[src*="Javascript/MyScript.js"]').length;
if (len === 0) {
alert('script not loaded');
loadScript('Javascript/MyScript.js');
if ($('script[src*="Javascript/MyScript.js"]').length === 0) {
alert('still not loaded');
}
else {
alert('loaded now');
}
}
else {
alert('script loaded');
}
function loadScript(scriptLocationAndName) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = scriptLocationAndName;
head.appendChild(script);
}
Create the script tag with a specific ID and then check if that ID exists?
Alternatively, loop through script tags checking for the script 'src' and make sure those are not already loaded with the same value as the one you want to avoid ?
Edit: following feedback that a code example would be useful:
(function(){
var desiredSource = 'https://sitename.com/js/script.js';
var scripts = document.getElementsByTagName('script');
var alreadyLoaded = false;
if(scripts.length){
for(var scriptIndex in scripts) {
if(!alreadyLoaded && desiredSource === scripts[scriptIndex].src) {
alreadyLoaded = true;
}
}
}
if(!alreadyLoaded){
// Run your code in this block?
}
})();
As mentioned in the comments (https://stackoverflow.com/users/1358777/alwin-kesler), this may be an alternative (not benchmarked):
(function(){
var desiredSource = 'https://sitename.com/js/script.js';
var scripts = document.getElementsByTagName('script');
var alreadyLoaded = false;
for(var scriptIndex in document.scripts) {
if(!alreadyLoaded && desiredSource === scripts[scriptIndex].src) {
alreadyLoaded = true;
}
}
if(!alreadyLoaded){
// Run your code in this block?
}
})();
Simply check if the global variable is available, if not check again. In order to prevent the maximum callstack being exceeded set a 100ms timeout on the check:
function check_script_loaded(glob_var) {
if(typeof(glob_var) !== 'undefined') {
// do your thing
} else {
setTimeout(function() {
check_script_loaded(glob_var)
}, 100)
}
}
Another way to check an external script is loaded or not, you can use data function of jquery and store a validation flag. Example as :
if(!$("body").data("google-map"))
{
console.log("no js");
$.getScript("https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&callback=initilize",function(){
$("body").data("google-map",true);
},function(){
alert("error while loading script");
});
}
}
else
{
console.log("js already loaded");
}
I think it's better to use window.addEventListener('error') to capture the script load error and try to load it again.
It's useful when we load scripts from a CDN server. If we can't load script from the CDN, we can load it from our server.
window.addEventListener('error', function(e) {
if (e.target.nodeName === 'SCRIPT') {
var scriptTag = document.createElement('script');
scriptTag.src = e.target.src.replace('https://static.cdn.com/', '/our-server/static/');
document.head.appendChild(scriptTag);
}
}, true);
Merging several answers from above into an easy to use function
function GetScriptIfNotLoaded(scriptLocationAndName)
{
var len = $('script[src*="' + scriptLocationAndName +'"]').length;
//script already loaded!
if (len > 0)
return;
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = scriptLocationAndName;
head.appendChild(script);
}
My idead is to listen the error log if there is an error on script loading.
const checkSegmentBlocked = (e) => {
if (e.target.nodeName === 'SCRIPT' && e.target.src.includes('analytics.min.js')) {
window.isSegmentBlocked = true;
e.target.removeEventListener(e.type, checkSegmentBlocked);
}
};
window.addEventListener('error', checkSegmentBlocked, true);
Some answers on this page are wrong. They check for the existence of the <script> tag - but that is not enough. That tells you that the tag was inserted into the DOM, not that the script is finished loading.
I assume from the question that there are two parts: the code that inserts the script, and the code that checks whether the script has loaded.
The code that dynamically inserts the script:
let tag = document.createElement('script');
tag.type = 'text/javascript';
tag.id = 'foo';
tag.src = 'https://cdn.example.com/foo.min.js';
tag.onload = () => tag.setAttribute('data-loaded', true); // magic sauce
document.body.appendChild(tag);
Some other code, that checks whether the script has loaded:
let script = document.getElementById('foo');
let isLoaded = script && script.getAttribute('data-loaded') === 'true';
console.log(isLoaded); // true
If the both of those things (inserting and checking) are in the same code block, then you could simplify the above:
tag.onload = () => console.log('loaded');
I found a quick tip before you start diving into code that might save a bit of time. Check devtools on the webpage and click on the network tab. The js scripts are shown if they are loaded as a 200 response from the server.

While using the YouTube api, the loadvideo function wont work unless I alert the video id first

function playPlaylist(trackstemp) {
trackstemp = trackstemp.split(' ');
for (i=0; i < trackstemp.length; i++){
tracks[i] = trackstemp[i];
}
numoftracks = tracks.length - 1;
currenttrack = 0;
loadNewVideo(tracks[currenttrack])
}
function loadNewVideo(id) {
ytplayerid.loadVideoById(id, 0);
}
I have a prev() and next() function that work just fine by calling loadNewVideo(tracks[currenttrack]), but the initial video only works if I alert(id) within the loadNewVideo() function.
Any clue to why this is happening?
Are you using onYouTubePlayerReady which tells you when the player is ready?
function onYouTubePlayerReady(){
//Call you first track here
}
Mostly likely the effect of the alert is to delay the execution of ytplayerid.loadVideoById(id, 0); so that it satisfies a race condition in your code - for example, the function doesn't exist yet, or some dependency in the function is not yet settled.
Make sure that the first loadNewVideo happens after all other scripts are loaded and the DOM is ready (ie, by attaching the function to the window.load event).
Separate this out and put in that head in its own script tags.
<script type="text/javascript" language="javascript">
function onYouTubePlayerReady(playerid) {
ytp = document.getElementById('ytplayer');
ytp.mute();
};
</script>

Categories