Add events to svg string in component and render it - javascript

Basically what I need is to add event listeners to particular elements in my svg, that I receive as in param in my component
export default function RoomPlan({ svg, startDate, endDate}) {
const [selectedDesk, setSelectedDesk] = useState(null);
const [previouslySelectedDesk, setPreviouslySelectedDesk] = useState(null);
var doc = new DOMParser().parseFromString(svg, "text/xml");
var elements = Array.from(doc.querySelectorAll('#desk rect'));
if (elements) {
elements.forEach(function (el) {
el.addEventListener("click", function(){alert('hi')});
})
}
return (
<div>
<h2 className={css.labels}>Select desk</h2>
<div id={css.roomPlan} dangerouslySetInnerHTML={{ __html: new XMLSerializer().serializeToString(doc)}}></div>
<div className={css.bookingButtons}>
<Button id={css.cancelButton}>CANCEL</Button>
<Button onclick={bookDesk(selectedDesk)}>BOOK DESK</Button>
</div>
</div>
);
I get it as a plain string then parse it to DOM then add my eventListeners and serialize it back. But with this approach the ivents are not presented and do not trigger. So question is: how I can make it work as intendet with string -> DOM -> addedEvents -> render the svg

You can only attach event handlers after react rendered the markup into the DOM. For this to work you might look up react ref with callback.
const ref = useCallback(node => {
if (node !== null) {
/* do your magic here: node.querySelectorAll('#desk rect') ... */
}
}, []);
<div id={css.roomPlan} dangerouslySetInnerHTML={{ __html: svg}} ref={ref}></div>

Related

How to run <script> when props of a component change?

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

How to wrap text typed with 'typed.js' ReactJs?

I'm building a web app with React that generates random movie quotes...
The problem arises when the quote is too long and it overflows outside the parent div...
I've tried altering the css with display flex and flex-wrap set to wrap. It does't work.
Here is my code.
import React from 'react';
import Typed from 'typed.js';
import './App.css';
import quotes from './quotes.json';
const random_quote = () => {
const rand = Math.floor(Math.random() * quotes.length);
let selected_quote = quotes[rand].quote + ' - ' + quotes[rand].movie;
return selected_quote;
}
const TypedQuote = () => {
// Create reference to store the DOM element containing the animation
const el = React.useRef(null);
// Create reference to store the Typed instance itself
const typed = React.useRef(null);
React.useEffect(() => {
const options = {
strings: [
random_quote(),
],
typeSpeed: 30,
backSpeed: 50,
};
// elRef refers to the <span> rendered below
typed.current = new Typed(el.current, options);
return () => {
// Make sure to destroy Typed instance during cleanup
// to prevent memory leaks
typed.current.destroy();
}
}, [])
return (
<div className="type-wrap">
<span style={{ whiteSpace: 'pre' }} ref={el} />
</div>
);
}
const App = () => {
return (
<>
<div className='background' id='background'>
<div className='quote-box'>
<TypedQuote />
</div>
<button onClick={random_quote}>New Quote</button>
</div>
</>
);
}
export default App;
I have this idea where I could implement a function that adds '\n' after like 10 words or like maybe after a '.' or ',' (I could implement some logic here). But this seems like a longshot. Is there a fancier way to do this?? Any help would be appreciated.
Try the property below on the parent container.
word-wrap: break-word;
or the below if you want to break words as well
word-break: break-all;

REACT JS: Dynamically convert part of paragraph into an anchor tag and on click of that anchor tag do an API call before doing a redirect

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

React and using ReactDOM.createPortal() - Target container is not a DOM element Error

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

JavaScript recursive array to select html elements

I'm currently trying to make a recursive function that takes html elements as an array so I can take html elements like the querySelector function
The reason i'm doing this is because I can't use getElementsByTagName() or querySelector()
Here is my code:
function flatten(items)
{
const flat = [];
items.forEach(item => {
if (Array.isArray(item)) {
flat.push(...flatten(item));
}
else {
flat.push(item);
}
});
return flat;
}
var button = flatten(footer).flatten(div);
count = 0;
button.onclick = function() {
count += 1;
button.innerHTML = count;
};
I get the following error: ReferenceError: footer is not defined
Thanks
Here is my HTML code:
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
Edit:
footer is defined in my HTML, I want to select footer in my function
Also, I can't add class or id to my html, I can't edit it
If, for the sake of practice (or a lost bet), you'd want to write your own querySelectorAll, you could write a recursive function that walks the DOM tree... The only thing you rely on is an entrance to the DOM: window.document.
Note that this will never be able to compete with the performance of your browser's default query implementations. We're just doing it to show we can.
Step 1: recursively walking the document (depth-first)
const walk = (el) => {
console.log(el.nodeName);
Array.from(el.children).forEach(walk);
};
walk(document);
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
As you can see, this function loops over each element in the document and its children.
Step 2: Adding the filter logic
If you want it to actually find and return elements, you'll have to pass some sort of filtering logic. querySelectorAll works with string inputs, which you could try to recreate... Since we're redoing this for fun, our select will work with functions of HTMLElement -> bool.
const selectIn = (pred, el, result = []) => {
if (pred(el)) result.push(el);
Array.from(el.children)
.filter(e => e)
.map(el2 => selectIn(pred, el2, result));
return result;
}
// EXAMPLE APP
// Define some selectors
const withClass = className => el =>
el && el.classList && el.classList.contains(className);
const withTag = tagName => el =>
el && el.nodeName === tagName.toUpperCase();
// Select some elements
const footer = selectIn(withTag("footer"), document)[0];
const container = selectIn(withClass("container"), document)[0];
const divsInFooter = selectIn(withTag("div"), footer);
// Log the results
console.log(`
footer:
${footer.outerHTML}
container:
${container.outerHTML}
divsInFooter:
${divsInFooter.map(d => d.outerHTML)}
`);
<div class="wrapper"><div class="container"><footer><div></div></footer></div></div>

Categories