Adding multiple <script>s to a specific page in Gatsby - javascript

I'm trying to add two scripts to a specific page in a gatsby.js app.
The page is /apply, and the second script depends on the first (the first script must be loaded before the second).
Of course this is straightforward on a traditional site as the scripts would be loaded synchronously in order. But in react-helmet, the scripts are loaded asynchronously so my second script errors (its trying to call a function in the first before the first is loaded).
I've taken a hook from https://usehooks.com/useScript/ and am trying to get things working.
If I inspect the page source after load, both scripts are present but I still get errors in the console (as if script2 is trying to run before script1).
myPage.js
const Apply = () => {
const scriptLoaded = useScript("https://myscripturl/script1.js");
if(scriptLoaded !== "ready"){
return <>Not loaded</>
}
return (
<>
<Helmet>
{scriptLoaded === "ready" &&
<script src="https://myscripturl/script2.js"></script>
}
</Helmet>
<!-- The rest of the page -->
</>
)
}
useScript.js
// taken from https://usehooks.com/useScript/
function useScript(src) {
// Keep track of script status ("idle", "loading", "ready", "error")
const [status, setStatus] = useState(src ? "loading" : "idle");
useEffect(
() => {
// Allow falsy src value if waiting on other data needed for
// constructing the script URL passed to this hook.
if (!src) {
setStatus("idle");
return;
}
// Fetch existing script element by src
// It may have been added by another intance of this hook
let script = document.querySelector(`script[src="${src}"]`);
if (!script) {
// Create script
script = document.createElement("script");
script.src = src;
script.async = true;
script.setAttribute("data-status", "loading");
// Add script to document body
document.body.appendChild(script);
// Store status in attribute on script
// This can be read by other instances of this hook
const setAttributeFromEvent = (event) => {
script.setAttribute(
"data-status",
event.type === "load" ? "ready" : "error"
);
};
script.addEventListener("load", setAttributeFromEvent);
script.addEventListener("error", setAttributeFromEvent);
} else {
// Grab existing script status from attribute and set to state.
setStatus(script.getAttribute("data-status"));
}
// Script event handler to update status in state
// Note: Even if the script already exists we still need to add
// event handlers to update the state for *this* hook instance.
const setStateFromEvent = (event) => {
setStatus(event.type === "load" ? "ready" : "error");
};
// Add event listeners
script.addEventListener("load", setStateFromEvent);
script.addEventListener("error", setStateFromEvent);
// Remove event listeners on cleanup
return () => {
if (script) {
script.removeEventListener("load", setStateFromEvent);
script.removeEventListener("error", setStateFromEvent);
}
};
},
[src] // Only re-run effect if script src changes
);
If I place both scripts in the index.html in /public, the code works without issue. But of course it runs on every route in the app which is no good. Is what I'm trying to do even possible?
Thanks for any help.

You can use Gatsby's Script API: https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-script/
It also has a section about loading script dependently: https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-script/#loading-scripts-dependently
So your code could be:
import React, { useState } from "react"
import { Script } from "gatsby"
function Apply() {
const [loaded, setLoaded] = useState(false)
return (
<>
<Script src="https://myscripturl/script1.js" onLoad={() => setLoaded(true)} />
{loaded && <Script src="https://myscripturl/script2.js" />}
</>
)
}
export default Apply

Related

addEventListener won't be triggered at the first click

I'm trying to load a js script when a button(in a React component) is clicked.
I use webpack import to load the JS file.
Not sure how come the addEventListener won't be triggered at the first click.
I add cosole.log for debugging. when clicking that button first time, will only get TEST_1.
And, if click that button second time, will get TEST_2 and TEST_3 as well.
React Component snippet
const Component = () => {
const loadJS = () => import(/* webpackChunkName: "od-survey" */ "./ol_od_survey.js");
return (
<>
<button
className={"button"}
onClick={loadJS}
>
Click Me
</button>
</>
);
};
export default Component;
JS file snippet
function(w, o) {
"use strict";
console.log("TEST_1");
w.addEventListener("click", function(event) {
console.log("TEST_2");
if ( event.target.textContent === "Click Me") {
console.log("TEST_3");
actionFunc();
}
});
var actionFunc = function() {
Do Someting Here
}
})(window, window.OOo);
Click the button to load that JS file
As discussed in the comments the reason why you need 2 click, because you are attaching your "loaded" eventListener within the first click, so it just cannot be triggered, only subsequent clicks will trigger that listenet.
Also, as discussed if what you need is to execute actionFunc() first time when script isn't loaded yet then you could just execute it manually on script load by adding call to actionFunc() at script end
function(w, o) {
"use strict";
console.log("TEST_1");
w.addEventListener("click", function(event) {
console.log("TEST_2");
if ( event.target.textContent === "Click Me") {
console.log("TEST_3");
actionFunc();
}
});
var actionFunc = function() {
Do Someting Here
}
//manually execute actionFunc on first load
actionFunc();
})(window, window.OOo);
Other "hacky" way would be to use ref on button and trigger click on it after import finished import(/* webpackChunkName: "od-survey" */ "./ol_od_survey.js").then(_ => btnRef.current.click());

