React: trigger onChange on checkbox input when changing checked state programmatically? - javascript

I have a ref to an <input type="checkbox"/> element, and when I programmatically set checked=false on the element, the element's onChange callback does not get called.
I tried using ref.dispatchEvent(new Event('input')) and ref.dispatchEvent(new Event('change')) and neither caused the React onChange callback to get executed.
All the questions and answers I could find on StackOverflow about this have to do with <input type="text"/> elements, none dealing with changing the checked property programmatically on an <input type="checkbox"/> element and its onChange handler not being invoked.
Here's a CodePen that demonstrates the issue:
https://codepen.io/dossy/pen/QWKVNzZ/left/?editors=0011
You can check and uncheck the checkbox, and the <div>Checked!</div> will appear and disappear as expected. However, clicking the <button>Reset</button> will uncheck the checkbox if it's checked, but since the input's onChange handler isn't being executed, the div isn't being hidden as it should be.
...
Yes, I know that I could do this as a Controlled Component but that's not the point: I have a use case where using refs is required so I must implement this as an Uncontrolled Component, and getting the onChange handler to execute when the DOM element changes is the problem I need to solve.
Thanks!

Here is the working code. working link https://codesandbox.io/s/blissful-wozniak-cc1sn?file=/src/Test.js:0-1753
import React, { useEffect, useRef } from "react";
export function Test() {
const ref_input = useRef(null);
const ref_text = useRef(null);
useEffect(() => {
ref_input.current.addEventListener("change", function (event) {
alert(event.target.checked);
});
}, []);
function triggerEvent(element, eventName) {
var event = document.createEvent("HTMLEvents");
event.initEvent(eventName, false, true);
element.dispatchEvent(event);
}
return (
<div className="h-screen flex bg-white text-gray-900 justify-center items-center">
<div className="flex items-start w-64">
<div className="flex items-center gap-4">
<button
className="inline-flex items-center px-4 py-2 border border-gray-500 rounded-md"
onClick={() => {
ref_input.current.checked = false;
triggerEvent(ref_input.current, "change");
//ref_input.dispatchEvent(new Event("input"));
//ref_input.current.dispatchEvent(new Event("onChange"));
}}
>
Reset
</button>
<div>Checkbox:</div>
<input
ref={ref_input}
type="checkbox"
className="h-4 w-4 border-gray-300 rounded"
// onChange={(e) => {
// console.log("onChange called", e.target.checked);
// e.target.checked
// ? ref_text.current.classList.remove("hidden")
// : ref_text.current.classList.add("hidden");
// }}
/>
<div ref={ref_text} className="hidden">
Checked!
</div>
</div>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));

