React add script JS to a component - javascript

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...

Related

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.

Access React DOM Element from external script

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);

react-dnd DragSource that has child DragSource

I have a tree structure with a root "Tree" component that has a list of root "TreeNodes", then TreeNodes can have an arbitrary number of children.
So inside of the TreeNode render method I have
childrenHTML = this.state.children.map((child) => {
return (<TreeNode nodeClick ={this.props.nodeClick} parentNode={this}
key={child.childId} node={child} level={this.state.level+1} />);
});
and
const { isDragging, connectDragSource, connectDragPreview} = this.props;
Then the final return for the render method looks like
return connectDragSource(
<div>
<div style={nodeStyle}>
{connectDragPreview(
<div className = {"nodeContainer" + ' ' + this.state.nodeHover} onMouseLeave={this.nodeUnHover} onMouseOver={this.nodeHover} onClick={()=>this.props.nodeClick(this)}>
<img alt = {this.state.titleIcon} className = "titleIcon" src = {Connections.getImageURLByName(this.state.titleIcon)} />
<p className="nodeLabel"> {this.state.nodeName}</p>
{nodeLabelsHTML}
<DescriptiveIcons descriptiveIcons={this.state.icons} />
</div>
)}
</div>
{childrenHTML}
</div>
);
I am exporting:
export default DragSource(DragTypes.STRUCTURE, treeNodeSource, collect)(TreeNode);
Then in the parent Tree file I am exporting
export default DragDropContext(HTML5Backend)(Tree)
and rendering the rootnodes like
rootNodesHTML = rootNodes.map((node) => {
return <TreeNode nodeClick={this.props.nodeClick} key={node.childId} node={node} level={0}/>
});
...
return (
<div className="treeContainer">
<div className="wrapContainer">
{rootNodesHTML}
</div>
</div>
);
This works great but only for the rootnodes, when I try to render the children (the childrenHTML variable is only populated after the parent is clicked on) I get the following error:TypeError: connectDragPreview is not a function
Leading me to believe that those react-dnd props that come from the "collect" function is not being passed to the rootnodes but not the children. It seems like it should to me because the same code should be executed for the parents as for the children as its the same class... really stuck here.
I am relatively new to react, and new to ideas like HOCs so all tips or suggestions are appreciated. Thank you!
I was able to get this working. Check out the example posted at the end of the thread in
https://github.com/react-dnd/react-dnd/issues/332.
Ultimately the solution was to wrap the TreeNode in a "DragContainer" with a very simple render method
render(){
const {...props} = this.props;
return <TreeNode {...props}/>
}
Then in the TreeNode render method, when rendering the child nodes render a DragContainer instead, passing in all the usual props.
childrenHTML = this.state.children.map((child) => {
return <DragNodeContainer modalFunctions = {this.props.modalFunctions} nodeClick ={this.props.nodeClick} parentNode={this} key={child.childId} node={child} level={this.state.level+1} />;
});
I am still unsure as to the technical reason for this, however, the fix seems to work for other people and it works for me!

How do I get the dom node of a child element in a component without adding logic to its parent/children?

In the following example WrapperComp needs to get access to the dom node of the divs in line 5 and line 8, without adding logic to PageComp or ItemComp. The only things I could change in PageComp are the div tags. E.g. I could add a ref, prop, data-attribute, etc to them.
The divs don't have to be created inside PageComp. WrapperComp would be allowed to create them too, but they must wrap each of its children (In this case each ItemComp).
Example
class PageComp extends React.Component {
render() {
return (
<WrapperComp>
<div>
<ItemComp/>
</div>
<div>
<ItemComp/>
</div>
</WrapperComp>
);
}
}
class WrapperComp extends React.Component {
render() {
return (
<div>
<h1>A wrapper</h1>
{this.props.children}
</div>
);
}
}
class ItemComp extends React.Component {
render() {
return (
<div>
<h1>An item</h1>
</div>
);
}
}
ReactDOM.render(
<PageComp/>,
document.getElementById('app')
);
JSBIN
What I tried so far:
I already tried to put a ref= on the divs, but that ref would only be available in PageComp not in WrapperComp.
I also tried to create the divs inside WrapperComp and put a ref= on them from there, but that would result in a Refs Must Have Owner Warning
Now I wonder.. what would be an appropriate way in react to solve that problem?
Till now the only solution that came to my mind was to put a data-attribute on each div and search the dom for them after componentDidMount like that: document.querySelectorAll('[data-xxx]'). Perhaps I'm not sure if this is how you do it in react..
Why do I want to get the node inside WrapperComp?
I want to create a component that adjusts the dimensions of its children. In the example that component would be WrapperComp. The adjustments can only be done after the children rendered to the dom, e.g. to get clientHeight.
If you don't restrict that this needs to be solved by how one should get the DOM, pass them down, etc, I would get rid of the puzzle and approach it in a different direction.
Since you are not given much control to <PageComp> whereas <WrapperComp> seems flexible, I would do the wrapping in the later by transforming the passed children to what you need them to be.
class PageComp extends React.Component {
render() {
return (
<WrapperComp>
<ItemComp/>
<ItemComp/>
</WrapperComp>
);
}
}
class WrapperComp extends React.Component {
render() {
const wrappedChldren = React.Children.map(this.props.children, function(child) {
return (
<div ref={function(div) {
this.setState{clientHeight: div.clientHeight}
}}>
<h1>A wrapper</h1>
{ child }
</div>
);
});
return <div>{ wrappedChildren }</div>;
}
}
With this concentrate can be put on the transformation in the <WrapperComp>, which is pretty intuitive as its name suggests.

Categories

Resources