How to use preventDefault function in useRef (reactjs + nodejs)

This is my function to download zip file
autoDownload.current.click() - automatically click on HTML element which download my zip file.
But Problem is page used to reload while this process.
How i can prevent my page to reload.
const downloadZipFile = () => {
console.log('download');
autoDownload.current.click();
}
I need something like this.
const downloadZipFile = () => {
console.log('download');
autoDownload.current.click((e) => {
e.preventDefalut();
});
}
Have you tried adding an 'onClick' property to the link/button with a function that includes the e.preventDefault() ? Not sure exactly how you're setting this up, but below has some ideas (using reactjs).
const downloadZipFile = (e) => {
e.preventDefault()
// Add your download zip functionality here
}
// Example of function that will execute on page load
React.useEffect(() => {
downloadZipFile()
}, [])
return (
<Button id='someid' onClick={downloadZipFile} />
)

why onload function is being called twice, though component is rendering once?

I am having a simple react component, the fire is printed once, but injectWindowInterval is called twice event though i am setting flag value, why is it so?
const Header = () => {
let flag = true;
console.log("fire");
function injectWindowInterval() {
if (flag && window.google) {
flag = false;
console.log(window.google);
}
}
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.onload = injectWindowInterval;
script.async = true;
document.querySelector("body")?.appendChild(script);
return (
<div className="header">
<h3 className="header__title">RE</h3>
</div>
);
};
Update: for some reason, the script is appending twice in body.
In some cases function are called on render when they are defined. One common example is with onClick methods for buttons
<button onClick={onClickFunction}>Click me</button> //fires on render
<button onClick={()=>onClickFunction()}>Click me</button> //no fire on render, works well
So maybe try
const Header = () => {
let flag = true;
console.log("fire");
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.onload = () => {
if (flag && window.google) {
flag = false;
console.log(window.google);
}
};
script.async = true;
document.querySelector("body")?.appendChild(script);
return (
<div className="header">
<h3 className="header__title">RE</h3>
</div>
);
};
If you are rendering the Component in <React.StrictMode>, the component will render twice for sure in development mode.
I tried rendering above component(Header) and the fire is logged in the console twice so do the script tag appended twice.
Here is the console Output
Note : Also note that React renders component twice only in development mode. Once you build the app the component will be rendered only once.

Injected event listeners in Vue JS project don't always fire on page change

