How to independently set a button click in react? - javascript

I am trying to figure out on how to set a button that can independently be disabled when using the .map function. So I created a state whenever the button is clicked, it disables that specific button. However, it only disables all of the buttons which is not what i wanted. Is there a way to make the buttons have their own click event which can be disabled according to the index?
const [click, setClick] = useState(false);
const array = ["item 1", "item 2", "item 3"];
const disableClick = () => {
setClick(true);
};
return (
<div className="App">
{array.map((item) => (
<div>
<h2>{item}</h2>
{!click ? (
<button onClick={disableClick}>CLICK {item}</button>
) : (
<button disabled>Button clicked</button>
)}
</div>
))}
</div>
);

Move the click handler and useState in a separate component.
const ButtonView = (textContent) => {
const [disabled, setDisabled] = useState(false);
const onClick = () => {
setDisabled(true);
};
if (disabled) {
return <button disabled={disabled}>Button clicked</button>;
}
return <button onClick={onClick}>CLICK {textContent}</button>;
};
export const View = () => {
const array = ["item 1", "item 2", "item 3"];
return (
<div className="App">
{array.map((item, key) => (
<div key={key}>
<h2>{item}</h2>
<ButtonView textContent={item} />
</div>
))}
</div>
);
};
Or do this:
const ButtonView = (textContent) => {
const [disabled, setDisabled] = useState(false);
const onClick = () => {
setDisabled(true);
};
const content = disabled ? "Button clicked" : textContent
return <button disabled={disabled} onClick={onClick}>CLICK {content}</button>;
};

Disable takes an arguments you can supply it with
<button disabled={click? true:false} >Button clicked</button>
Is there a way to make the buttons have their own click event which
can be disabled according to the index?
Yes, How? instead of setting the click state to true or false, we can set it to contain name of the button const [disabledBtn, setDisabledBtn] = useState("item 1"); and then we can compare it with the items from the map.
<button disabled={disabledBtn == item ? true:false} >Button clicked</button>
This code render the buttons with the first button disabled by default, if the user clicks on one of the other available buttons it will trigger rerender and that btn will be disabled.
You should add a key to your childern components to maintain uniqueness between renderings.
This code snippet should answer your question
const [disabledBtn, setDisabledBtn] = useState("item 1");
const array = ["item 1", "item 2", "item 3"];
const handleDisableClick = (item) => {
setDisabledBtn(item);
};
return (
<div className="App">
{array.map((item, idx) => (
<div key={idx}>
<h2>{item}</h2>
<button
disabled={disabledBtn == item ? true : false}
onClick={() => handleDisableClick(item)}
>
Button clicked
</button>
</div>
))}
</div>
);

Related

In React how do I set up a question form and run different answer-boxes that a user can click?

