Very new to React. I'm using (the equivalent of) checkboxes as a way for a user to select/deselect their skills and I'm wondering what's the best way to handle the state?
Essentially the flow is like this:
Initial values are pulled from the database. The API will provide a
list of skills (by name) and all of those skills are the ones that should
start out as checked whilst the others are unchecked
The user can freely check/uncheck boxes and if they click the cancel button the states will return to the initial values
If the user presses the submit button, the state will be changed and the form submitted with the value
I've done the same thing on another page but that was text fields and there was only 5 of them so I just stored their values in the state individually. Since I have over 30 checkboxes that seems like it'd be very messy. Is there a good way to condense this sort of thing and also how would I map this?
They are checked/unchecked as indicated by their property "checked: (true/false)"
This is the code at the moment (I removed most of the select items so it wasn't a massive list):
import * as React from "react";
import { Form, Card, Grid } from "tabler-react";
import { Button } from "semantic-ui-react";
class Skills extends React.Component {
constructor(props) {
super(props);
this.state = {
showSaveButton: false,
showCancelButton: false,
};
}
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
if(event.target.checked){
console.log(event.target.checked)
} else {
console.log("not checked")
}
}
cancelChanges = () => {
this.setState(
{
showSaveButton: false,
showCancelButton: false,
});
}
render() {
return (
<div className="card" name="skills">
<Card.Body>
<Card.Title>Top skills</Card.Title>
<Grid.Row>
<Grid.Col offset={1} md={10}>
<Form.Group name="softskills" label="Soft Skills">
<Form.SelectGroup canSelectMultiple pills onChange={this.onChange}>
<Form.SelectGroupItem
label="Communication"
name="communication"
value="Communication"
/>
<Form.SelectGroupItem
label="Teamwork"
name="teamwork"
value="Teamwork"
/>
</Form.SelectGroup>
</Form.Group>
</Grid.Col>
</Grid.Row>
<Button content='Cancel changes' floated='left' color='red' basic hidden={this.state.showCancelButton ? '' : 'hidden'} onClick={this.cancelChanges}/>
<Button content='Save changes' floated='right' color='green' basic hidden={this.state.showSaveButton ? '' : 'hidden'}/>
</Card.Body>
</div>
);
}
}
export default Skills;
With plain react (without redux and hooks) you can maintain a state object of type map for checked items like this
selectedItems: new Map(),
then in onChange event update selctedItems.
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
this.setState(prevState => ({
selectedItems: prevState.selectedItems.set(event.target.name, event.target.checked),
showSaveButton: true,
showCancelButton: true
}));
}
React doc has explained this with a nice example on how to maintain state of multiple inputs (here checkboxes).
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
Related
Given I have this code (I removed a lot of the select items so it wasn't a massive list but there would be an extra 20 or so):
import * as React from "react";
import { Form, Card, Grid } from "tabler-react";
import { Button } from "semantic-ui-react";
class Skills extends React.Component {
constructor(props) {
super(props);
this.state = {
showSaveButton: false,
showCancelButton: false,
};
}
onChange = (event) => {
this.setState(
{
showSaveButton: true,
showCancelButton: true,
});
}
cancelChanges = () => {
this.setState(
{
showSaveButton: false,
showCancelButton: false,
});
}
render() {
return (
<div className="card" name="skills">
<Card.Body>
<Card.Title>Top skills</Card.Title>
<Grid.Row>
<Grid.Col offset={1} md={10}>
<Form.Group name="softskills" label="Soft Skills">
<Form.SelectGroup canSelectMultiple pills onChange={this.onChange}>
<Form.SelectGroupItem
label="Communication"
name="communication"
value="Communication"
/>
<Form.SelectGroupItem
label="Teamwork"
name="teamwork"
value="Teamwork"
/>
</Form.SelectGroup>
</Form.Group>
</Grid.Col>
</Grid.Row>
<Button content='Cancel changes' floated='left' color='red' basic hidden={this.state.showCancelButton ? '' : 'hidden'} onClick={this.cancelChanges}/>
<Button content='Save changes' floated='right' color='green' basic hidden={this.state.showSaveButton ? '' : 'hidden'}/>
</Card.Body>
</div>
);
}
}
export default Skills;
The current functionality is that on change, 2 buttons will appear that are cancel or accept.
I need the below functionality but I can't work out how to do it unless I have like 60+ states (an initial and a working state for each option) which seems ridiculous:
The initial state is pulled from a database in a JSON array whereby everything that appears in that array should start out as selected (checked=true). For example, if the array is ["communication", "timemanagement"] I need to set the Communication and Time Management options to checked=true.
The initial state needs to be saved so that if anything changes and then the user clicks cancel, the checked boolean for each option is reset to what it was originally
If accept is clicked, the information needs to be sent to the database and so it needs to know what options have checked=true and be able to grab their names
So is there a way to do this without having a massive amount of states?
What you can do is create a mapping in state for all 60. When you get the results from the database, store them in state with fields to track checked and changed statuses:
// isDirty is a flag to say there are pending changes on the option
const options = arrayFromDatabase.map(arrayElement => ({ name: arrayElement, checked: true, isDirty: false })
then store that array in your state, e.g.,
this.setState({...this.state, options })
When a change is made, mark the specific option as dirty -> isDirty = true. If it's cancelled, flip the flag back to false -> isDirty = false.
Should look something like,
this.setState({
...state,
options: this.state.map(
option => option.name === valueToChange ? {
...option,
isDirty: true | false } :
option
)
})
Handle your check-changed in the same way.
I followed a tutorial and created a form with as many checkboxes to be clicked. But, in another case, I need only one box to be checked. The values of checkboxes are dynamic and you never know, how many checkboxes will be created. But, only one can be clicked. Can you please help me in finding the solution thankyou.
import React, { Component } from 'react';
import Checkbox from "./Checkbox.component";
class PatientSelectTiming extends Component {
state = {
options: [...this.props.props],
checkboxes: [...this.props.props].reduce(
(options, option) => ({
...options,
[option]: false
}),
{}
),
appointmentSlots: null
};
handleCheckboxChange = e => {
const { name } = e.target;
this.setState(prevState => ({
checkboxes: {
...prevState.checkboxes,
[name]: !prevState.checkboxes[name]
}
}))
}
handleFormSubmit = formSubmitEvent => {
formSubmitEvent.preventDefault();
Object.keys(this.state.checkboxes)
.filter(checkbox => this.state.checkboxes[checkbox])
.forEach(checkbox => {
let appointmentSlot = [];
appointmentSlot.push(checkbox);
console.log(appointmentSlot);
this.setState({appointmentSlots: appointmentSlot})
localStorage.setItem('appointmentSlots', JSON.stringify(appointmentSlot))
});
};
createCheckbox = option => (
<Checkbox
label={option}
isSelected={this.state.checkboxes[option]}
onCheckboxChange={this.handleCheckboxChange}
key={option}
/>
);
createCheckboxes = () => this.state.options.map(this.createCheckbox);
render() {
return (
<div>
<p>Only select one item and only first date clicked will be your time</p>
<form onSubmit={this.handleFormSubmit}>
{this.createCheckboxes()}
<button type="submit">
Save
</button>
</form>
{this.state.appointmentSlots === null ? <p>Click on any slot to get your time.</p> : <p>Your time is {JSON.parse(localStorage.getItem("appointmentSlots"))}</p>}
</div>
)
}
}
export default PatientSelectTiming;
You can use a radio button
https://www.w3schools.com/tags/att_input_type_radio.asp
Radio button is the same as checkbox but only allows users to check only 1 option.
So I am trying to render a form to start a new darts game conditionally depending on the game type selected. The form has a local state and "knows" which game is selected. So when selecting game "X01" I need a variant, inCondition and outCondition Dropdown whereas the game "Cricket" just needs one additional dropdown for variant (other values than x01 variants). I started to design a game form which looks like this:
gameform.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import SelectInputMultiple from '../common/SelectInputMultiple';
import SelectInput from '../common/SelectInput';
import { games, x01Variants, conditions, cricketVariants } from './assets';
export default class GameForm extends Component {
constructor(props) {
super(props);
this.players = props;
this.handleMultipleChange = this.handleMultipleChange.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
state = {
selectedPlayers: [],
game: 'x01',
x01variant: '501',
inCondition: 'straight',
outCondition: 'double',
cricketVariant: 'cutthroat',
errors: {}
};
formIsValid() {
const _errors = {};
if (this.state.selectedPlayers.length === 0)
_errors.selectedPlayers = 'You need to select at least one player';
this.setState({
errors: _errors
});
return Object.keys(_errors).length === 0;
}
handleChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleMultipleChange = e => {
let _selectedPlayers = [...e.target.options]
.filter(o => o.selected)
.map(o => o.value);
this.setState(prevState => ({
selectedPlayers: { ...prevState.selectedPlayers, _selectedPlayers }
}));
};
handleSubmit = e => {
e.preventDefault();
if (!this.formIsValid()) return;
let _game = {
selectedPlayers: this.state.selectedPlayers,
game: this.state.game,
x01Variant: this.state.x01variant,
inCondition: this.state.inCondition,
outCondition: this.state.outCondition,
cricketVariant: this.state.cricketVariant
};
this.props.onSubmit(_game);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<SelectInputMultiple
id="players"
label="Players"
name="players"
onChange={this.handleMultipleChange}
options={this.props.players}
error={this.state.errors.selectedPlayers}
/>
<SelectInput
id="game"
label="Game Type"
name="game"
onChange={this.handleChange}
options={games}
value={this.state.game}
error={this.state.errors.game}
/>
<SelectInput
id="x01Variant"
label="X01 Variants"
name="x01Variant"
onChange={this.handleChange}
options={x01Variants}
value={this.state.x01variant}
error={this.state.errors.x01Variants}
/>
<SelectInput
id="inCondition"
label="In Condition"
name="inCondition"
onChange={this.handleChange}
options={conditions}
value={this.state.inCondition}
error={this.state.errors.condition}
/>
<SelectInput
id="outCondition"
label="Out Condition"
name="outCondition"
onChange={this.handleChange}
options={conditions}
value={this.state.outCondition}
error={this.state.errors.condition}
/>
<SelectInput
id="cricketVariant"
label="Variant"
name="cricketVariant"
onChange={this.handleChange}
options={cricketVariants}
value={this.state.cricketVariant}
error={this.state.errors.cricketVariant}
/>
<input type="submit" value="Start Game" className="btn btn-primary" />
</form>
);
}
}
GameForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
players: PropTypes.array
};
So my goal is to just show the corresponding fields linked to the game type. How can I do that depending on this.state.game?
Thanks in advance for any hint!
You could just conditionally render the variant select input when the game mode is set to "Cricket"
{this.state.game === 'Cricket' && (
<SelectInput />
)}
The reason you can write it as such is because in JavaScript the && operator basically returns the first value if it is falsy and the second value if the first value is truthy. i.e.
a && b will return 'a' if 'a' is falsy, and it will return 'b' if 'a' is truthy.
So in this case when this.state.game === 'Cricket' then the JSX code will be returned hence rendering the form input.
One additional tip!
If you want to render one of two JSX elements based on a condition, you can just use a ternary expression!
{this.state.game === 'Cricket' ? (
// Renders when game mode is 'Cricket'
<Cricket />
) : (
// Renders when game mode is NOT 'Cricket'
<SomeOtherGame />
)}
So in this case you need a generic and simple solution, so in your state since you have only limited number of games you can have a state variable as shown below.
state = {selectBoxEnabled : {xo1: ['xo1variant', 'inCondition', 'outCondition'], cricket: ['variant'], 'split-score': ['split-score_select']} }
So the above selectBoxEnabled field has the same key as in your dropdown field
Game Select Box : xo1, cricket, split-score has option values
So when the user choose an option you will be getting the key as xo1 Assume
Now you need to find the fields of xo1 game
const findFields = (currentGameKey) => {
let selectBoxKeys = selectBoxEnabled[Object.keys(selectBoxEnabled).find(key => key === currentGameKey)]
this.setState(selectBoxKeys)
}
Now you know which are the fields to be shown since you are using same component SelectInput , you can do a simple array.map and render the fields from the config.
I hope this will give a better understanding of the problem
I was doing my project and in my project, and I faced such a situation
enter image description here
if a user selects one of the there options as an option
if the option is one of the three except they select the form lookalike this
enter image description here
else if they select DDP the form will be changed by adding a new input field
enter image description here
I handle it using a ternary operator based on the input user selected and it works for me
if it's helpful the code snippet will be here
{
ddp !== undefined ? (
incotermData == ddp ? (
<Controls.Input
name="exchangeRate"
label="Exchange Rate"
onChange={(e) => setExchangeRate(e.target.value)}
/>
) : (
<></>
)
) : (
<></>
);
}
I'm trying to send list of selected radio button ids from multiple radio button groups on clicking send button,
My problem:
I am getting selected radio button from backend , then I should be able to change the radio button and send back to backend. but when I try to change the radio button it is not working.
What I did not understand:
How to handle the on change function, normally on change we can change the state but to change the state on load we should grab the values radio buttons. Finally I got struck here, not understanding how to move forward.
Here is the wireframe and code snippet:
function CardsList(props) {
const cards = props.cards;
return (
<div>
{cards.map((card, idx) => (
<div>
{card.cardName}
{
card.options.map((lo,idx) => (
<li key={idx}>
<input
className="default"
type="radio"
name={card.cardName}
checked={lo.selected}
/>))
}
<div>
))}
</div>
);
}
//array of cards coming from the backend
const cards = [
{cardName:'card1',options:[{radioName:'card1-radio1',selected:'true'},
{radioName:'card1-radio2',selected:'false'}]},
{cardName:'card2',options:[{radioName:'card2-radio1',selected:'true'},
{radioName:'card2-radio2',selected:'false'}]}
];
ReactDOM.render(
<CardsList cards={cards} />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You can use an object as a lookup table that holds the group names as keys.
On each change you will need to find the relevant group with the relevant option and set the new state accordingly.
Important! - one thing to notice here, is that i changed the type of the selected property from a String to a Boolean. this will let me handle the conditions like this:
<input checked={option.selected} />
If you can't change it to a Boolean then you will need to handle the condition like this:
<input checked={option.selected === 'true'} />
Here is a running example:
//array of cards coming from the backend
const data = [
{
cardName: 'card1', options: [{ radioName: 'card1-radio1', selected: true },
{ radioName: 'card1-radio2', selected: false }]
},
{
cardName: 'card2', options: [{ radioName: 'card2-radio1', selected: true },
{ radioName: 'card2-radio2', selected: false }]
}
];
class CardsList extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: []
};
}
componentDidMount() {
setTimeout(() => {
// mimic an async server call
this.setState({ cards: data });
}, 1000);
}
onInputChange = ({ target }) => {
const { cards } = this.state;
const nexState = cards.map(card => {
if (card.cardName !== target.name) return card;
return {
...card,
options: card.options.map(opt => {
const checked = opt.radioName === target.value;
return {
...opt,
selected: checked
}
})
}
});
this.setState({ cards: nexState })
}
onSubmit = () => { console.log(this.state.cards) };
render() {
const { cards } = this.state;
return (
<div>
{
cards.length < 1 ? "Loading..." :
<div>
{cards.map((card, idx) => (
<ul>
{card.cardName}
{
card.options.map((lo, idx) => {
return <input
key={idx}
type="radio"
name={card.cardName}
value={lo.radioName}
checked={!!lo.selected}
onChange={this.onInputChange}
/>
})
}
</ul>
))
}
< button onClick={this.onSubmit}>Print Cards</button>
</div>
}
</div>
);
}
}
ReactDOM.render(<CardsList />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
The reason why you can't change them is because of their current checked state which you are setting here:
<input
className="default"
type="radio"
name={card.cardName}
checked={lo.selected}
/>
An approach I have used for this exact scenario is storing the component's state (from the server) in my component's state (this.state), passing the state to the element: checked={this.state.isChecked}, and updating the element's state onClick.
Example:
class CardsList extends Component {
constructor(props){
super(props);
this.state = {isChecked: false};
this.inputOnClick = this.inputOnClick.bind(this);
}
//fetch data from server
fetchData(){
fetch('/api')
.then(res => res.json())
//this will be our initial state
.then(res => this.setState(res))
}
componentDidMount(){
this.fetchData();
}
//change radio button state on click
inputOnClick(e){
e.preventDefault();
//invert state value
this.setState((prevState, props) => {isChecked: !prevState.isChecked});
}
render(){
return (
<input
type="radio"
checked={this.state.isChecked}
onClick={this.inputOnClick}
/>
)
}
}
this answer may work with single radio button group , but i am facing
problem with multiple radio buttons with in multiple radio button
groups.if you see the array of cards , how does it know which radio
button group it belongs to.
We can modify the state based on the radio button's name.
Let's save all of your cards in your component's state. I know the cards are retrieved from the server and will be saved using setState but I am writing it like this for visual purposes.
this.state = {cards: [
{ cardName:'card1',
options:[
{radioName:'card1-radio1',selected:true},
{radioName:'card1-radio2',selected:false}
]
},
{ cardName:'card2',
options:[
{radioName:'card2-radio1',selected:true},
{radioName:'card2-radio2',selected:false}
]
}
]}
Now when we click on a radio button, we will use that radio button's name to update the state where it needs to be updated. Since React state needs to be immutable, we will create a deep copy of the state, modify it, and then set the state with it.
inputOnClick(e){
e.preventDefault();
var thisRadioBtn = e.target.name;
//make a deep copy of the state
const stateCopy = JSON.parse(JSON.stringify(this.state.cards));
//go through state copy and update it
stateCopy.forEach(card => {
card.options.forEach(option => {
if(option.radioName === thisRadioBtn){
//invert value
//make sure the values are booleans
option.selected = !option.selected;
}
});
});
//update the components state
this.setState({cards: stateCopy});
}
In June 2022, I'm facing a similar issue with you. My solution is just add tag <form> on both sections with no OnSubmit or action on it.
Next to my redux forms I've a small sidebar with form names to switch between different forms. To keep the app state updated anytime when a user switch between these forms via the sidebar I do a submission of the current form state via the redux-form handleSubmit function. The submission is fired with a dispatch which updates the app state. This works fine but to make the clicked form visible I need to change the 'active' value in my state to the name of the form, this name is set on the list item in the sidebar items loop as a classname.
I tried to receive the clicked element classname via event.target.className but it seems to have no results as event seems not to be the event I expected. How can I access the event in a handleSubmit mission to change the active form with the clicked className of list item of the sidebar?
p.s. I will create a separated function, if / else statement or something else for this saveData function as I know the event.target.className won't be correct if the form would be submitted by the form button itself.
Example code
... some form field code
class ComponentName extends React.Component {
constructor(props) {
super(props);
this.state = {
someName : this.props.someName,
}
this.saveData = this.saveData.bind(this);
}
saveData(values, dispatch) {
let newState = {
SomeName : update(this.state.someName, {
active : {
$set : this.state.active <======= event.target.className ?
},
rows : {
data : {
$set : values.rows
}
}
})
}
this.setState(newState);
dispatch(someActionNAme(newState.someName));
}
render() {
const { handleSubmit, submitting } = this.props;
var items = {}
Object.values(this.state.someName).map(function(item, i) {
if (typeof item == 'object' && item !== 'undefined' && item !== null) {
items[i] = item;
}
})
return (
<div className="someName__form">
<form onSubmit={handleSubmit(this.saveData)}>
<ul className="someName__sidebar">
{ Object.keys(items).map((item, i) => (
<li
key={i}
data-id={i}
onClick={handleSubmit(this.saveData)}
id={items[item].meta.name}
className={items[item].meta.name}
data-sort={items[item].meta.sort}
>
{items[item].meta.label}
{this.state.someName.active == items[item].meta.name &&
<b>
[Active]
</b>
}
</li>
)
)}
</ul>
<FieldArray name="rows" component={renderRows} />
<div>
<button id="button" type="submit" disabled={submitting} onClick={handleSubmit(this.saveData)}>
Save
</button>
</div>
</form>
</div>
)
}
}
const mapStateToProps = (state) => ({
initialValues : state.someName.rows,
someName : state.someName,
});
const mapDispatchToProps = (dispatch) => ({
dispatch,
});
ComponentName = reduxForm({
form : 'formName',
destroyOnUnmount: false
})(ComponentName);
export default connect(mapStateToProps, mapDispatchToProps)(ComponentName);
To help others out, I found myself the solution by diving deep into the github issue articles of redux-form.
It's easy and possible to add some additional data to the form values via the handleSubmit function. By adding data as below, the additional data will be merged into the object with the values as follow.
...onClick={handleSubmit(values => this.saveData({...values, active: 'componentName' }))}
Values object will now contain an extra key value pair:
{
"languages": [
{
"name": "Spanish",
"level": "3"
}
],
"active": "componentName"
}