Access React DOM Element from external script - javascript

I am trying to get access to a React DOM element 'id' from an external script file. I believe my script is being imported correctly as console.log('test') from the file is working, though console.log(myDiv) returns null.
How can I achieve this in React?
// COMPONENT
import './../data/script.ts';
render() {
return (
<div id='targetDiv'>
<p>{This will be populated from my external script file...}</p>
</div>
);
}
// SCRIPT
var myDiv = document.getElementById('targetDiv');
console.log(myDiv);

To fix the issue I needed to import my external script as a function, and then call the function after the component mounted:
// COMPONENT
import { myScript } from './../data/script';
componentDidMount() {
{
myScript();
}
}
render() {
return (
<div id='targetDiv'>
{My script now renders correctly inside the div}
</div>
);
}
// SCRIPT
var myDiv = document.getElementById('targetDiv');
const d = document.createElement('p');
myDiv.appendChild(d);

Related

Running js script from public folder with <script> tag works, but not importing from local file

When I place js script into "public" folder then import this way into script tag script.src = "/scoper.js";, js script runs fine. But when I move that js script into "src" folder then directly import it, script runs but not properly. So what is the difference when running js script from <script src="..."> and running from import { scoper } from "./scoper.js";?https://codesandbox.io/s/stylesheet-scoping-b0k9pp?file=/src/App.js:65-102
When script runs successfully, "Hello World" text should be red color.
App.js
import React, { useEffect } from "react";
import "./styles.css";
import { scoper } from "./scoper.js"; // this doesn't work
export default function App() {
useEffect(() => {
const script = document.createElement("script");
// script.src = "/scoper.js"; //this works
script.async = true;
document.body.appendChild(script);
let style = document.createElement("style");
style.setAttribute("scoped", "");
style.innerHTML =
".Puma {" +
"color: purple;" +
"font-size: 50px;" +
"text-align: left;" +
"}";
let main = document.getElementById("raptors");
main.appendChild(style);
return () => {
document.body.removeChild(script);
};
}, []);
return (
<>
<div id="lakers" className="Puma">
<div>Hello World!</div>
</div>
<div id="raptors" className="Puma">
<h1>Raptors win!</h1>
</div>
</>
);
}
With import { scoper } from "./scoper.js"; the script is imported and executed when the file App.js is executed the first time, that is the time when e.g. function App() { ... is defined, but before App() gets executed, and before any components are rendered.
useEffect(() => { ... is executed after the component is rendered. So at the time the imported script is executed, the DOM elements rendered by React are available.
Generally, you should not execute the script inside a module immediately when it is imported, because that will always be confusing (there are exceptions, of course). Instead you should export an object or function, so that the importing file can choose when to call it. In case the importing file wants to execute the imported function immediately, it still can do that.

Cannot appendChild to a custom web component

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.

Javascript dynamically inserted later on: how to make it run?

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.

React add script JS to a component

I try to add a JS script to an React component, nevertheless nothing append.
The message Hello is displayed but not the script.
The swcript is to the same repository that the component Canvas.
I try to add it like this:
class Accueil extends Component {
render() {
return (
<div>
<Canvas/>
</div>
);
}
}
class Canvas extends Component {
componentDidMount() {
const script = document.createElement("script");
script.async = true;
script.src = "./Sketch.js";
this.div.appendChild(script);
}
render() {
return (
<div className="App" ref={el => (this.div = el)}>
<h1>Hello</h1>
{/* Script is inserted here */}
</div>
);
}
}
But when I insert directly the script Sketch in top of the page
import {Sketch} from './Sketch.js'
The script is well add but it is add also for other components, so I think it is not the well way to do it.
Thanks in advance :)
If you dynamically append a script to body or anything inside body, it will not get executed (because everything there executes only once during page load. You have to append it to head.
You can use global variables help it 'find its component' or vice versa...

Can't use require() method that receives a prop in React

As part of a personal project, I have to create an image gallery. For every image (miniature) that is clicked in the gallery, I want the chosen image to appear in a larger size on the page. I have been trying to do this by passing the desired image's address as a prop to my ImageOnView component, like so:
<ImageOnView imgSrc = {this.state.imageLarge}/>
Inside my ImageOnView component, however, I'm having trouble displaying the image using the require() method. Right now it looks like this:
class ImageOnView extends Component {
render() {
//This alert displays the image source as expected
alert(this.props.imgSrc);
return (
<img id = "image-large" src = {require(this.props.imgSrc)}/>
);
}
}
I get the following error on my webpage: "Error: Cannot find module ".""
How do I go about resolving this issue? Thanks in advance.
Just try
src={require('' + this.props.imgSrc)}
Since you are using Webpack as a bundler in runtime, require can't use a variable name like this. It should accept a resolvable path, ie a string here. So, you can require the image in the parent instead of the child and use the resolved path:
class App extends Component {
state = {
imageLarge: require( "./somedir/some.jpg" ),
};
render() {
return (
<div>
<ImageOnView imgSrc={this.state.imageLarge} />
</div>
);
}
}
class ImageOnView extends React.Component {
render() {
// This alert displays the image source as expected
alert( this.props.imgSrc );
return (
<img id="image-large" src={this.props.imgSrc} />
);
}
}
Can also do like this:-
import React from 'react';
function index({title, image, rating}) {
return <div>
<img alt="img" src={require("" + image)}></img>
</div>;
}
export default index;
require() is used to import a module or file. So to get an imageLarge from another file you might want to
const imageLarge = require(path)
or es6 version:
import imageLarge from 'path'
in the img tag you just need to provide a src attribute with imageLarge in component it would look something like:
const imageLarge = require(path)
<ImageOnView imgSrc = {imageLarge}/>
class ImageOnView extends Component {
render() {
return (
<img id = "image-large" src = {this.props.imgSrc}/>
);
}
}

Categories

Resources