I have created a form like structure for users to chose one box and then click next like this (all pictures below). I use a map to display the boxes and when click next I am using state to load the next answerboxes. I have also a title that I have above the map as an ordinary p-tag. The way I have tested this on is that I use a counter (also state object) that I increment every time next is clicked. But the questions are stored in arrays (se code test) and I dont know how to implement this so that I can have a flow and a working way for both the title and the arrays of questions to show up when I click next. I was thinking of using props somehow but I cant seem to figure out the way to do it. As it is now I can loop through one array (with title) and display answers in qboxes but when I want to add the next automatically I cannot do it in any smooth way.
(qbox-answer clicked and next button clicked)
code:
//For looping through and have qboxes created.
const testAnswers1 = ["answer 1", "answer 2", "answer 3", "answer 4", "answer 5", "answer 6"];
const testAnswers2 = ["answer 1", "answer 2", "answer 3", "answer 4", "answer 5", "answer 6"];
const testAnswers3 = ["answer 1", "answer 2", "answer 3", "answer 4", "answer 5", "answer 6"];
//Every array should have one title displayed until end
const questionTitles = ["title1", "title2", "title3", "title4"]
function ReportForm(props){
const [title, setTitle] = useState(titles);
const [qBoxes, setqBoxes] = useState(test);
//const [isMouseOver, setMouseOver] = useState(false);
//counter for title and qboxes
let [counter, setCounter] = useState(0);
//For storing answers
let [answers, setAnswers] = useState([]);
//Code for handling hovering over an answerbox (css) and also collect the clicked answer
const handleClick = (event) => {
if(event.currentTarget.classList.contains('active')) {
event.currentTarget.classList.remove('active');
answers.pop(event.currentTarget.value);
console.log(event.currentTarget.value);
} else {
event.currentTarget.classList.add('active');
setAnswers([...answers, event.target.value]);
}
event.preventDefault();
}
//Next button code
const submitNext = (event) => {
if (counter < titles.length -1){
setqBoxes(qboxes[counter]);
setCounter(counter = counter +1);
setTitle(event.target.value);
} else {
//resets counter to last value so nothing happens when next is clicked
setCounter(counter);
}
}
//Code for map function
return (
<div>
<p>{questionTitles[counter]}</p>
{qBoxes.map((e) => {
return (
<button
onClick={handleClick}
className="boxStyle"
name="contentYes"
value={e}
onchange={handleChange}
>{e}</button>
)
})};
<button className="buttonNext" onClick={submitNext}>Next</button>
<button className="buttonPrev" onClick={submitPrev}>Previous</button>
</div>
);
These things get much easier when using a pre-built form-handling library. Check out Formik or React-Hook-Form
For the wizard itself, consider wrapping each question (with answer array) in a separate component, so that it can't see or be bothered by the other questions.
Here is a recipe that is pretty declarative: Each question is its own component (you can map these from an array), and they are wrapped by a parent whose job it is to know where in the wizard the user is, what has been answered, and to only show the current question.
Wrapper
First of all, the Wrapper can use a state to remember where in the wizard it currently is
const increment = (x) => x + 1;
const decrement = (x) => x - 1;
const Wizard = ({ children }) => {
const maxCount = React.Children.count(children) - 1;
const minCount = 0;
const [current, setCurrent] = React.useState(minCount);
const incrementCurrent = () => setCurrent(increment);
const decrementCurrent = () => setCurrent(decrement);
const isMaxCount = current === maxCount;
const isMinCount = current === minCount;
...
Then, it can select the child component that corresponds to that state
...
return (
<fieldset className="wizard">
<legend>{name}</legend>
{React.Children.toArray(children).at(current)}
<button type="button" disabled={isMinCount} onClick={decrementCurrent}>
prev
</button>
<button type="button" disabled={isMaxCount} onClick={incrementCurrent}>
next
</button>
</fieldset>
)
You can try it out with just any children
<Wizard name="my wizard">
<span>how</span>
<span>YOU</span>
<span>doin'</span>
</Wizard>
Add form-handling library
Everything gets easier if we use a library to help us handle user input in form-like situations. It probably need some kind of initialisation, so we do that in the parent as well, and make sure to pass whatever controls or registering functions that the children (questions components) are going to need. In this example, I went with React-Hook-Form
const form = useForm();
const onSubmit = () => {
console.log(form.getValues())
}
return (
...
<form onSubmit={form.handleSubmit(onSubmit)}>
{React.cloneElement(React.Children.toArray(children).at(current), { form })}
...
Receiving the form handler in the question components
Each question can now use that form prop and interact with the form
E.g text input
const DescribeMarcellus = ({ form }) => (
<article>
<h2>Describe what Marsellus Wallace looks like</h2>
<input type="text" {...form.register('description')} />
</article>
)
Or buttons like you have
const WhichCountry = ({ form }) => (
<article>
<h2>What country you from?</h2>
<div className="flexCenter">
{["What", "Tobleronistan", "Option 3"].map((ans) => (
<div
key={ans}
className="option"
style={{
backgroundColor: form.getValues("country") === ans ? "blue" : "green"
}}
onClick={() => form.setValue("country", ans)}
>
{ans}
</div>
))}
</div>
</article>
);
const SayWhatAgain = ({ form }) => (
<article>
<h2>
<i>{form.getValues("country")}</i> ain't no country I ever heard of! They
speak English in What?
</h2>
{["Yeah", "Nah"].map((ans) => (
<div key={ans}>
<input
type="radio"
id={ans}
name="contact"
value={ans}
{...form.register("english")}
/>
<label htmlFor={ans}>{ans}</label>
</div>
))}
</article>
);
Putting it all toghether:
import React from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
const increment = (x) => x + 1;
const decrement = (x) => x - 1;
const Wizard = ({ name, children }) => {
const maxCount = React.Children.count(children) - 1;
const minCount = 0;
const [current, setCurrent] = React.useState(minCount);
const incrementCurrent = () => setCurrent(increment);
const decrementCurrent = () => setCurrent(decrement);
const isMaxCount = current === maxCount;
const isMinCount = current === minCount;
const form = useForm();
const onSubmit = () => {
console.log(form.getValues());
};
return (
<fieldset className="wizard">
<legend>{name}</legend>
<form onSubmit={form.handleSubmit(onSubmit)}>
{React.cloneElement(React.Children.toArray(children).at(current), {
form
})}
<button type="button" disabled={isMinCount} onClick={decrementCurrent}>
prev
</button>
<button type="button" disabled={isMaxCount} onClick={incrementCurrent}>
next
</button>
<button type="submit">submit</button>
</form>
</fieldset>
);
};
const DescribeMarcellus = ({ form }) => (
<article>
<h2>Describe what Marsellus Wallace looks like</h2>
<input type="text" {...form.register("description")} />
</article>
);
const WhichCountry = ({ form }) => (
<article>
<h2>What country you from?</h2>
<div className="flexCenter">
{["What", "Tobleronistan", "Option 3"].map((ans) => (
<div
key={ans}
className="option"
style={{
backgroundColor: form.getValues("country") === ans ? "blue" : "green"
}}
onClick={() => form.setValue("country", ans)}
>
{ans}
</div>
))}
</div>
</article>
);
const SayWhatAgain = ({ form }) => (
<article>
<h2>
<i>{form.getValues("country")}</i> ain't no country I ever heard of! They
speak English in What?
</h2>
{["Yeah", "Nah"].map((ans) => (
<div key={ans}>
<input
type="radio"
id={ans}
name="contact"
value={ans}
{...form.register("english")}
/>
<label htmlFor={ans}>{ans}</label>
</div>
))}
</article>
);
export default function App() {
return (
<div className="App">
<Wizard name="my wizard">
<DescribeMarcellus />
<WhichCountry />
<SayWhatAgain />
</Wizard>
</div>
);
}
codeSanbox Link: https://codesandbox.io/s/simple-wizard-sspt9u?file=/src/App.js

Conditional rendering an input inside a map

I have a list of task which inside have another task(subTask).
I use a map to list all the tasks and have a button to add more subtasks to that task.
The input button is supposed to be invisible unless the button is pressed.
The problem is that I used the useState hook to conditional render the input but whenever I click in any of the button, the inputs of all tasks open and is supposed to only open the input from that certain index.
const Task = () => {
const [openTask, setOpenTask] = useState(false);
return task.map((item, index) => {
return (
<div>
<button onClick={() => setOpenTask(!openTask)}>Add</button>
{item.name}
{openTask && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
Try using array
const Task = () => {
const [openTask, setOpenTask] = useState([]);
const openTaskHandler = (id) => {
setOpenTask([id])
}
return task.map((item, index) => {
return (
<div>
<button onClick={() => openTaskHandler(item.id)}>Add</button>
{item.name}
{openTask.includes(item.id) && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
That's because you are setting openTask to true for all the items. You can create object and store the specific id to true.
for eg.
const [openTask, setOpenTask] = useState({});
and then on button onClick you can set the specific id to open
setOpenTask({...openTask,[`${item.name}-${index}`]:true});
and then check against specific id
{openTask[`${item.name}-${index}`] && <input placeholder="Add more tasks..."/>}

How to change cart button "ADD" to some other name

I want to make the "ADD" button to " ADDED" after I clicked the ADD button and make it disabled.
{
products.map((val, i)=>(
<div className="product_item">
{
val.productType === 'chicken'?
<>
<img src={img2}></img>
<p className="product_data">Chicken Name: {val.chickeName}</p>
<p align="left" className="product_data">weight: 500gm</p>
<p align="left"className="product_data">₹{val.price}/- <button onClick={addTocartProduct}>ADD</button></p>
</>: null
}
</div>
))
}
I have sample project in react js having add to cart button is there. But I want to make the "ADD" button to " ADDED" after I clicked the ADD button and make it disabled.
If anyone can know please, drop your answer.
Considering that you have an array of products you should initialize an array of bool:
const [buttonsClicked, setButtonsClicked] = useState(new Array(products).fill(false));
Then
{
products.map((val, i)=>(
<div className="product_item">
{
val.productType === 'chicken'?
<>
<img src={img2}></img>
<p className="product_data">Chicken Name: {val.chickeName}</p>
<p align="left" className="product_data">weight: 500gm</p>
<p align="left"className="product_data">₹{val.price}/- <button onClick={(e) => addTocartProduct(val, i)}>{buttonsClicked[i] ? 'ADDED' : 'ADD'}</button></p>
</>: null
}
</div>
))
}
And finally the addTocartProduct function:
const addTocartProduct = (val , i) => {
let result = [...buttonsClicked];
result[i] = !result[i];
setButtonsClicked(result);
}
Youjust need a simple ternary condition and a state. Here is an example :
const App = () => {
const [isAdded, setIsAdded] = React.useState(false);
const handleClick = () => {
setIsAdded(true);
}
return (
<div>
<button onClick={handleClick}>{isAdded ? 'ADDED' : 'ADD'}</button>
</div>
);
};
You have to define some state in your component
const [ctaButton,setCtaButton] = useState('ADD')
Then in your addTocartProduct function set the state to new one
setCtaButton('ADDED')
Finally
<button onClick={addTocartProduct}>{ctaButton}</button>

Is there a way to iterate over a list of buttons & toggle a specific button when it is clicked?

Issue - When I click one button to display answer text, the other button opens too. I only want the answer for each question to be opened when I click on it.
My Code -
import React, { useState } from "react"
const Questions = [
{
question: "What is your favourite colour?",
answer: "My favourite colour is pink",
},
{
question: "What is your favourite animal?",
answer: "My favourite animal is a monkey",
},
]
const FAQs = () => {
const [isOpen, setIsOpen] = useState(false)
const toggle = () => setIsOpen(!isOpen)
return (
<div>
{Questions.map((element, i, array) => {
return (
<div>
<button onClick={toggle} key={i}>
{element.question}
</button>
{isOpen && <p key={i}>{element.answer}</p>}
</div>
)
})}
</div>
)
}
export default FAQs
Make your isOpen to an object, then assign the boolean value as isOpen[index]=false|true as required. And in the HTML, bind as isOpen[i] &&...
The poblem what you are doing here is that the variable isOpen is not related to your Questions objects.
You can change isOpen to an object and set it like this.
const [isOpen, setIsOpen] = useState({})
const toggle = (i) => setIsOpen({...isOpen, [i]:!isOpen[i]})
Then use it like this
<button onClick={() => toggle(i)} key={i}>
{element.question}
</button>
{isOpen[i] && <p key={i}>{element.answer}</p>}

How to toggle CSS class between buttons rendered with .map()

I'm trying to change the color of a button from gray to blue when it gets clicked, and also change the color of the previously selected button back to gray, so only the current selected button is blue. However, the buttons are generated using the map() method, so with useState I get all buttons to change color at the same time:
codesandbox
import React, {useState } from "react";
import "./styles.css";
export default function App() {
const menuItems = ["day 1", "day 2", "day 3"]
const [active, setActive] = useState(false);
return (
<nav>
{menuItems.map((days, idx) => {
return (
<button
key={days}
onClick={() => {
setActive(!active)
}}
style={{backgroundColor: active? "blue" : ""}}
>
{days}
</button>
);
})}
</nav>
);
};
A possible solution is to store the currently selected button's name (or some Id) in the state.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const menuItems = ["day 1", "day 2", "day 3"];
const [activeButton, setActiveButton] = useState('');
return (
<nav>
{menuItems.map((days, idx) => {
return (
<button
key={days}
onClick={() => {
setActiveButton(days);
}}
style={{ backgroundColor: activeButton === days ? "blue" : "" }}
>
{days}
</button>
);
})}
</nav>
);
}
Make sure your buttons have different names (as they are already now) for this to work and also because you're passing them in the key prop. If you think more than one button could have the same names in the future, add an id field too.
const menuItems = [{id: 1, name: "day 1"}, {id: 2, name: "day 2"}, {id: 3, name: "day 3"}];
Update the code accordingly. It won't be a good idea to pass array index in the key prop, if the situation changes. So, the id field will come in handy.
You can change your active state from a bool to a number and set it to the index of the selected button
const [active, setActive] = useState(null);
And change your click event
onClick={() => {
setActive(idx)
}}
And change your class condition to this
style={{backgroundColor: active === idx ? "blue" : ""}}
You should do this way:
export default function App() {
const menuItems = ["day 1", "day 2", "day 3"];
const [active, setActive] = useState(0);
return (
<nav>
{menuItems.map((days, idx) => {
return (
<button
key={days}
onClick={() => {
setActive(idx);
}}
style={{ backgroundColor: idx===active ? "blue" : "" }}
>
{days}
</button>
);
})}
</nav>
);
}
In this case, you selected index key
Currently, the state active will get toggled no matter which button you're firing, so you'll be changing the state that every button depends on.
One of the ways for you to solve this problem would be for you to create a button component that has the state action, by doing this each state will belong to the instance of the component.
Once you created your component, map the component instead and pass in the props you need
export default function App() {
const menuItems = ["day 1", "day 2", "day 3"];
return menuItems.map((day, idx) => <Button key={idx} day={day} />);
}
function Button({ day }) {
const [active, setActive] = useState(false);
return (
<button
style={{ backgroundColor: active ? "blue" : "" }}
onClick={() => setActive(!active)}
>
{day}
</button>
);
}

Categories