I'm a beginner in React/Node.js and i got a problem on my custom shopify app dev.
i have to import, load and save an external js file on my current shoppify shop from the back.
In my react project, i could import the script via an init on button click as :
const onClick = () => {
if (typeof window !== "undefined") {
addScript({
src: `https://sdk.com/loader.js`,
id: "script",
async: "true",
onLoad: () => {
console.log("script initialized!");
},
});
}
};
But when i refresh, close and reopen, i have to re-import the script.
My question is, how can i adjust my code to save the script on my DOM.
Thank you.
You can try something like this
const onClick = () => {
const exists = document.getElementById("myScript");
if (!exists) {
const script = document.createElement("script");
script.src = "https://sdk.com/loader.js";
script.id = "myScript";
document.body.appendChild(script);
script.onload = () => {
console.log("Script loaded")
}
}
}
Related
So I have a Single Page JavaScript web app that I've built from a tutorial HERE.
It's working well - I've integrated a few changes that people had in the Issues and Pull Requests on the GitHub link as well.
My links are also working well. If you click on a <a href> link, it redirects without reloading the whole page. AKA It's working as expected!
My problem comes when I want to use Javascript to change the page location. IE: As a result of a function/result of API call/something else, I want to be able to force the page to navigate to a different page without reloading the whole screen.
I'm hoping that I have all the required code below for someone to be able to help... Fingers crossed! Please let me know if I've missed something that you need to help.
My thought process goes along the lines of:
The singlePageApp.js adds an event listener to the <a href='' data-link> tags that calls navigateTo
I just need to be able to somehow call that navigateTo function from somewhere else
I don't fully understand the code on that page enough to be able to make it available to the rest of the app because I can't just call navigateTo('/whatever'); from any script. It results in a function not defined error.
What have I tried?
A lot - and I've forgotten what, but a few things
history.pushState(null, null, url); from a script
navigateTo('/whatever'); from a script
window.location.pathname = '/whatever'; router() <- This just results in a router not defined message
Other notes
I'm not using react or other frameworks. Straight Javascript.
I have jquery installed if it helps - Only reason is as a dependance on dataTables, but I'm not using it anywhere else.
Thank you VERY much in advance
Working links in a NAV bar
<a class="dropdown-item" href="#" data-link>List all services</a>
<a class="dropdown-item" href="/service/vm365/step1" data-link>Provision Service</a>
index.html
<script type="module" src="/static/js/singlePageApp.js"></script>
singlePageApp.js
import ServiceVM365Step1 from "./views/ServiceVM365Step1.js";
import ServiceVM365Step2 from "./views/ServiceVM365Step2.js";
import ServiceVM365Step3 from "./views/ServiceVM365Step3.js";
import ServiceVM365Step4 from "./views/ServiceVM365Step4.js";
import Dashboard from "./views/Dashboard.js";
const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");
const getParams = match => {
const values = match.result.slice(1);
const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]);
return Object.fromEntries(keys.map((key, i) => {
return [key, values[i]];
}));
};
const navigateTo = url => {
history.pushState(null, null, url);
router();
};
const router = async () => {
const routes = [
{ path: "/", view: Dashboard },
{ path: "/service/vm365/step1", view: ServiceVM365Step1 },
{ path: "/service/vm365/step2", view: ServiceVM365Step2 },
{ path: "/service/vm365/step3", view: ServiceVM365Step3 },
{ path: "/service/vm365/step4", view: ServiceVM365Step4 }
];
// Test each route for potential match
const potentialMatches = routes.map(route => {
return {
route: route,
result: location.pathname.match(pathToRegex(route.path))
};
});
var match
potentialMatches.forEach(entry => {
if (entry.result != null) {
match = (match === undefined || entry.route.path.length > match.route.path.length) ? entry : match
}
})
if (!match) {
match = {
route: routes[0],
result: [location.pathname]
};
}
const view = new match.route.view(getParams(match));
document.querySelector("#content").innerHTML = await view.getHtml();
};
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", e => {
if (e.target.matches("[data-link]")) {
e.preventDefault();
navigateTo(e.target.href);
}
});
router();
});
//////////////////////////////////////////////////////////////////
//// CODE BELOW WAS ME TRYING TO GET IT WORKING... It doesn't ////
var oldHref = document.location.href;
window.addEventListener("load", () => {
var bodyList = document.querySelector("body")
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
//////////////////////////////////////////
//NOTE: This does get called when a <a href> link is clicked, but not when ONLY history.pushState is used to change the path from a script
console.error("CALLED NOW")
router();
}
});
});
var config = {
childList: true,
subtree: true
};
observer.observe(bodyList, config);
});
I'm trying to import a script called Mollie (used for payments), but I'm not sure how to do it in React.
Normally in Javascript you would do something like this:
<html>
<head>
<title>My Checkout</title>
</head>
<body>
<script src="https://js.mollie.com/v1/mollie.js"></script>
</body>
</html>
I've tried this (according to other Stackoverflow posts)
useEffect(() => {
const script = document.createElement("script");
script.src = "https://js.mollie.com/v1/mollie.js";
script.addEventListener("load", () => setScriptLoaded(true));
document.body.appendChild(script);
}, []);
const mollie = Mollie(); // Mollie is not defined
But then Mollie is undefined. Can anyone point in the right direction on how to import Mollie in React?
I'm following this guide (but it's for standard Javascript)
You can easily install this package from npmjs.com where you can find necessary documentations and examples to get started. Installation:
npm i #mollie/api-client
the point here is that the effect is being invoked after the react component mounted and rendered to the user.
The next line where you are trying to call Mollie in fact running earlier when component is being constructed but not rendered yet.
There are multiple options what you can do about it:
Import script in the index.html file as you do for standard non-React solution. There should be a "public" folder containing this file in case of create-react-app usage or other place in case of custom project setup. The HTML should exist in any form even in case it's being generated on the server side dynamically. A mollie instance can later be created in the component or globally.
Use multiple effects in the React component: one to load Mollie and another one to use it when loaded:
// MollieExample.jsx
const MollieExample = () => {
const [mollie, setMollie] = useState();
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://js.mollie.com/v1/mollie.js';
script.addEventListener("load", () => {
setMollie(window.Mollie(/* Mollie arguments */);
});
document.body.appendChild(script);
}, []);
useEffect(() => {
if (mollie) {
console.log('Mollie exists here');
console.log(typeof mollie);
}
} , [mollie]);
return <p>typeof mollie: {typeof mollie}</p>;
};
Use dynamic script loading in case it is required with globally shared Mollie instance via custom hook:
// useMollie.js
let molliePromise;
const useMollie = (effect, deps) => {
const [mollie, setMollie] = useState();
const mollieCb = useCallback((mollie) => effect(mollie), deps);
useEffect(() => {
if (!molliePromise) {
molliePromise = new Promise((resolve) => {
const script = document.createElement("script");
script.src = "https://js.mollie.com/v1/mollie.js";
script.addEventListener("load", () => {
resolve(window.Mollie(/* Mollie arguments */);
});
document.body.appendChild(script);
});
}
molliePromise.then((mollie) => setMollie(mollie));
}, []);
useEffect(() => {
if (mollie) {
mollieCb(mollie);
}
}, [mollie, mollieCb]);
};
// MollieConsumer.jsx
const MollieConsumer = () => {
useMollie((mollie) => {
console.log('Mollie exists here');
console.log(typeof mollie);
}, [/* useMollie effect callback dependencies array */]);
return (
<p>Mollie consumer</p>
);
};
// App.jsx
function App() {
/* Both consumers use the same instance of Mollie */
return (
<div>
<MollieConsumer/>
<MollieConsumer/>
</div>
);
}
I assume you will end up with using some middle option. For instance, with importing script in the index.html (or any other sort of the HTML page you have containing the React application host element) and global hook.
Background
I am using ZenDesk's Web Widget (Classic) to show restricted content on the page. As per documentation
You can specify settings for the widget by defining a
window.zESettings object. Make sure you add any settings before the
Web Widget (Classic) snippet.
That means window.zESettings must be set BEFORE script is loaded. The Web Widget (Classic) snippet is loaded using script URL looks like
https://static.zdassets.com/ekr/snippet.js?key=xyz
For restricted content to show inside the widget, I will have to set jwt token to window.zESettings and that needs to happen before script is loaded. The jwt token is generated on server.
Zendesk expect that window.zESettings needs to be set and the script needs to be loaded on each page. Which is kind of ridiculous because 90+% of the time user would not use Zendesk help on the page.
Issue
I would like do it on demand when user clicks on a button.
$("#myHelpWidget").click(function () {
window.zESettings = {
webWidget: {
authenticate: {
jwtFn: function (callback) {
// Fetch your jwt token and then call our supplied callback below.
$.ajax({
type: "POST",
url: "/myaccount/jwtticket"
})
.done(function (data, textStatus, jqXHR) {
callback(data.Token);
})
}
},
contactForm: {
attachments: true,
fields: [
{ id: 'name', prefill: { '*': 'foo bar' } },
{ id: 'email', prefill: { '*': 'foobar#example.com' } }
]
}
}
};
// dynamically create script tag here
var s;
s = document.createElement('script');
s.id = "ze-snippet";
var head = document.getElementsByTagName("head")[0];
head.appendChild(s);
s.type = 'text/javascript';
s.src = "https://static.zdassets.com/ekr/snippet.js?key=xyz";
//ERROR: Get error at line below zE is not defined ****
zE('webWidget', 'show');
zE('webWidget', 'open');
zE('webWidget:on', 'close', function () {
zE('webWidget', 'hide');
});
})
The code above throws error zE is not defined.
Is there a way to inject a script tag into header and wait till its loaded completely. (Note that jQuery's $.getScript did not work.)
Put all the zE calls in the script.load event:
var s;
s = document.createElement('script');
s.id = "ze-snippet";
var head = document.getElementsByTagName("head")[0];
head.appendChild(s);
s.type = 'text/javascript';
s.src = "https://static.zdassets.com/ekr/snippet.js?key=xyz";
s.onload = function() {
zE('webWidget', 'show');
zE('webWidget', 'open');
zE('webWidget:on', 'close', function () {
zE('webWidget', 'hide');
});
}
I'm using the React useScript hook (from useHooks website). It allows to easily load external scripts and cache them once loaded.
It works fine however I found an edge-case causing me some issues..
The problem is with the caching of scripts.
If I have a component loaded 2 times in a page using useScript as below:
const ScriptDemo = src => {
const [loaded, error] = useScript("https://hzl7l.codesandbox.io/test-external-script.js");
return (
<div>
<div>
Script loaded: <b>{loaded.toString()}</b>
</div>
<br />
{loaded && !error && (
<div>
Script function call response: <b>{TEST_SCRIPT.start()}</b>
</div>
)}
</div>
);
};
function App() {
return (
<div>
<ScriptDemo />
<ScriptDemo />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can see and reproduce here: https://codesandbox.io/s/usescript-hzl7l
If my App only have one ScriptDemo it's fine, however having two or more would make it fails.
Indeed the flow will be:
ScriptDemo -> is script cached ? no -> add script to cache -> fetch it -> render
ScriptDemo2 -> is script cached ? yes -> render (but it's not finish loading
yet ..)
One way to fix it is to change the useScript hook to only cache the script after a successful onScriptLoad callback.
The issue with this approach is that the external script will be called twice.
See here: https://codesandbox.io/s/usescript-0yior
ScriptDemo -> is script cached ? no -> fetch it -> add script to cache -> render
ScriptDemo -> is script cached ? no -> fetch it -> add script to cache -> render
I thought about caching the script src AND a loading boolean but then it implies setting up a timeout handling and it gets very complex in my opinion.
So, what is the best way to update the hook in order to load the external script only once but ensuring it's correctly loaded?
In useScript module, we will need to keep track of status of loading the script.
So instead of cachedScripts being simple array of strings, we now need to keep an object representing the status of loading.
This modified implementation of useScript will address the issue:
import { useState, useEffect } from 'react';
let cachedScripts = {};
export function useScript(src) {
// Keeping track of script loaded and error state
const [state, setState] = useState({
loaded: false,
error: false
});
useEffect(
() => {
const onScriptLoad = () => {
cachedScripts[src].loaded = true;
setState({
loaded: true,
error: false
});
};
const onScriptError = () => {
// Remove it from cache, so that it can be re-attempted if someone tries to load it again
delete cachedScripts[src];
setState({
loaded: true,
error: true
});
};
let scriptLoader = cachedScripts[src];
if(scriptLoader) { // Loading was attempted earlier
if(scriptLoader.loaded) { // Script was successfully loaded
setState({
loaded: true,
error: false
});
} else { //Script is still loading
let script = scriptLoader.script;
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
} else {
// Create script
let script = document.createElement('script');
script.src = src;
script.async = true;
// Script event listener callbacks for load and error
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
// Add script to document body
document.body.appendChild(script);
cachedScripts[src] = {loaded:false, script};
// Remove event listeners on cleanup
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
},
[src] // Only re-run effect if script src changes
);
return [state.loaded, state.error];
}
Edit:
Went to GitHub page of useHooks to suggest this improvement and found some one has already posted similar fix:
https://gist.github.com/gragland/929e42759c0051ff596bc961fb13cd93#gistcomment-2975113
I need your help very much...
I have a Vue component and endpoint which gives me a script with a small menu and that script includes some actions.
So, after my script is loaded its actions don't work on a page and I don't know why.
Below the example code:
<template>
<div id="menu"></div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
mounted () {
// here I get a script which contains menu bar with actions
// css and styles
this.$http.get("https://endpointwithscript.com/menu").then(response => {
if (response.status === 200) {
//in that case script doesn't work. Eg click
var div = document.getElementById("menu")
div.innerHTML += response.body;
document.appendChild(div);
}
})
}
}
</script>
If I insert my downloaded script that way :
mounted () {
this.$http.get("https://endpointwithscript.com/menu").then(response => {
if (response.status === 200) {
document.write(response.body);
}
})
}
then script works but another html elements are overridden by that script and not displayed.
How to download script, insert it on a page and keep all functionality ?
Thank you!
You can try adding your external script into Mounted()
mounted() {
let yourScript= document.createElement('script')
yourScript.setAttribute('src', 'https://endpointwithscript.com/menu')
document.head.appendChild(yourScript)
},
<div class="menu"></div>
var div = document.getElementById("menu")
You are grabbing the element by ID, but gave it a class instead. Change the div to be <div id="menu"></div>
You could use v-html to include HTML-Code into a specific tag.
It seems the only way to implement a working script is by defining a new script element.
This worked for me here
https://jsfiddle.net/nh475cq2/
new Vue({
el: "#app",
mounted () {
this.$http.get("https://gist.githubusercontent.com/blurrryy/8f7e9b6a74d1455b23989a7d5b094f3f/raw/921937ea99ff233dfc78add22e18868b32cd11c0/Test").then(res => {
if(res.status === 200) {
let scr = document.createElement('script');
scr.innerHTML = res.body;
document.body.appendChild(scr);
}
})
}
})