Run a javascript onclick event from a text content - javascript

I have an onClick event that comes from a String as follows.
const text = `some text and <sup onClick={handleClick}>123</sup>`;
This text is actually coming from an external API response.
And I have a handleClick function as follows:
const handleClick = () => alert('clicked!');
This handleClick function is in my component. I expect the alert to occur when the sup text 123 is clicked.
The text is clickable, but when I click it, I get the error:
Unhandled Runtime Error
ReferenceError: handleClick is not defined
Call Stack
HTMLElement.onclick
But handleClick is there in the component.
I even validated it with a button that doesn't use the text. That works fine.
Why doesn't it work for the text and how could I resolve this?
Component
export default function Home() {
const handleClick = () => alert('clicked!');
const text = `some text and <sup onClick={handleClick}>123</sup>`; // assume this comes from an api.
return (
<div>
<div dangerouslySetInnerHTML={{ __html: text }} /> // this is the line throwing error when I click it.
// this button shows the alert fine when clicked.
<button
onClick={handleClick}
>
Click me now!
</button>
</div>
)
}

Here's an example using altered api txt with onclick being html and not jsx, as well as adding the function to global scope using uesEffect
const {useState, useEffect} = React;
const Home = () => {
const handleClick = () => alert('clicked!');
const text = `some text and <sup onClick="handleClick()">123</sup>`;
useEffect(() => {
window.handleClick = handleClick;
}, []);
return (
<div>
<div dangerouslySetInnerHTML={{ __html: text }} />
<button
onClick={handleClick}
>
Click me now!
</button>
</div>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Home />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Here's an example without useEffect that would listen to the parent for which child was clicked, if you have the ability to shape used data to have a data attribute with which function is meant to be hit.
const {useState, useEffect} = React;
const Home = () => {
const handleClick1 = () => alert('clicked1!');
const handleClick2 = () => alert('clicked2!');
const handleClick3 = () => alert('clicked3!');
const text1 = `1 text and <sup data-func="func1">123</sup>`;
const text2 = `2 text and <sup data-func="func2">123</sup>`;
const text3 = `3 text and <sup data-func="func3">123</sup>`;
const handleAllClicks = e => {
const func = e.target.dataset.func;
switch (func) {
case "func1": return handleClick1();
case "func2": return handleClick2();
case "func3": return handleClick3();
}
};
return (
<div onClick={handleAllClicks}>
<div dangerouslySetInnerHTML={{ __html: text1 }} />
<div dangerouslySetInnerHTML={{ __html: text2 }} />
<div dangerouslySetInnerHTML={{ __html: text3 }} />
</div>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Home />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

When you dangerouslySetInnerHTMl you are setting HTML, not React JSX. Your div will be rendered into the HTML DOM something like this:
<div>
some text and <sup onclick="{handleClick}">123</sup>
</div>
In essence, this is a regular HTML gross onclick property that's executed in the context of the window as pure JS, not React. The curly braces are still valid JS, but there is no defined property handleClick, so you get a reference error.
Try running the following snippet and inspect the HTML and you'll see what I mean:
function Home() {
const handleClick = () => alert('clicked!')
const text = `some text and <sup onClick={handleClick}>123</sup>`
return (
<div>
<div dangerouslySetInnerHTML={{ __html: text }} />
<button
onClick={handleClick}
>
Click me now!
</button>
</div>
)
}
ReactDOM.createRoot(document.getElementById("root")).render(<Home />)
<script src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
The only thing I can think of is using the React create element instead:
function DataRender({ data, onClick }) {
return React.createElement(
"div",
null,
...data.map(seg => {
if(!seg.clickable) return seg.value
return React.createElement(
"sup",
{ onClick },
seg.value
)
})
)
}
function Home() {
const handleClick = () => alert('clicked!')
// change your API to give JSON responses or manually process
const data = [{ value: "some text and " }, { value: "123", clickable: true }]
return (
<div>
<DataRender data={data} onClick={handleClick} />
<button
onClick={handleClick}
>
Click me now!
</button>
</div>
)
}
ReactDOM.createRoot(document.getElementById("root")).render(<Home />)
<script src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>

The problem here is that you are trying to access the handleClick function inside a string, which is not possible as JavaScript evaluates the string as plain text. To resolve this issue, you need to parse the string as HTML and dynamically bind the event handler to the element in the DOM. You can use a library like React-DOM-Parser to parse the string as HTML and bind the event handler to the element.

Related

React callback to parent not changing state

I cannot for the life of me figure out why this isn't working. I have a WorkoutCard component:
WorkoutCard:
const key = require('weak-key');
function WorkoutCard({ workout }) {
const { userID } = useContext(AuthContext);
const [modalOpen, setModalOpen] = useState(false);
const closeModalCallback = () => setModalOpen(false);
return (
<div className="WorkoutCard" onClick={() => setModalOpen(true)}>
<div className="header">
<h2>{workout.name}</h2>
<h3>Days</h3>
{workout.days.map((day) => {
return (
<p key={key({})}>{day}</p>
)
})}
<button className="deleteButton" onClick={handleDelete}>Delete</button>
</div>
<div className="line"></div>
<div className="exercises">
<h3>Exercises</h3>
{workout.exercises.map((exercise) => {
return (
<p key={key({})}>{exercise}</p>
)
})}
</div>
<EditWorkoutModal key={key({})} modalOpen={modalOpen} closeModalCallback={closeModalCallback} />
</div>
);
}
export default WorkoutCard;
And I have the EditWorkoutModal component:
Modal.setAppElement('#root');
function EditWorkoutModal({ modalOpen, workout, closeModalCallback }) {
const { userID } = useContext(AuthContext);
return (
<Modal isOpen={modalOpen} className="editModal">
<div className="rightHalf">
<p className='closeButton' onClick={() => closeModalCallback()}>+</p>
</div>
</Modal>
)
}
export default EditWorkoutModal
The problem here is that closeModalCallback is not changing the state whatsoever. It is called, but modalOpen is still set to true.
And, this is even more confusing, because I have this functionality working in another part of the app. I have a workouts page that has both WorkoutCard components, as well as a Modal, and it works this way. However, the closeModalCallback on the WorkoutCard components' modals will not work.
onClick events bubble up the DOM. For example, see the below snippet (see browser console for output):
const App = () => {
const parent = () => console.log("parent");
const child = () => console.log("child");
return <div onClick={parent}>
<div onClick={child}>Click me</div>
</div>
}
ReactDOM.createRoot(document.body).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
The above logs the following in the console:
> child
> parent
When you click on your child element, the click event bubbles up the DOM, eventually reaching your div with the WorkoutCard class, which fires the onClick that sets your modal-open state to true. You can stop the event from bubbling by calling e.stopPropagation() on your close-modal button:
onClick={(e) => {
e.stopPropagation();
closeModalCallback();
}}
This way the event won't bubble up to your parent div and trigger the onClick which is changing your state.

JavaScript element.contains returns false for conditionally rendered components

I am building a form selector component and I want the dropdown element to close when a user clicks elsewhere on the screen.
The component is designed as follows:
export const FormSelect = ({
defaultText,
disabled,
formError,
formSuccess,
formWarning,
groupTitle,
items,
isOpen,
selected,
selectorId,
selectorWidth,
setSelected,
setIsOpen,
textCaption,
textCaptionError,
textCaptionSuccess,
textLabel,
}) => {
const toggling = () => setIsOpen(!isOpen);
const mainSelector = React.useRef(null);
const closeMenu = () => {
setIsOpen(false);
};
React.useEffect(() => {
var ignoreClickOnMeElement = document.getElementById("option-container");
document.addEventListener("click", function (event) {
var isClickInsideElement = ignoreClickOnMeElement.contains(event.target);
if (!isClickInsideElement) {
closeMenu();
}
});
}, []);
return (
<div id={"option-container"}>
<SelectContainer>
{isOpen ? (
<ArrowUp />
) : (
<ArrowDownward />
)}
</SelectContainer>
<div>
{isOpen ? (
<OptionContainer>
<OptionList>
{groupTitle ? (
{items.map((item, index) => (
<ListItem/>
))}
</OptionList>
</OptionContainer>
) : null}
</div>
</div>)
The useEffect hook contains the logic to close the component if a user clicks outside of the "option-container". However, the form will not open if a user clicks directly on ArrowDown. I am fairly certain this is because the logic checking if the user click is within "option-container" is resulting in false.
if (!isClickInsideElement) {
closeMenu();
}
Why is the conditionally rendered element not considered inside the container around it?
This element:
var ignoreClickOnMeElement = document.getElementById("option-container");
is retrieved once, right after the component mounts. But there's no guarantee that that exact element is the same one that exists in the DOM when the click listener runs. If you try logging ignoreClickOnMeElement.isConnected, it may give you false, indicating that it's not in the DOM anymore.
Retrieve the element only inside the click listener instead - or, even better, use a ref instead of using getElementById.
Live demo:
const FormSelect = () => {
React.useEffect(() => {
document.addEventListener("click", function (event) {
var ignoreClickOnMeElement = document.getElementById("option-container");
var isClickInsideElement = ignoreClickOnMeElement.contains(event.target);
console.log('isClickInsideElement', isClickInsideElement);
});
}, []);
const [open, setOpen] = React.useState();
return (
<div id="option-container">
option container content
<button onClick={() => setOpen(!open)}>toggle</button>
{open ? <div>open</div> : <div>closed</div>}
</div>
);
};
ReactDOM.render(<FormSelect />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div>
other content outside option container
<div class='react'></div>
other content outside option container
</div>

Modal component does not render on a custom button component

I am trying to render a custom and dynamic modal on button clicks. For example, when a "Game" button is clicked, I would like a modal to render with specfics about the game and when a "Bank" button is clicked, I would like the modal to populate with specfics about a bank.
First, when I add an onClick function to a custom button component, the modal does not render. However, when I put the onClick function on a regular button, the modal does render. How can I simply add an onClick function on any component to render a dynamic modal?
Second, I would like to populate each modal with differnet data. For example, a "Game" button would populate the modal with a title of "Game" and so on. I'm using props to do this, but is that the best solution?
Here is the code I have so far, but it is broken when I add the onClick function to components.
// Navbar.js
import { ModalContext } from '../contexts/ModalContext'
function Navbar() {
const [showModal, updateShowModal] = React.useState(false)
const toggleModal = () => updateShowModal((state) => !state)
return(
<ModalContext.Provider value={{ showModal, toggleModal }}>
<Modal
title="Title"
canShow={showModal}
updateModalState={toggleModal}
/>
</ModalContext.Provider>
)
// does not render a modal
<Button
onClick={toggleModal}
type="navItem"
label="Game"
icon="windows"
/>
// render a modal
<button onClick={toggleModal}>Show Modal</button>
)
}
import { ModalContext } from '../contexts/ModalContext'
// Modal.js
const Modal = ({ title }) => {
return (
<ModalContext.Consumer>
{(context) => {
if (context.showModal) {
return (
<div style={modalStyles}>
<h1>{title}</h1>
<button onClick={context.toggleModal}>X</button>
</div>
)
}
return null
}}
</ModalContext.Consumer>
)
}
// modalContext.js
export const ModalContext = React.createContext()
// Button.js
function Button({ label, type = 'default', icon }) {
return (
<ButtonStyle buttonType={type}>
{setIcon(icon)}
{label}
</ButtonStyle>
)
}
First problem:
I think the onClick prop of the <Button> component is not pointing to the onClick of the actual HTML button inside the component.
Could you please check that? And if you think It's been set up in the right way, then can you share the code of the component?
Second Problem
Yes, there's another way to do that. And I think it's React Composition. You can build the modal as the following:
<Modal
showModal={showModal}
updateModalState={toggleModal}
>
<div className="modal__header">{title}</div>
<div className="modal__body">{body}</div>
<div className="modal__footer">{footer}</div>
</Modal>
I think this pattern will give you more control over that component.
Issue
You are not passing the onClick prop through to the styled button component.
Solution
Given style-component button:
const ButtonStyle = styled.button``;
The custom Button component needs to pass all button props on to the ButtonStyle component.
// Button.js
function Button({ label, type='default', icon, onClick }) {
return (
<ButtonStyle buttonType={type} onClick={onClick}>
{setIcon(icon)}
{label}
</ButtonStyle>
)
}
If there are other button props then you can use the Spread syntax to collect them into a single object that can then be spread into the ButtonStyle component.
// Button.js
function Button({ label, type = 'default', icon, ...props }) {
return (
<ButtonStyle buttonType={type} {...props}>
{setIcon(icon)}
{label}
</ButtonStyle>
)
}
Second Question
For the second issue I suggest encapsulating the open/close/title state entirely in the modal context provider, along with the Modal component.
Here's an example implementation:
const ModalContext = React.createContext({
openModal: () => {},
});
const Modal = ({ title, onClose}) => (
<>
<h1>{title}</h1>
<button onClick={onClose}>X</button>
</>
)
const ModalProvider = ({ children }) => {
const [showModal, setShowModal] = React.useState(false);
const [title, setTitle] = React.useState('');
const openModal = (title) => {
setShowModal(true);
setTitle(title);
}
const closeModal = () => setShowModal(false);
return (
<ModalContext.Provider value={{ openModal }}>
{children}
{showModal && <Modal title={title} onClose={closeModal} />}
</ModalContext.Provider>
)
}
Example consumer to set/open a modal:
const OpenModalButton = ({ children }) => {
const { openModal } = useContext(ModalContext);
return <button onClick={() => openModal(children)}>{children}</button>
}
Example usage:
function App() {
return (
<ModalProvider>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<OpenModalButton>Modal A</OpenModalButton>
<OpenModalButton>Modal B</OpenModalButton>
</div>
</ModalProvider>
);
}
Demo

useState passed to useContext not updating state

I am trying to use useContext to create a generic Tooltip component that passes a close() function to the content inside the Tooltip. I have written my Tooltip like this
export function Tooltip(props) {
const [active, setActive] = useState(false);
const close = () => {
setActive(false);
}
return (
<div className="tooltip-wrapper"
onClick={() => setActive(true)}
>
{props.children}
<TooltipContext.Provider value={{close}}>
{active && (
<div className='tooltip-tip bottom' ref={node}>
{props.content}
</div>
)}
</TooltipContext.Provider>
</div>
)
}
I create the Tooltip in a different class component as follows
function Category(props) {
return (
<Tooltip content={<AddCategoryInnerTooltip name={props.name}/>}>
<p className="tooltip-name-opener">{props.name}</p>
</Tooltip>
);
}
function AddCategoryInnerTooltip(props) {
const {close} = useContext(TooltipContext);
return(
<div className="inner-tooltip-wrapper">
<input
className="tooltip-custom-input"
type="text"
defaultValue={props.name}
/>
<div className="button-end">
<button onClick={close}>Cancel</button>
<button>Ok</button>
</div>
</div>
)
}
When I attempt to call close within the AddCategoryInnerTooltip, the state passed from the Tooltip component doesn't update. When I console.log the state, it always comes as true without changing. What am I doing wrong?
should be a callback function
<button onClick={()=>close}>Cancel</button>

how to access a component style in react js

I have a div tag I want to render only if renderCard() style overflow is scroll. I have tried a renderCard().style.overflow which does not seem to target this
Edit: renderCard added
const SearchCard = () => (
<button class="invisible-button" onClick={onSearchCardClick}>
//
</button>
);
const AnswerCard = () => (
<div className="results-set">
//
</div>
);
const renderCard = () => {
if (card && card.answer) {
return AnswerCard();
} else if (card) {
return SearchCard();
}
return null;
};
<React.Fragment>
<div id="search-results">{renderCard()}</div>
{renderFollowup ? null : (
<React.Fragment>
<div id="search-footer">
{
(renderCard().style.overflow = "scroll" ? (
<div className="scroll-button">
<a href="#bottomSection">
<img src="images/arrow_down.svg" alt="scroll to bottom" />
</a>
</div>
) : null)
}
</div>
</React.Fragment>
)}
</React.Fragment>
);
};
To do this you need a ref to the actual DOM element rendered by renderCard().
renderCard() here returns a React element which doesn't have the style property or any other DOM properties on it - it's just a React representation of what the DOM element will eventually be once rendered - hence you need to get the actual DOM element via a ref where you'll have access to this and other properties.
Example code below using useRef to create the ref that will be attached to the element with the style you need to access. Note how useEffect is used to access the ref's value because it's only available after the first render when the DOM element is present.
const Example = () => {
const ref = React.useRef()
React.useEffect(() => {
alert('overflow value is: ' + ref.current.style.overflow)
}, [])
return (
<div ref={ref} style={{ overflow: 'scroll' }}>hello world</div>
)
}
ReactDOM.render(<Example />, document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Refactor the cards to be actual rendered components, pass the ref and attach to the elements
const SearchCard = ({ overflowRef }) => (
<button ref={overflowRef} class="invisible-button" onClick={onSearchCardClick}>
//
</button>
);
const AnswerCard = ({ overflowRef }) => (
<div ref={overflowRef} className="results-set">
//
</div>
);
const RenderCard = ({ overflowRef }) => {
if (card && card.answer) {
return <AnswerCard overflowRef={overflowRef} />;
} else if (card) {
return <SearchCard overflowRef={overflowRef} />;
}
return null;
};
In the component rendering them, create a ref using either createRef or useRef react hook if it is a functional component
const overflowRef = createRef();
or
const overflowRef = useRef();
Pass the ref to RenderCard, to be passed on
<RenderCard overflowRef={overflowRef} />
And then check the overflow value as such
overflowRef.current.style.overflow === "scroll"
With the approach above you might want to refactor some of the HoC components and pass a function from parent to child that returns the ref of the element to be accessed also I am not sure of the scope of the block of code where you are executing the ternary.
Perhaps a simpler hack is to rely on using a useState hook with vanilla DOM selectors and passing that into the or even better, just add it to the stateless func component that wraps React.Fragment as a hook:
const myComponent = () => {
const [hasScrollOverflow, setHasScrollOverflow] = useState(false);
React.useEffect(() => {
const element = document.querySelector(".results-set");
const elementStyle = element.style;
if (elementStyle.getPropertyValue('overflow') === 'scroll') {
setHasScrollOverflow(true);
}
}, [])
return (
<React.Fragment>
<div id="search-results">{renderCard()}</div>
{renderFollowup ? null : (
<React.Fragment>
<div id="search-footer">
{hasScrollOverflow ? (
<div className="scroll-button">
<a href="#bottomSection">
<img src="images/arrow_down.svg" alt="scroll to bottom" />
</a>
</div>
) : null}
</div>
</React.Fragment>
)}
</React.Fragment>
);
};

Categories

Resources