Is it possible to execute a <Script/> every time the props of a react/nextjs component change?
I am converting markdown files to html using marked and, before rendering the html, I would like to have a [copy] button on each <pre> block (those are the code blocks). I have a <script/> that iterates through the <pre> blocks of the DOM document.querySelectorAll("pre") and injects the button needed. If the html changes though at a later stage, then I have found no way to re-run the script to add the copy buttons again.
I have the impression that this is not a very react/nextjs way of doing this, so any hints would be appreciated.
The Script to add the copy buttons. I have added this as the last tag of my <body>:
<Script id="copy-button">
{`
let blocks = document.querySelectorAll("pre");
blocks.forEach((block) => {
if (navigator.clipboard) {
let button = document.createElement("img");
button.src = "/images/ic_copy.svg"
button.title = "Copy"
button.id = "copy"
button.addEventListener("click", copyCode);
block.appendChild(button);
}
});
async function copyCode(event) {
const button = event.srcElement;
const pre = button.parentElement;
let code = pre.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
button.src = "/images/ic_done.svg"
setTimeout(()=> {
button.src = "/images/ic_copy.svg"
},1000)
}
`}
</Script>
the React component. Not much to say here. The content is coming from the backend. Not sure what would be the 'React' way to do this without the script.
export default function Contents({ content }) {
return (
<div className='pl-2 pr-2 m-auto w-full lg:w-2/3 mb-40 overflow-auto break-words'>
<div className="contents" dangerouslySetInnerHTML={{ __html: content }} />
</div>
)
}
You should absolutely not do this and instead incorporate this logic into your react app, but if you must you can leverage custom window events to make logic from your html script tags happen from react.
Here is an example script:
<script>
function addEvent() {
function runLogic() {
console.log("Stuff done from react");
}
window.addEventListener("runscript", runLogic);
}
addEvent();
</script>
And calling it form react like this:
export default function App() {
const handleClick = () => {
window.dispatchEvent(new Event("runscript"));
};
return (
<div className="App" onClick={handleClick}>
<h1>Hello</h1>
</div>
);
}
Related
hope you having great days,
i'm trying to get div for change its attribute value
but document.getElementById() is not working
i put the statement for after the div is completly load,
and even tried windows.load but nothing workout,
any suggestions?
var size_list = [[76.01, 77.81,23.99,11.09,0,11.09]
,[69.9, 71.56,20.51,14.22,9.59,14.22]
,[64.1,65.63,17.56,17.19,18.34,17.19]
,[59.22,60.63,15.15,19.69,25.64,19.69]
,[54.79,56.09,12.87,21.95,32.34,21.95]]
function size_update(src,index) {
console.log("i'm working");
let element = window.getComputedStyle(document.getElementById("thisid"),null)
element.setProperty('.height',size_list[index][0]+'%');
element.setProperty('width',size_list[index][1]+'%');
element.setProperty('top',size_list[index][2]+'%');
element.setProperty('right',size_list[index][3]+'%');
element.setProperty('bottom',size_list[index][4]+'%');
element.setProperty('left',size_list[index][5]+'%');
}
const Videoframe = ({src,index,title,before_span,span,after_span,subtitle}) =>{
try{
// do something
return(
<div>
<script>
function (){console.log("start!")};
</script>
<div className="videoframe" name = "thisid" id = "thisid" >
<div className="textgroup1">
<div className="title">{title}</div>
<div className="main">
<span>{before_span}</span>
<i className="better">{span}</i>
<span> {after_span}</span>
</div>
<div className="padding" />
<div className="subtitle">
{subtitle}
</div>
</div>
</div>
<script>
function (){console.log("ended!")};
</script>
</div>
);}
finally{
size_update(src,index);
}
}
export default Videoframe;
export { size_list }
tried googling and window.load
window.getComputedStyle() returns the style of the object, but you need the DOM-Element.
To get an element once the document is loaded you need to use the window load event by doing either
window.addEventListener("load", () => {
let element = document.getElementById("thisID");
});
or
window.onload = () => {
let element = document.getElementById("thisID");
};
In both cases, you supply a function to be executed once the document is loaded. Note that the code inside the eventhandler is executed only when the site is loaded, so after the code which is written after this code snippet. If you want to access the element, you'll need to write the logic for the element inside the function.
JavaScript:
const dispatchEvent = createEventDispatcher();
let dialog: Dialog;
HTML
<Dialog bind:this={dialog} on:close={() => {dispatchEvent("deletedPost", postIndex)}} modal={true} open={false}>
... Here I want to insert some HTML to be used in the slot of the "Dialog" component.
</Dialog>
My question is, how do I set the on:close and the inner HTML dynamically via JS?
I have this demand because this Dialog component should be reused for multiple purposes by its parent component. Sometimes it displays info about a post has been created, sometimes deleted, sometimes edited and so on.
And each of the cases requires different content in the component as well as a different handler for the close event emitted by the Dialog.
Should I use something like
dialog.$on = handlerFunction;
// I didn't find a possible property for setting HTML though
Thanks!
There currently is no way to programmatically interact with the slots (unless you want to investigate the Svelte internals and rely on implementation details which may change).
Either pass in a full component via the props (which can be rendered using <svelte:component>), insert only text or use {#html} to render HTML content (which is not recommended for security reasons).
(The syntax for the events is dialog.$on('close', event => ...) as detailed in the client-side API section.)
Example for all mentioned property types:
<!-- Dialog.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let open = true;
export let component = null;
export let text = null;
export let html = null;
const dispatch = createEventDispatcher();
$: dispatch(open ? 'open' : 'close');
</script>
<div>
<h2>Dialog</h2>
{#if component}
<svelte:component this={component} />
{/if}
{#if text}
{text}
{/if}
{#if html}
{#html html}
{/if}
<div><button on:click={() => open = false}>Close</button></div>
</div>
Usage:
// ...
import Content from './Content.svelte';
const d = new Dialog({
// ...
props: { text: 'Hello world' },
});
// or
const d = new Dialog({
// ...
props: { html: '<input placeholder="Text here"/>' },
});
// or
const d = new Dialog({
// ...
props: { component: Content },
});
d.$on('close', () => d.$destroy());
REPL
As noted, #html is dangerous and ideally should only be used with XSS sanitization in place.
I am working on the CMS based vue page. In the page I have one root container inside that I have two child container as looks below
<div id="app">
<div class="above-the-fold">...</div>
<div class="below-the-fold noscript-container">
<noscript>
<div v-if="true">{{someDynamicBinding}}</div>
<div v-if="false">{{someDynamicBinding}}</div>
</noscript>
</div>
</div>
If javascript is enabled I am removing noscript tag and appending inside contents to the parent element using a below script.
document.addEventListener("DOMContentLoaded", function () {
const appendContents = function () {
const noScript = document.querySelector(".noscriptcontainer noscript");
const belowContent = noScript.innerHTML;
noScript.parentElement.innerHTML += belowContent;
console.log("elm appended");
window.removeEventListener("scroll", appendContents);
noScript.remove();
console.log("eve removed");
};
window.addEventListener("scroll", appendContents);
});
Now the problem is vue doesn't evaluating v-if or {{dynamicBinding}}
Jsfiddle Link https://jsfiddle.net/69yr3mp5/2/
Now what is the best way to make this work?
Try to replace the second if with v-else-if
<div v-if="true"><h1>
{{someDynamicBinding}}
</h1></div>
<div v-else-if="false"><h1>
{{someDynamicBinding}}
</h1></div>
</noscript>
TL;DR;
You can update the template run-time, but you can't update the template after component is mounted.
Run-time, in the context of these frameworks, means that it is not pre-compiled, which is something that Vue supports. However, the problem here (one of them anyway) is that you are modifying the template after the application is mounted. The Vue app will not continue to check the template after the initial read. While I'm not sure what you're trying to accomplish here, It seems that the approach is wrong.
To illustrate, if you use this code, it will compile the updated code, but not until you scroll.
let mounted = false
document.addEventListener("DOMContentLoaded", function() {
const appendContents = function() {
const noScript = document.querySelector(".noscript-container noscript");
const belowContent = noScript.innerHTML;
noScript.parentElement.innerHTML += belowContent;
console.log("elm appended");
window.removeEventListener("scroll", appendContents);
noScript.remove();
console.log("eve removed");
if (!mounted) {
Vue.createApp({
data() {
return {
someDynamicBinding: 'Some Content'
};
},
mounted() {
console.log('mounted')
},
}).mount('#app');
};
mounted = true
}
window.addEventListener("scroll", appendContents);
});
In React JSX I want to convert a part of the text into an anchor tag dynamically. Also on click of the anchor tag, I want to do some API call before redirecting it to the requested page. But I am failing to achieve this. Can anyone have a look and let me know where am I going wrong?
I have recreated the issue on code sandbox: here is the URL: Code Sandbox
Relevant code from sandbox:
import React from "react";
import "./styles.css";
export default function App() {
let bodyTextProp =
"This text will have a LINK which will be clicked and it will perform API call before redirect";
let start = 22;
let end = 26;
let anchorText = bodyTextProp.substring(start, end);
let anchor = `<a
href="www.test.com"
onClick={${(e) => handleClick(e)}}
>
${anchorText}
</a>`;
bodyTextProp = bodyTextProp.replace(anchorText, anchor);
const handleClick = (e) => {
e.preventDefault();
console.log("The link was clicked.");
};
const handleClick2 = (e) => {
e.preventDefault();
console.log("The link was clicked.");
};
return (
<div className="App">
<h3 dangerouslySetInnerHTML={{ __html: bodyTextProp }} />
<a href="www.google.com" onClick={(e) => handleClick2(e)}>
Test Link
</a>
</div>
);
}
The problem is variable scope. While it is entirely possible to use the dangerouslySetInnerHTML as you are doing, the onClick event isn't going to work the same way. It's going to expect handleClick to be a GLOBAL function, not a function scoped to the React component. That's because React doesn't know anything about the "dangerous" html.
Normally React is using things like document.createElement and addEventListener to construct the DOM and add events. And since it's using addEventListener, it can use the local function. But dangerouslySetInnerHTML bypasses all of that and just gives React a string to insert directly into the DOM. It doesn't know or care if there's an event listener, and doesn't try to parse it out or anything. Not really a good scenario at all.
The best solution would be to refactor your code so you don't need to use dangerouslySetInnerHTML.
*Edit: since you say that you need to do multiple replacements and simply splitting the string won't suffice, I've modified the code to use a split.
When used with a RegExp with a capturing group, you can keep the delimiter in the array, and can then look for those delimiters later in your map statement. If there is a match, you add an a
import React from "react";
import "./styles.css";
export default function App() {
let bodyTextProp =
"This text will have a LINK which will be clicked and it will perform API call before redirect";
let rx = /(\bLINK\b)/;
let array = bodyTextProp.split(rx);
const handleClick = (e) => {
console.log("The link was clicked.");
e.preventDefault();
};
const handleClick2 = (e) => {
e.preventDefault();
console.log("The link was clicked.");
};
return (
<div className="App">
<h3>
{array.map((x) => {
if (rx.test(x))
return (
<a href="www.test.com" onClick={handleClick}>
{x}
</a>
);
else return x;
})}
</h3>
<a href="www.google.com" onClick={(e) => handleClick2(e)}>
Test Link
</a>
</div>
);
}
I'm working on a React app and am trying to use ReactDOM.createPortal() to add html content to a div that is outside the component (called ToDoItem).
{ReactDOM.createPortal(
<Route path={`/profile/project/${this.props.project._id}`} render={() =>
<ProjectView project={this.props.project}/>} />,
document.getElementById('tasks')
)}
None of the HTML in the public folder is predefined - it is all dynamically created.
I think this could be the cause of the error: React tries to add HTML to the div with the id of tasks which, but the div is not loaded into the DOM before this happens?
If this method is incorrect, is there any other method I can use append html content to another div outside the component?
Some other info: the component from which I tried to run this method is a stateless component, not a class component.
This is the error:
You can wait until the DOM is ready using React.useEffect, and then you call ReactDOM.createPortal:
function Component() {
const [domReady, setDomReady] = React.useState(false)
React.useEffect(() => {
setDomReady(true)
})
return domReady
? ReactDOM.createPortal(<div>Your Component</div>, document.getElementById('container-id'))
: null
}
The problem is that you can't createProtal to react component.
the second parameter have to be dom elemenet, not react created element
I had this issue because I forgot to add <div id="some-id"></div> to my index.html file :-D
So just as a reminder for anyone who has a similar problem or doesn't know how to use React portals ( to create a modal for example):
in modal.jsx:
const modalRoot = document.getElementById('modal');
const Modal = () => {
const modalElement = document.createElement('div');
// appends the modal to portal once modal's children are mounted and
// removes it once we don't need it in the DOM anymore:
useEffect(() => {
modalRoot.appendChild(modalElement);
return () => {
modalRoot.removeChild(modalElement);
};
}, [modalElement]);
return createPortal(<div>modal content</div>, modalRoot);
};
in index.html:
<!DOCTYPE html>
<html lang="en">
<head>
// head content
</head>
<body>
<div id="root"></div>
// dont't forget about this:
<div id="modal"></div>
</body>
</html>
Before first render of components, there is no element in the DOM and document.getElementById cannot reach to any element. elements added to DOM after first render.
Use useRef in parent component and send it to Portal Component.
In parent component:
const tasksRef = React.useRef(null);
const [tasksSt, setTasksSt]= React.useState();
....
<div ref={
(current) => {tasksRef.current = current;
setTasksSt(tasksRef.current);
}
}/>
<YourPortalComp tasksRef={tasksRef} />
In Portal Component
{this.props.tasksRef.current?(ReactDom.createPortal(<...>, this.props.tasksRef.current):null}
If the target element (In your case element with id tasks) is loaded but still you are getting the Target container is not a DOM element Error error, you can try the below solution.
const MyElement = () => {
const [containerToLoad, setContainerToLoad] = useState(null);
// If the target element is ready and loaded
// set the target element
useEffect(() => {
setContainerToLoad(document.getElementById('container'));
}, []);
return containerToLoad && createPortal(<div>modal content</div>, containerToLoad);
};
check your id in index.html file,
So lets say, your index.html file has:
<div id="overlays"></div>
Your Modal should point to same div, ie
const portalElement = document.getElementById('overlays');
const Modal = (props) => {
return (
<Fragment>
{ReactDOM.createPortal(<Backdrop onClose={props.onClose} />, portalElement)}
</Fragment>
);
};
For those that might be using Next.js, an equivalent to AlexxBoro's answer for next js can be found here. https://www.learnbestcoding.com/post/101/how-to-create-a-portal-in-next-js