It's better to do things the "react way".
That means, instead of manipulating dom elements with imperative code (if you're using refs, you're using imperative code), do it declaratively with state/props:
function App() {
const [checked,setChecked] = React.useState(false);
return (
<div className="h-screen flex bg-white text-gray-900 justify-center items-center">
<div className="flex items-start w-64">
<div className="flex items-center gap-4">
<button
className="inline-flex items-center px-4 py-2 border border-gray-500 rounded-md"
onClick={() => setChecked(false)}
>
Reset
</button>
<div>Checkbox:</div>
<input
type="checkbox"
checked={checked}
onChange={() => setChecked(!checked)}
className="h-4 w-4 border-gray-300 rounded"
/>
<div className={checked ? '' : 'hidden'}>
Checked!
</div>
</div>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Here's a link to the updated pen: https://codepen.io/zuze-lab/pen/QWKVEbY?editors=0011
EDIT: I want to be really clear, ref's aren't bad, not at all. Lots of react libraries expose refs because imperative code makes sense for those libraries APIs. When using refs to add event listeners, or imperatively manipulate elements, you're doing things wrong and you need to back up.

Related

Incorrect component display when passing props and using Tailwind

I pass the color data to the created component to set a specific color in several places. At the output, it turns out that it is installed only in one place. What could it be? In the project I use only tailwind.
<div className="w-full flex justify-between items-center gap-5">
<FilterButtons dataFilter={""} flag="Artists" color="yellowColor" />
<FilterButtons dataFilter={""} flag="Albums" color="orangeColor" />
</div>
<div className="w-full flex justify-between items-center gap-5">
<FilterButtons dataFilter={filterByCategory} flag="Category" color="orangeColor" />
<FilterButtons dataFilter={filterByLanguage} flag="Language" color="yellowColor" />
</div>
Broadcast props
const FilterButtons = ({ dataFilter, flag, color }) => {
return (
<div className={`w-1/2 flex items-center justify-between px-4 py-2 border border-${color} text-${color} rounded-xl cursor-pointer`}>
<p className={`tracking-1 text-lg`}>{flag}</p>
<MdOutlineKeyboardArrowDown className={`text-4xl`} />
</div>
)
}
Usage in the component
The first block displays a field with a yellow border with black text and with orange texts and a white border
Result
I tried to divide into different variables and set conditions with different flags, nothing changes
You might be having some css specificity issue which is very common with tailwind. But tailwind has a workaround for this.
Use exclamation on the class you want to force a higher specificity for. Example (in your case):
!text-${color}

Button takes two clicks using boolean state hook

I'm fairly new to React. Here I've made a small form component for a project (with a bit of tailwind included). Above the form proper is a hidden alert box that will show on submission (green for success and red for fail). The handler that I have attached to the form shows the correct alert, however it takes two clicks. In the handler validateFormData() I'm resetting state (isError). I'm aware the useState hook is asynchronous, so my isError variable is not updating properly before I render my alert box (right?). I've tried passing callbacks to my setIsError functions (after looking online for solutions) but I could not resolve the issue. Am I even in the right ball park here? Or am I missing something?
function ContactInsert() {
const contactForm = useRef(null);
const alertBox = useRef(null);
const [isError, setIsError] = useState(false);
function showAlertBox() {
alertBox.current.classList.add("alert-show");
setTimeout(() => {
alertBox.current.classList.remove("alert-show");
}, 3000);
}
async function validateFormData() {
// unpack and validate data
const name = contactForm.current.querySelector("[name=name]").value.trim();
const email = contactForm.current.querySelector("[name=email]").value.trim();
const comment = contactForm.current.querySelector("[name=comment]").value.trim();
if (name.length > 0 && email.length > 0 && comment.length > 0) {
setIsError(false);
} else {
setIsError(true);
}
showAlertBox();
}
return (
<div className="flex flex-col">
<div className="flex justify-center my-2">
<h1>Drop me a message!</h1>
</div>
{isError ?
<div ref={alertBox} className="flex h-12 w-2/3 justify-center bg-[#ff462e] invisible">hello</div> :
<div ref={alertBox} className="flex h-12 w-2/3 justify-center bg-[#77ff6e] invisible">hello</div>
}
<form
className="flex flex-col items-center justify-center md:h-full"
method="POST"
name="contact"
id="contact"
type="submit"
ref={contactForm}
onSubmit={(e) => {
e.preventDefault();
validateFormData();
}}
>
<div className="flex flex-col justify-between w-2/3">
<label>name</label>
<input type="text" name="name"/>
</div>
<br/>
<div className="flex flex-col justify-between w-2/3">
<label>email</label>
<input type="text" name="email"/>
</div>
<br/>
<div className="flex flex-col justify-between w-2/3 h-40">
<label>comment</label>
<textarea className="h-full" name="comment"/>
</div>
<div className="flex w-2/3 justify-start my-4">
<button className="p-1" form="contact">Submit</button>
</div>
</form>
</div>
);
}
It is always a bad idea to manipulate DOM directly as React will have a hard time to match the rendered output and defeats the purpose of using react. Better to store this info in the state and let react handle it.
Also setState is asynchronous, and calling that will force the react component to rerender. We can use useEffect to let React know that component has to do something on next render.
function ContactInsert() {
const [isError, setIsError] = useState(false);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [comment, setComment] = useState('');
const [displayAlert, setDisplayAlert] = useState(false);
const handleNameOnchangeHandler = (event) => {
setName(event.target.value);
};
const handleEmailOnchangeHandler = (event) => {
setName(event.target.value);
};
const handleCommentOnchangeHandler = (event) => {
setName(event.target.value);
};
const validateFormData = () => {
const hasError = !name || !email || !comment;
setIsError(hasError);
}
useEffect(() => {
if(isError) {
setDisplayAlert(true);
setTimeout(() => {
setDisplayAlert(false);
setIsError(false);
}, 3000);
}
}, [isError]);
return (
<div className="flex flex-col">
<div className="flex justify-center my-2">
<h1>Drop me a message!</h1>
</div>
{displayAlert ?
<div ref={alertBox} className="flex h-12 w-2/3 justify-center bg-[#ff462e] show-alert">hello</div> : null
}
<form
className="flex flex-col items-center justify-center md:h-full"
method="POST"
name="contact"
id="contact"
type="submit"
ref={contactForm}
onSubmit={(e) => {
e.preventDefault();
validateFormData();
}}
>
<div className="flex flex-col justify-between w-2/3">
<label>name</label>
<input type="text" name="name" onchange={handleNameOnchangeHandler}/>
</div>
<br/>
<div className="flex flex-col justify-between w-2/3">
<label>email</label>
<input type="text" name="email" onchange={handleEmailOnchangeHandler}/>
</div>
<br/>
<div className="flex flex-col justify-between w-2/3 h-40">
<label>comment</label>
<textarea className="h-full" name="comment" onchange={handleCommentOnchangeHandler}/>
</div>
<div className="flex w-2/3 justify-start my-4">
<button className="p-1" form="contact">Submit</button>
</div>
</form>
</div>
);
}
This is an example using useEffect. This can also be done without using it. You could also have a single onchange handler instead and set the value based on the target where the event was triggered.
you are using react completely wrong.
i highly recommend you to read react docs carefully and digging into the rendering concept.
the problems i saw in your code was :
the way you’re getting the input value is not good.
you should define a state (controlled component) or pass a ref to get the value (uncontrolled).
changing your css classes with a ref is not gonna work because it doesn’t trigger a rerender in your app (read useRef docs for more details)
you should define a new state and use that state for changing your className.

Cant get the selected value from input

I'm trying to get the task value from the button when pressed to show it in the side nav, but the value is always empty even though it's not. help appreciated.
See the comments in the code for more details:
import './index.css';
import React, {useState} from 'react';
function App() {
return (
<div>
<TodoListItem />
</div>
);
}
const TodoListItem = (props) => {
//for task list
const [tasks, setTasks] = useState(['']);
//toggling the side menu
const [toggle, setToggle]= useState(true);
//toggling grid layout
const [grid, setGrid]= useState('');
//getting the selected item !! not working
const [selectedTask, setSelectedTask]= useState('');
//brings out the side nav bar
const TodoItemDetails = () => {
setToggle(false)
setGrid("grid grid-cols-2")
}
const onFormSubmit = e => {
e.preventDefault();
}
return (
<div class="bg-gray-100 items-center justify-center min-h-screen min-w-screen p-4">
<div class={grid}>
{/* grid one */}
<div>
{/* task form */}
<form onSubmit={onFormSubmit} class="bg-white rounded py-4 px-2 my-4 h-16 shadow-sm">
<input
class="w-[92%] h-full float-left focus:outline-none placeholder-blue-500 ml-2 py-1 focus:placeholder-black"
type="text"
id="task"
placeholder="Add a task"
/>
{/* task add button*/}
<button
type="submit"
class="text-blue-500 float-right text-2xl -translate-y-0.5 -translate-x-1"
onClick={() => {
let taskdom = document.getElementById("task");
{/* creates individual task */}
let task = <button
onClick={() => {
{/* nav bar comes out whne the invidual task is pressed with the task value */}
TodoItemDetails();
{/* the below setSelectedTask should set */}
{/* the selected task and get the value from taskdom.value but its always empty (cont) */}
setSelectedTask(taskdom.value);
}}
{/* even though taskdom.value works properly right after that */}
class="bg-white w-full hover:bg-gray-200 p-4 my-1 rounded shadow-sm text-left">{taskdom.value}</button>
{/* adds the new task to the the task array */}
setTasks((oldTasks) => [ ...oldTasks, task ])
{/* empties the task text box */}
taskdom.value = "";
}}
>+</button>
</form>
{/* shows all the tasks */}
{tasks}
</div>
{/* grid two: side nav bar */}
<div hidden={toggle}>
{/* nav bar hides itself when this is pressed !!!! this value is supposed to be from the pressed value but its empty */}
<button onClick={() => {
setToggle(true)
setGrid("")
}}>{selectedTask}</button>
</div>
</div>
</div>
);
}
export default App;
sorry for bad formatting...this is the first time I'm posting a question even though I've been using stack overflow for 2 years and I don't know how to exactly ask this question...
The React way to do this is to use a controlled input element and store the value in local state on change. You can then get it from the state when the button is clicked.
You could use a useRef hook on your input
// logic
const [selectedTask, setSelectedTask]= useState('');
const inputValue = useRef('');
const handleInput = () => {
setSelectedTask(inputValue.current.focus());
}
// render
<input type="text" id="task" ref={inputValue} />
<button onClick={handleInput}>Click</button>
Then you should be able to use the state.

How to collapse Siblings with Headless UI

To make the accordion component with Headless UI, I have used Disclosure component. But I have a problem to control the collapse/expand state for it's siblings.
So, I want to close other siblings when I open one, but Disclosure component is only supporting internal render props, open and close. So, I can't control it outside of the component and can't close others when I open one.
import { Disclosure } from '#headlessui/react'
import { ChevronUpIcon } from '#heroicons/react/solid'
export default function Example() {
return (
<div className="w-full px-4 pt-16">
<div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2">
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
<span>What is your refund policy?</span>
<ChevronUpIcon
className={`${
open ? 'rotate-180 transform' : ''
} h-5 w-5 text-purple-500`}
/>
</Disclosure.Button>
<Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
If you're unhappy with your purchase for any reason, email us
within 90 days and we'll refund you in full, no questions asked.
</Disclosure.Panel>
</>
)}
</Disclosure>
<Disclosure as="div" className="mt-2">
{({ open }) => (
<>
<Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
<span>Do you offer technical support?</span>
<ChevronUpIcon
className={`${
open ? 'rotate-180 transform' : ''
} h-5 w-5 text-purple-500`}
/>
</Disclosure.Button>
<Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
No.
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
</div>
)
}
How do we control the close/open state outside of the component?
I don't think so it's possible using HeadlessUI, although you can create your own Disclosure like component.
Lift the state up to the parent component by creating a disclosures state that stores all the information about the disclosures.
Loop over the disclosures using map and render them.
Render a button that toggles the isClose property of the disclosures and also handles the aria attributes.
On button click, toggle the isOpen value of the clicked disclosure and close all the other disclosures.
Checkout the snippet below:
import React, { useState } from "react";
import { ChevronUpIcon } from "#heroicons/react/solid";
export default function Example() {
const [disclosures, setDisclosures] = useState([
{
id: "disclosure-panel-1",
isOpen: false,
buttonText: "What is your refund policy?",
panelText:
"If you're unhappy with your purchase for any reason, email us within 90 days and we'll refund you in full, no questions asked."
},
{
id: "disclosure-panel-2",
isOpen: false,
buttonText: "Do you offer technical support?",
panelText: "No."
}
]);
const handleClick = (id) => {
setDisclosures(
disclosures.map((d) =>
d.id === id ? { ...d, isOpen: !d.isOpen } : { ...d, isOpen: false }
)
);
};
return (
<div className="w-full px-4 pt-16">
<div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2 space-y-2">
{disclosures.map(({ id, isOpen, buttonText, panelText }) => (
<React.Fragment key={id}>
<button
className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
onClick={() => handleClick(id)}
aria-expanded={isOpen}
{...(isOpen && { "aria-controls": id })}
>
{buttonText}
<ChevronUpIcon
className={`${
isOpen ? "rotate-180 transform" : ""
} h-5 w-5 text-purple-500`}
/>
</button>
{isOpen && (
<div className="px-4 pt-4 pb-2 text-sm text-gray-500">
{panelText}
</div>
)}
</React.Fragment>
))}
</div>
</div>
);
}
it is possible just you need to add some extra props selectors to the Disclosure.Button Component, in this case, I am adding aria-label='panel' like so...
import { Disclosure } from '#headlessui/react'
function MyDisclosure() {
return (
<Disclosure>
<Disclosure.Button aria-label="panel" className="py-2">
Is team pricing available?
</Disclosure.Button>
<Disclosure.Panel className="text-gray-500">
Yes! You can purchase a license that you can share with your entire
team.
</Disclosure.Panel>
</Disclosure>
)
}
next you need to select the following with "querySelectorAll" like...
<button
type='button'
onClick={() => {
const panels = [...document.querySelectorAll('[aria-expanded=true][aria-label=panel]')]
panels.map((panel) => panel.click())
}}
>
</button>
with this, you just need to change 'aria-expanded' to either 'true' or 'false' to expand or collapse
There's a way to do this with React (assuming you're using #headlessui/react) via useState:
const [disclosureState, setDisclosureState] = useState(0);
function handleDisclosureChange(state: number) {
if (state === disclosureState) {
setDisclosureState(0); // close all of them
} else {
setDisclosureState(state); // open the clicked disclosure
}
}
And in each Disclosure component, just pass an onClick callback to the Disclosure.Button:
<Disclosure.Button onClick={() => handleDisclosureChange(N)} />
Where N is the index of the clicked Disclosure (using 1 as the first Disclosure, since 0 handles all disclosures closed).
Finally, conditionally render the Disclosure.Panel based on the disclosureState:
{
disclosureState === N && (<Disclosure.Panel />)
}
Where N is the index of the clicked Disclosure. Using this method you can open just 1 disclosure at a time, and clicking an open disclosure will close all of them.

React - List icons with different param

I'm trying to use the map function but I can't get it right.
I have a side-bar which I want to show some icons. Here is an example without the map.
const SideBar = () => {
return (
<div className="fixed top-0 left-0 h-screen w-20 m-0 flex flex-col bg-gray-100 text-white shadow-lg">
<SideBarIcon icon={<FaFire size="30" />} />
<SideBarIcon icon={<FaPoo size="30" />} />
</div>
);
};
const SideBarIcon = ({ icon, text = "tooltip 💡"}) => (
<div className="sidebar-icon group">
{icon}
<span class="sidebar-tooltip group-hover:scale-100">{text}</span>
</div>
);
Here is an example with the map function
const SideBar = () => {
const icons = [FaFire, FaPoo];
return (
<div className="fixed top-0 left-0 h-screen w-20 m-0 flex flex-col bg-gray-100 text-white shadow-lg">
{icons.map(function(icon) {
return <SideBarIcon icon={<icon size="30"/>}/>
})}
</div>
);
};
const SideBarIcon = ({ icon, text = "tooltip 💡"}) => (
<div className="sidebar-icon group">
{icon}
<span class="sidebar-tooltip group-hover:scale-100">{text}</span>
</div>
);
Can you tell me what I'm doing wrong?
Thank you for your time!
By simply putting icon inside the tags, it thinks you're rendering an HTML element called icon, therefore it's not rendering the mapped item. It also wouldn't work if you set it as <{icon}/>, because it would be trying to render an empty element.
Luckily, there's an easy fix -- Just capitalize Icon, and React will render it as a JSX Component.
{icons.map(function(Icon) {
return <SideBarIcon icon={<Icon size="30"/>}/>
})}
{icons.map(function (Icon) {
return <SideBarIcon icon={<Icon size="30"/>}/>
})}
Components start with capital letters, just change icon to Icon.
Please do not forget to use key prop for your mapped items.
https://reactjs.org/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized
const SideBar = () => {
const icons = [<FaFire size="30" />, <FaPoo size="30" />];
return (
<div className="fixed top-0 left-0 h-screen w-20 m-0 flex flex-col bg-gray-100 text-white shadow-lg">
{icons.map(function(icon) {
return <SideBarIcon icon={icon}/>
})}
</div>
);
};
Since you are using icon in lowercase, it is not recognized as a jsx component. To fix this, change it to uppercase.
turn this
{icons.map(function(icon) {
return <SideBarIcon icon={<icon size="30"/>}/>
})}
to this
{icons.map(function(Icon) {
return <SideBarIcon icon={<Icon size="30"/>}/>
})}
Also, if the underlying components allow it, you could use the direct invocation, like so:
return <SideBarIcon icon={icon({size:"30"})/>
Keep in mind, that in most cases that is not what you want, and it might introduce hard-to-fix bugs.

Categories