I'm having troubles loading the content of an HTML file in a Vue component. Basically i have a Django backend that generates an HTML file using Bokeh and a library called backtesting.py. My frontend is using Nuxt/Vue, so i can't just load the HTML on the page dynamically.
Here is what the HTML file looks like (it was too long to post here): https://controlc.com/aad9cb7f
The content of that file should be loaded in a basic component:
<template>
<div>
<h1>Some content here</h1>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
The problem is that i really don't know how to do that. If i just copy and paste the content in the vue component, i'll get a lot of error due to the fact that i'm using a <script> tag in a component. The only thing i managed to do was to load the BokehJS CDN in my index.html file, but even after that i'll get a Bokeh is undefined error in the component.
What can i do to accomplish this? Any kind of advice is appreciated
Tao's answer is spot on and is very similar to how I've solved this issue for myself in the past.
However, I'd like to throw in an alternative iframe approach that could work in case reactivity is important. Here's a codesandbox link
The only difference is that this approach loads the code/HTML via XHR and writes it manually into the iframe. Using this approach, you should be able to add some reactivity if necessary.
<script>
export default {
components: {},
data() {
return {};
},
async mounted() {
this.initialize();
},
methods: {
async initialize() {
const html = await this.loadHTML();
const doc = this.htmlToDocument(html);
this.updateIframe(doc);
},
async loadHTML() {
const response = await fetch("/plot");
const text = await response.text();
return text;
},
htmlToDocument(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
},
updateIframe(doc) {
const iframe = this.$refs.frame;
const iframeDocument = iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(doc.documentElement.innerHTML);
iframeDocument.close();
}
},
};
</script>
In the codesandbox, I've thrown in two additional methods to give you an example of how reactivity can work with this approach:
modify() {
if (this.orig) {
// Only for the purpose of this example.
// It's already been modified. Just short-circuit so we don't overwrite it
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
// Store old data so we can reset it
this.orig = data.Close[idx];
data.Close[Math.floor(data.Close.length / 2)] = 0;
modelWithData.change.emit();
},
reset() {
if (!this.orig) {
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
data.Close[idx] = this.orig;
modelWithData.change.emit();
delete this.orig;
}
Probably the simplest way is to make your HTML available at the URL of your choice, on your server (regardless of Vue).
Then, in your app, use an <iframe> and point its src to that html. Here's an example, using codesandbox.io, where I placed what you posted into the index.html. Below you can see it working with both <iframe> and <object> tags:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
'el': '#app'
})
body {
margin: 0;
}
h1, h3 {padding-left: 1rem;}
object, iframe {
border: none;
height: 800px;
width: 100%;
min-height: calc(100vh - 125px);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>This content is placed in Vue</h1>
<h3>Vue doesn't really care.</h3>
<iframe src="https://1gk6z.csb.app/"></iframe>
<h1><code><object></code> works, too:</h1>
<object type="text/html" data="https://1gk6z.csb.app/"></object>
</div>
Note: if the domain serving the graph and the one displaying it differ, you'll need server-side configuration to allow the embed (most domains have it turned off by default).
Strategy:
insert and init bokeh in head tag of public/index.html
read file in a string via ajax/xhr and parse as dom tree
extract each needed dom element from the parsed tree
recreate and append each element
No iframe needed. window.Bokeh is directly accessible.
A skeletal example of reactivity is suggested through the method logBkh that logs the global Bokeh object when clicking on the graph
<template>
<div id="app">
<div id="page-container" #click="logBkh"></div>
</div>
</template>
<script>
// loaded from filesystem for test purposes
import page from 'raw-loader!./assets/page.txt'
// parse as dom tree
const extDoc = new DOMParser().parseFromString(page, 'text/html');
export default {
methods: {
logBkh(){
console.log(window.Bokeh)
}
},
mounted() {
const pageContainer = document.querySelector('#page-container')
// generate and append root div
const dv = document.createElement('div')
const { attributes } = extDoc.querySelector('.bk-root')
for(const attr in attributes) {
dv.setAttribute(attributes[attr].name, attributes[attr].value)
}
pageContainer.append(dv)
for(const _scrpt of extDoc.body.querySelectorAll('script')) {
// generate and append each script
const scrpt = document.createElement('script')
for(const attr in _scrpt.attributes) {
scrpt.setAttribute(
_scrpt.attributes[attr].name,
_scrpt.attributes[attr].value
)
}
scrpt.innerHTML = _scrpt.innerHTML
pageContainer.append(scrpt)
}
}
}
</script>
result:
Related
I have a web-component at root level. The simplified version of which is shown below:
class AppLayout {
constructor() {
super();
this.noShadow = true;
}
connectedCallback() {
super.connectedCallback();
this.render();
this.insertAdjacentHTML("afterbegin", this.navigation);
}
render() {
this.innerHTML = this.template;
}
get template() {
return `
<h1>Hello</h1>
`;
}
navigation = `
<script type="module">
import './components/nav-bar.js'
</script>
`;
}
customElements.define('app-layout', AppLayout);
I want to load a script after this component loads. The script creates html for navigation and tries to add it to the app-layout element shown above. However, even though, it does find the app-layout element, it is unable to append the navBar element. It is, however, able to append the navBar to the body of the html. Any ideas what I'm missing.
const navLinks =
`<ul>
<li>Some</li>
<li>Links</li>
</ul>
`;
const navBar = document.createElement('nav');
navBar.innerHTML = navLinks;
const appLayout = document.querySelector('app-layout'); // works with 'body' but not with 'appLayout'
console.log(appLayout); // Logs correct element
appLayout.appendChild(navBar);
I know that what I'm trying to do here (loading a script inside a web component) is not ideal, however, I would like to still understand why the above doesn't work.
using innerHTML or in your case insertAdjacentHTML to add <script> tags to the document doesn't work because browsers historically try to prevent potential cross site script attacks (https://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0)
What you could do is something like:
const s = document.createElement("script");
s.type = "module";
s.innerText = `import './components/nav-bar.js'`;
this.append(s);
// or simply directly without the script: `import('./comp..')` if it is really your only js in the script tag.
I have scripts In my React app that are inserted dynamically later on. The scripts don't load.
In my database there is a field called content, which contains data that includes html and javascript. There are many records and each record can include multiple scripts in the content field. So it's not really an option to statically specify each of the script-urls in my React app. The field for a record could for example look like:
<p>Some text and html</p>
<div id="xxx_hype_container">
<script type="text/javascript" charset="utf-8" src="https://example.com/uploads/hype_generated_script.js?499892"></script>
</div>
<div style="display: none;" aria-hidden="true">
<div>Some text.</div>
Etc…
I call on this field in my React app using dangerouslySetInnerHTML:
render() {
return (
<div data-page="clarifies">
<div className="container">
<div dangerouslySetInnerHTML={{ __html: post.content }} />
... some other data
</div>
</div>
);
}
It correctly loads the data from the database and displays the html from that data. However, the Javascript does not get executed. I think the script doesn't work because it is dynamically inserted later on. How can I make these scripts work/run?
This post suggest a solution for dynamically inserted scripts, but I don't think I can apply this solution because in my case the script/code is inserted from a database (so how to then use nodeScriptReplace on the code...?). Any suggestions how I might make my scripts work?
Update in response to #lissettdm their answer:
constructor(props) {
this.ref = React.createRef();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.postData !== this.props.postData) {
this.setState({
loading: false,
post: this.props.postData.data,
//etc
});
setTimeout(() => parseElements());
console.log(this.props.postData.data.content);
// returns html string like: `<div id="hype_container" style="margin: auto; etc.`
const node = document.createRange().createContextualFragment(this.props.postData.data.content);
console.log(JSON.stringify(this.ref));
// returns {"current":null}
console.log(node);
// returns [object DocumentFragment]
this.ref.current.appendChild(node);
// produces error "Cannot read properties of null"
}
}
render() {
const { history } = this.props;
/etc.
return (
{loading ? (
some code
) : (
<div data-page="clarifies">
<div className="container">
<div ref={this.ref}></div>
... some other data
</div>
</div>
);
);
}
The this.ref.current.appendChild(node); line produces the error:
TypeError: Cannot read properties of null (reading 'appendChild')
If your are sure about HTML string content is safety and contains a string with valid HTML you can use Range.createContextualFragment() (executes scripts 🚨)
function App() {
const ref = useRef();
useEffect(() => {
/* convert your HTML string into DocumentFragment*/
const node = document.createRange().createContextualFragment(HTML);
ref.current.appendChild(node);
}, []);
return (
<div>
<h1>HTML String</h1>
<div>
<div ref={ref}></div>
</div>
</div>
);
}
See how script content is executed on JavaScript console working example
If your are using class component create ref within class constructor, then update node content, I did it in componentDidMount just for testing:
class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
const node = document.createRange().createContextualFragment(HTML);
this.ref.current.appendChild(node);
}
render() {
return (
<div>
<h1>HTML String</h1>
<div>
<div ref={this.ref}></div>
</div>
</div>
);
}
}
see this working example
There are various ways to do this. You may create a function that can be called on to dynamically create and inject the <script> tag into the <body> of the React application.
const addScript = () => {
const script = document.createElement("script");
script.src = '<url-of-the-script>';
script.async = true;
script.onload = function() {
// Do something
};
document.head.appendChild(script);
}
You may call this addScript function when the required component loads using the useEffect hook.
useEffect(() => {
addScript();
return () => {
// remove the script on component unmount
};
}, []);
Rendering raw HTML without React recommended method is not a good practice. React recommends method dangerouslySetInnerHTML to render raw HTML.
You would need to first fetch the dynamic data from the db using a fetch call and make use of useEffect, Inside which after the data is fetched you set it to a useState hook variable which will hold the data for this.
const [dbData,setDbData] = useState([]);
useEffect(()=>{
const dbReqFunc = async () => {
// req your dynamic data here and set the data fetched to dbData
// using setDbData(...)
};
dbReqFunc();
},[]); // Making the call on component loading (Modify accordingly based on
// needs)
After this once the data has been set you can make use of another useEffect hook which should be below the previous useEffect hook. Then you can call your function for the entire dynamic URL fetched and append it to the HTML document ( I have attached all the scripts to a div for easy cleanup).
useEffect(()=>{
if(dbData.length>0){
const elem = document.createElement('div');
dbData.map(data=> {
// based on data returned modify accordingly
const script = document.createElement("script");
script.src = '...script-url...';
script.async = true;
script.onload = function() {
// Do something
};
//...other script fields...
elem.appendChild(script);
});
// attach elem to body
document.body.appendChild(elem);
return ()=>{
// clean-up function
document.body.removeChild(elem);
};
}
},[dbData]);
This should load the script data and should load the scripts.
NOTE: Make sure you are putting the the dynamic db call for fetching the data before the 2nd useEffect. So, that it runs in order.
I would like to be able to embed the content that gets created via this snippet anywhere I like, using one line of code - same way that you get any kind of snippet somewhere, so that it renders an iframe or similar in return. I am not sure where to start with that, and if this what I already have is usable/ready to "convert" to an embeddable snippet. I went through a few tutorials but it was quite confusing since it was using some backend stuff I don't really understand...
Obviously, it should be hosted somewhere, but the part where I need to "call" the snippet is not really clear to me. Now, it just appears everywhere on my website since it's just a normal JS file which gets included, same as all other files.
It would be great if I could just pack it somehow and call it like this let's say:
<script src="link-to-my-snippet.js"></script>
If someone could direct me a bit that would be great.
const data = [
{
name: "John Doe",
age: 40
},
{
name: "Jane Doe",
age: 50
}
];
window.addEventListener("DOMContentLoaded", function() {
function Item(configurationObject) {
apiCall(data);
}
function apiCall(data) {
// Do some API call and get back the data
// With the Unique ID that wass passed in via the
// configuration Object
// "data" is faked just for the demonstration
createHTML(data);
}
function createHTML(data) {
const mainDiv = document.createElement("div");
document.body.appendChild(mainDiv);
let html = '';
data.forEach((user) => {
html += `
<div class="test">
<p>${user.name}</p>
<p>${user.age}</p>
</div>
`;
});
mainDiv.innerHTML = html;
createStylesheet();
}
function createStylesheet() {
const style = document.createElement("style");
style.innerHTML = `
.test {
background-color: lightgreen;
color: white;
}
`;
document.head.appendChild(style);
}
let configurationObject = {
uniqueID: 1234
}
let initialize = new Item(configurationObject);
});
There are two ways:
Using modern javascript - ES6, Webpack, SCSS, and then bundle all in a single file using NPM
Follow: https://blog.jenyay.com/building-javascript-widget/
Pure JavaScript - Custom Created.
You can create a self-executable anonymous function like this and write your complete widget code - including your HTML, CSS, etc inside this. It will be executed once your page is loaded with this script.
(function() {
// The following code will be enclosed within an anonymous function
var foo = "Hello World!";
document.write("<p>Inside our anonymous function foo means '" + foo + '".</p>');
})(); // We call our anonymous function immediately
For the second type solution you can also follow following article:
https://gomakethings.com/the-anatomy-of-a-vanilla-javascript-plugin/
I'm assuming that you have some local static HTML/CSS page.
First off, you don't need to render the generated HTML by the javascript in an iframe, almost any element will do. The purpose of JS is to create, manipulate and read DOM-elements, so don't feel limited.
Secondly, some of that code is useless for your purpose, unless you plan on doing stuff with an API (which I assume not), and have an actual need for a unique ID. In that code, that unique Id isn't unique anyway and isn't used for anything.
There is so many ways to implement this script on any page of your choice, but here's one:
You have a HTML-file, in that one, put:
<div id="users-list"></div>
wherever you want the list to appear.
Create a file called whatever you want, but for example users-list.js. Check the demo in my answer for the JS code to put in that file.
In any HTML file where you have added an element with the ID of 'users-list', simply also add the script in that same HTML file. Preferably before the ending tag.
<script src="/path/to/users-list.js"></script>
Of course, you make this in so many ways, and expand on it infinitely. For example, something like this could be cool to have:
<div id="some-div-id"></div>
...
<script src="/path/users-list.js">
getUsers({
element: 'some-div-id'
theme: 'dark',
layout: 'boxes' // or 'rows'
});
</script>
Then you could invoke the script on different pages with different generated HTML, but of course, that would require some modification of your JS code, to actually print out different css content, and getting the right element to place the data in.
In any case, it wouldn't be hard to do.
But back on topic, working demo:
const users = [
{
name: "John Doe",
age: 40
},
{
name: "Jane Doe",
age: 50
}
];
const styles = `
.user {
background-color: lightgreen;
color: white;
}
.age { font-weight: bold; }
`;
function setStyles() {
const styleElement = document.createElement('style');
styleElement.innerHTML = styles;
document.head.appendChild(styleElement);
}
function setUsers(users) {
let element = document.getElementById('users-list')
let usersHtml = '';
users.forEach(user => {
usersHtml += `
<div class="user">
<p class="name">${user.name}</p>
<p class="age">${user.age}</p>
</div>
`;
})
if (element) element.innerHTML = usersHtml;
}
window.addEventListener("DOMContentLoaded", function () {
setUsers(users);
setStyles(styles);
});
<div id="users-list"></div>
Here is an example of a self invoking recursive IIFE checking for the document readyState, better than the accepted answers solution
const myPlugin = () => {
// stuff
}
/**
* Checks the document readyState until it's ready
*/
(ready = (delay) => {
// this is always 'complete' if everything on the page is loaded,
// if you want to check for a state when all html/js is loaded but not all assets, check for 'interactive'
if (document.readyState == 'complete') {
myPlugin() // your stuff being invoked when doc is ready
} else {
console.log('Retrying!')
setTimeout(() => { ready(delay) }, delay)
}
})(50)
I'm trying to create a React Portal that when mounted, requires running a specific line to load an ActiveReports Designer component.
Here's is my portal code:
constructor(props: IWindowPortalProps) {
super(props);
this.containerEl = document.createElement('div'); // STEP 1: create an empty div
this.containerEl.id = 'designer-host';
this.containerEl.className = styles.designerHost;
this.externalWindow = null;
}
private copyStyles = (sourceDoc: Document, targetDoc: Document) => {
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
if (styleSheet.cssRules) { // true for inline styles
const newStyleEl = sourceDoc.createElement('style');
Array.from(styleSheet.cssRules).forEach(cssRule => {
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
});
targetDoc.head.appendChild(newStyleEl);
} else if (styleSheet.href) { // true for stylesheets loaded from a URL
const newLinkEl = sourceDoc.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
targetDoc.head.appendChild(newLinkEl);
}
});
}
componentDidMount() {
this.externalWindow = window.open('', '', `height=${window.screen.height},width=${window.screen.width}`);
this.externalWindow.document.body.appendChild(this.containerEl);
this.externalWindow.document.title = 'A React portal window';
this.externalWindow.addEventListener('load', () => {
new Designer('#designer-host');
});
}
render() {
return ReactDOM.createPortal(null, this.containerEl);
}
However, when the new window loads, I get the error
Error: Cannot find the host element. at Function.<anonymous>
which indicates that the designer-host div is not there. I think the load function points to the main DOM and not the new window's one.
Alternatively, I tried appending the ActiveReports .js file by doing in my componentDidMount()
s.type = "text/javascript";
s.src = "../node_modules/#grapecity/activereports/lib/node_modules/#grapecity/ar-js-designer/index.js";
this.externalWindow.document.head.append(s);
and then assigning the Designer instantiation on the onLoad property of the element. Again with no luck.
Is there maybe a way I could run JavaScript a code after the portal has been loaded and point to that DOM?
Thank you
I work for GrapeCity. Could you please go to our support portal and submit a ticket. We will need a full code sample for us to be able to answer this question. Please give us a download link to the sample within the ticket.
Thank you
I'm creating a website with vue.Js (vue.cli) to show some of my work.
Actually all the interactions and experiments are made in a iframe, because I don't understand how to load and use external javascript
For example in one of my components I would like to use gio.js.
To use it you need to call 4 libraries:
jquery -> https://code.jquery.com/jquery-3.3.1.slim.min.js
three.js -> https://threejs.org/build/three.min.js
gio.js -> https://raw.githack.com/syt123450/giojs/master/build/gio.min.js
data -> https://raw.githack.com/syt123450/giojs/master/assets/data/sampleData.js
So actually I write that in my component, but nothing works:
So, I would like to proper load external js files, and write regular js in my components or in an externals files.
<template>
<div class="planet">
//globalArea is for render the planisphere
<div id="globalArea"></div>
</div>
</template>
<script>
export default {
mounted() {
let jqueryScript = document.createElement('script')
jqueryScript.setAttribute('src', 'https://code.jquery.com/jquery-3.3.1.slim.min.js')
document.head.appendChild(jqueryScript);
let threeScript = document.createElement('script')
threeScript.setAttribute('src', 'https://threejs.org/build/three.min.js')
document.head.appendChild(threeScript);
let gioScript = document.createElement('script')
gioScript.setAttribute('src', 'https://raw.githack.com/syt123450/giojs/master/build/gio.min.js')
document.head.appendChild(gioScript);
let dataScript = document.createElement('script')
dataScript.setAttribute('src', 'https://raw.githack.com/syt123450/giojs/master/assets/data/sampleData.js"')
document.head.appendChild(dataScript);
}
methods: {
// Get the container to hold the IO globe
var container = document.getElementById( "globalArea" );
// Create Gio.controller
var controller = new GIO.Controller( container );
// Add data
controller.addData( data );
// Initialize and render the globe
controller.init();
}
}
</script>
You have a common error, the methods properties is an object that carries all the methods you want to define for example:
export default {
methods: {
myFirstMethod () {
//code goes here
},
mySecondMethod () {
//code goes here
},
}
}
If you want to initialize your data when the component is shown you can load the scripts on the beforeCreate hook and paste your code on the mounted hook. Here is how it should look:
var app = new Vue({
el: '#app',
beforeCreate() {
let jqueryScript = document.createElement('script')
jqueryScript.setAttribute('src', '//code.jquery.com/jquery-3.3.1.slim.min.js')
document.head.appendChild(jqueryScript);
let threeScript = document.createElement('script')
threeScript.setAttribute('src', '//threejs.org/build/three.min.js')
document.head.appendChild(threeScript);
let gioScript = document.createElement('script')
gioScript.setAttribute('src', '//raw.githack.com/syt123450/giojs/master/build/gio.min.js')
document.head.appendChild(gioScript);
let dataScript = document.createElement('script')
dataScript.setAttribute('src', '//raw.githack.com/syt123450/giojs/master/assets/data/sampleData.js')
document.head.appendChild(dataScript);
},
mounted() {
setTimeout(() => {
// Get the container to hold the IO globe
var container = document.getElementById( "globalArea" );
// Create Gio.controller
var controller = new GIO.Controller( container );
// Add data
controller.addData( data );
// Initialize and render the globe
controller.init();
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="planet">
//globalArea is for render the planisphere
<div id="globalArea" style="height: 500px"></div>
</div>
</div>
Also keep in mind that you can only access those scripts when they are fully loaded, that's why I'm using a setTimeout with 2 seconds of delay.