I'm working with a Nuxt JS / Vue JS app with dynamic pages. My pages fetch data from a remote api which includes HTML and JS that needs to be executed.
In my context, I have a bunch of accordions, that when tapped should open the contents, so in the then block of my axios request I'm creating a script tag on the page with the contents:
const string = "some JS from api"
const injectTo = document.querySelector('[data-beam-injectes-interactivity]')
const script = document.createElement('script')
script.innerHTML = string
injectTo.appendChild(script)
The string of JS injected is:
function initDeviceGroupCollapses () {
const buttons = document.querySelectorAll('[data-toggle="collapse"]')
if (buttons) {
for (const [index, button] of buttons.entries()) {
if (button) {
button.addEventListener('click', (event) => {
event.stopPropagation()
const target = event.target.dataset.target
if (!target) {
return
}
const collapse = document.querySelector(target)
collapse.style.display = collapse.style.display == 'none' ? 'block' : 'none'
}, false)
}
}
}
}
The issue I'm having though is that if the JS is injected into a page where the HTML doesn't initially exist, the event listeners no longer fire on data-toggle="collapse".
How can I always make sure that the injected JS works?

Event Listener not firing from JS script within HTML Import

I am importing form.html into index.html with the following function:
function importHTML() {
let link = document.createElement('link');
link.rel = 'import';
link.href = 'form.html';
link.onload = (e) => {
console.log('Successfully loaded import: ' + e.target.href);
importContent();
}
link.onerror = (e) => {
console.log('Error loading import: ' + e.target.href);
}
document.head.appendChild(link);
let importContent = () => {
let importContent = document.querySelector('link[rel="import"]').import;
if (importContent != null) {
let el = importContent.querySelector('#formContainer');
let container = document.body.querySelector('main');
container.appendChild(el.cloneNode(true));
}
}
}
This works to creates a new link rel="import" tag, appending it to the head of index.html. When the link has completed loading, the content from form.html is appended to the main body container.
Inside form.html I have a script that gets a handle to a pagination element to attach an event handler:
<section id="formContainer">
<form>
...
</form>
<!-- NOTE: pagination controls -->
<div class="pagination">
<span id="pageBack"><i><</i></span>
<span id="pageForward"><i>></i></span>
</div>
<script>
let importDoc = document.currentScript.ownerDocument;
let pageForward = importDoc.querySelector('#pageForward');
let pageBack = importDoc.querySelector('#pageBack');
// these elements are present in the console at runtime
console.log(pageForward, pageBack);
pageForward.addEventListener('click', (e) => {
console.log('click event heard on pageBack');
});
pageBack.addEventListener('click', (e) => {
console.log('click event heard on pageBack');
});
</script>
</section>
The issue I'm having is that the Event Listeners are not firing despite the console showing no errors.
I thought this might have something to do with load order and experimented around with this a little bit, making sure that the import loads before the script is parsed though I'm not 100% on whether or not this is working as expected.
I've found it works to move my acting script into the main document by dynamically loading it after the importContent() function but I'd prefer to keep the form's associated script encapsulated within the Import.
Thoughts?
The Event Listeners are attached to the wrong element. In your example, they are set on the <span> elements in the imported document.
But these elements are cloned and the <span> elements that are clicked are the cloned elements with no set Event Listeners.
To make the code work, you should query the elements form the <body> instead of querying the imported document.
In form.html:
<script>
let importDoc = document.currentScript.ownerDocument
let el = importDoc.querySelector( '#formContainer' )
let container = document.body.querySelector( 'main ' )
container.appendChild( el.cloneNode( true ) )
let pageForward = container.querySelector( '#pageForward' )
let pageBack = container.querySelector( '#pageBack')
// these elements are present in the console at runtime
console.log(pageForward, pageBack);
pageForward.addEventListener('click', e =>
console.log( 'click event heard on pageBack' )
)
pageBack.addEventListener('click', e =>
console.log( 'click event heard on pageBack' )
)
</script>
NB : the in the imported document is executed as soon as the document is imported. No need to wait for a onload event and call a callback from the main document.
If you want to defer the execustion of the script, you'll need to put it in a <template> element.

Categories