I have a drop down with check boxes inside it (used Bootstrap). When two are checked, I want to make it so the users cannot check anymore until the others are unchecked. I attempted this using the disable attribute but keep getting the error below.
What I tried:
export default function Test() {
const [countries, setCountries] = useState([]);
const [disableRadio, setDisableRadio] = useState(false);
function createCountryList() {
const countries = ["a", "b", "c", "d"];
return countries.map((country, key) => (
<Form.Check
disabled={disableRadio ? (e) => e.target.checked : false}
key={key}
label={country}
onChange={(e) =>
e.target.checked ? handleChecked(e) : handleUncheck(e)
}
/>
));
}
function handleChecked(e) {
const Arr = countries;
Arr.push(e.target.value);
setCountries(Arr);
if (countries.length > 1) {
setDisableRadio(true);
} else {
setDisableRadio(false);
}
}
function handleUncheck(e) {
const Arr = countries;
const index = Arr.indexOf(e.target.value);
Arr.splice(index, 1);
setCountries(Arr);
}
return (
<>
<Dropdown>
<Dropdown.Menu as={CustomMenu}>
{createCountryList()}
</Dropdown.Menu>
</Dropdown>
</>
);
}
Getting this error: react-dom.development.js:67 Warning: Invalid value for prop 'disabled' on <input> tag. Either remove it from the element, or pass a string or number value to keep it in the DOM.
What I want:
Thanks!
There are several issues with your code, and I have a feeling that you're overcomplicating the handling of checkbox events.
You have countries as a state, and also as a fixed array of available countries. I'd suggest you store them separately: one is pure data (fixed array of available countries), and the other is the component state.
You can use Set to handle adding/removing of countries, without too much of a fuss. There is no need to have separate event handlers for checked/unchecked states. We can handle adding/removing selected countries to the array using this logic below:
const [countries, setCountries] = useState(new Set());
function onChange(e) {
if (e.target.checked) {
setCountries(c => new Set([...c, e.target.value]));
} else {
setCountries(c => new Set([...c].filter(x => x !== e.target.value)))
}
}
There is no need to store disableRadio in a separate state. Instead, take advantage of useMemo, or simply use the logic of countries.size > 1 in the template itself. Combine that with countries.has(country) to check whether a given checkbox's value is in the set of values. In this sense, a checkbox is disabled when (1) more than 1 countries have been selected AND (2) the count is NOT in the list of selected countries
disabled={countries.size > 1 && !countries.has(country)}
Your checkbox options are missing the value attribute: remember to add that.
With all these changes proposed, your code can be simplified into something like this:
export default function Test() {
// NOTE: Store available countries as a constant
const availableCountries = ["a", "b", "c", "d"];
// NOTE: countries here refer to SELECTED countries
// We use new Set() so we automatically dedupe values
const [countries, setCountries] = useState(new Set());
function createCountryList() {
return availableCountries.map((country, key) => (
<Form.Check
disabled={countries.size > 1 && !countries.has(country)}
key={key}
value={country}
label={country}
onChange={onChange}
/>
));
}
function onChange(e) {
if (e.target.checked) {
setCountries(c => new Set([...c, e.target.value]));
} else {
setCountries(c => new Set([...c].filter(x => x !== e.target.value)))
}
}
return (
<>
<Dropdown>
<Dropdown.Menu as={CustomMenu}>
{createCountryList()}
</Dropdown.Menu>
</Dropdown>
</>
);
}
See example of CodeSandbox:
Related
This is my code for the category filter. I used this in all places.
{categapi.map((data, index) => (
<Form.Check
onChange={props.handleSingleCheck }
key={index}
name={data.category_id}
label={data.category_name}
type="checkbox"
id={data.category_name}
/>
))}
It's the function for the handle the checkbox.
const handleSingleCheck = (e) => {
if (e.target.checked == true) {
const { name } = e.target;
isChecked.push(name);
console.log(isChecked);
setIsChecked([...isChecked]);
} else {
let index = isChecked.indexOf(e.target.name);
isChecked.splice(index, 1);
}
catfilter();
};
when selecting two categories it is selected. But when I click the more button it opens the modal but the selected items are disabled in the modal.
When clicking on the checkbox it needs to check both inside the modal and outside the modal. How to do this?
In your handleSingleCheck function you are pushing the values and slicing the state variable isChecked directly. Which is the wrong way to interact with react state. Learn more here (you should treat the state variables as if they were immutable)
Change your handleSingleCheck logic as follows:
const handleSingleCheck = (e) => {
let isCheckedCopy = [...isChecked] // create a local variable
if (e.target.checked == true) {
const { name } = e.target;
isCheckedCopy.push(name) // push the data into local mutable array
} else {
let index = isCheckedCopy.indexOf(e.target.name);
isCheckedCopy.splice(index, 1); // slice the mutable array
}
console.log(isCheckedCopy);
setIsChecked([...isCheckedCopy]); // change isChecked state through setIsChecked(...)
catfilter();
};
I am creating a page to display all the tasks that I get from redux. this is how I get my tasks
const tasks = useSelector((state)=>state.tasks);
I have also search input so the user can enter a value and get all the tasks that contains this value in their names
const [search,setSearch] = useState('');
<input onChange={(e)=> {setSearch(e.target.value)} }></input>
I created a function that returns the list of tasks based on the search value
const getTasksWithSearch = () => {
var myTasks = [];
tasks.forEach((t) => {
if(t.name.search(search) !== -1){
myTasks.push(t);
}
})
return myTasks ;
}
now the problem is that I don't know how to display it. this is how I work with tasks
tasks.map((t,index) => (
<div key={index}>my code</div>
))
I want to use the return of this function how can I ?
you can create a useState hook in which you store your tasks.
const [searchTasks,setSearchTasks] = useState('');
then get advantage of useEffect to update your searchTasks each time the value of search gets updated.
useEffect(()=>{
if(search === '') {
setSearchTasks(tasks)
}else{
setSearchTasks(getTasksWithSearch(tasks));
}
},[search])
finlly in your jsx you display searchTasks instead of tasks
if(searchTasks === ''){
return <div>Loading..<div/>
}else{
return(
<>
...
searchTasks.map((task)=>(
<div key={index}>...</div>
))
</>
)
}
In UI it will look like this :
{tasks.map((t,index) => (
<div key={index}>{t.name}</div>
))}
I have this React Component like below:
const ProductCell = (props) => {
const [option, setOption] = useState();
return(
<div>
<NativeSelect
value={option}
onChange={e => setOption(e.target.value)}
>
{props.product.variations.nodes.map( // here I extracted all the item
(item, i) => (
<option value={item} key={i}>{item.name}</option> // set option value to item object
))
}
</NativeSelect>
<Typography variant="h5" className={classes.title}>
{option.price} // I want to update the value of price according to the selected option.
</Typography> // according to the selected option above
</div>
)
}
I have a NativeSelect component which is from React Material-Ui, so basically it is a Select html tag. In the code above, what I do is, extract all the element inside props.product.variations.nodes and put all the extracted item and put each of the element into a <options/> tag.
The Json object for item will look like this:
"variations": {
"nodes": [
{
"id": "someId",
"name": "abc1234",
"variationId": 24,
"price": "$100.00"
},
{
.. another ID, name,variation and price
}
]
}
As you can see, I targeting the part of id, name , variationId and price as an object. Therefore each <option/> tag will present with item.name as the presentation to user. So far in this part having no problem, let say having 5 variations, and can present all of them.
What I want to do is:
I want to update the value of price under the <Typography /> component. Example, user selected 3rd options in the Select, I want to update the price value of the 3rd item in <Typography /> .
What I tried:
I create a react hooks const [option, setOption] = useState(); , then when handleChange, I setOption() with event.target.value in NativeSelect component . Therefore the value of <option /> tag is set as item object.
Lastly, I get the price value from the hooks in the Typography section.
But what I get is:
The price value is undefined in console log. So I can't get the value of option.price.
and this error:
TypeError: Cannot read property 'price' of undefined
Question:
How can I get the option.price value(which I expect it is same with item.price) outside the NativeSelect component in my above example?
I tried my best to explain based on what I understand by this time being. So any help will be well appreciated.
Update:
Here is what I got when console log the item object in variation.node.map() section and data object inside onHandleChanged section, but also produce the same result:
You have to set a default selected option on your ProductCell component. Also your onChange handler will receive a string instead of an object when you access the value on event.target.value.
From the docs
function(event: object) => void event: The event source of the callback. You can pull out the new value by accessing event.target.value (string).
event.target.value will be a string even though you pass the value as object on NativeSelect component.
What you might want to do? Don't set the current selected item as an object, instead use the id and have a function that look-ups the item using the current selected id.
Check the code below.
const ProductCell = (props) => {
const { variations } = props.product;
const { nodes } = variations;
// we're setting the first node's id as selected value
const [selectedId, setSelectedId] = useState(nodes[0].id);
const getSelectedPrice = () => {
// finds the node from the current `selectedId`
// and returns `price`
const obj = nodes.find((node) => node.id === selectedId);
return obj.price;
};
function onChange(event) {
// event.target.value will be the id of the current
// selected node
setSelectedId(parseInt(event.target.value));
}
return (
<div>
<NativeSelect value={selectedId} onChange={onChange}>
{nodes.map((item, i) => (
<option value={item.id} key={i}>
{item.name}
</option>
))}
</NativeSelect>
<Typography variant="h5">{getSelectedPrice()}</Typography>
</div>
);
};
Also notice that were passing the id as a value prop on each of our options.
<option value={item.id} key={i}>
And how we now display the price, we're calling our getSelectedPrice().
Update
I thought a better solution. I realized that you can set your selected state as an object and on your onChange handler given the id from event.target.value find the item on nodes and set that as your new selected state.
const ProductCell = (props) => {
const { variations } = props.product;
const { nodes } = variations;
const [selected, setSelected] = useState(nodes[0]);
function onChange(event) {
const value = parseInt(event.target.value);
setSelected(nodes.find((node) => node.id === value));
}
return (
<div>
<NativeSelect value={selected.id} onChange={onChange}>
{nodes.map((item, i) => (
<option value={item.id} key={i}>
{item.name}
</option>
))}
</NativeSelect>
<Typography variant="h5">{selected.price}</Typography>
</div>
);
};
I'm trying to create a component that tests english vocabulary.
Basically, there are 4 options with 1 correct.
When user chooses option, the right option is highlited in green, and the wrong one in red.
Then user can push the "next" button to go to the next batch of words.
I store refs in object (domRefs, line 68).
Populate it at line 80.
And remove all refs at line 115.
But it doesnt get removed, and leads to error (line 109)
https://stackblitz.com/edit/react-yocysc
So the question is - How to store these refs and what would be the better way to write this component?
Please help, Thanks.
You shouldn't keep refs for component in global variable, since it's making your component singleton. To apply some styles just use conditional rendering instead. Also, it's better to split your test app into several separate components with smaller responsibilities:
const getClassName(index, selected, rightAnswer) {
if (selected === null) {
return;
}
if (index === rightAnswer) {
return classes.rightAnswer;
}
if (index === selected) {
return classes.wrongAnswer;
}
}
const Step = ({ question, answers, rightAnswer, selected, onSelect, onNext }) => (
<div ...>
<div>{ question }</div>
{ answers.map(
(answer, index) => (
<Paper
key={ index }
onClick={ () => onSelect(index) }
className={ getClassName(index, selected, rightAnswer) }
) }
{ selected && <button onClick={ onNext() }>Next</button> }
</div>
);
const Test = () => {
const [ index, setIndex ] = useState();
const word = ..., answers = ..., onSelect = ..., onNext = ...,
return (
<Question
question={ word }
answers={ answers }
... />
);
}
I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.