ReactJS: How to set state of an object selected via radio button? - reactjs

In my UsedComponents component I have a list of mapped radio buttons that each return componentType as value.
this.props.usedComponents.map((component, index) => (
<RadioButton
key={index}
id={component.componentType}
icon={ButtonIco}
label={component.componentName}
name="select"
value={component.componentType}
inputClass={classes.RadioInput}
labelClass={classes.RadioLabel}
selected={component.selected}
handleChange={this.props.selectComponent}
/>
))
My state looks like this:
constructor(props) {
super(props);
this.state = {
components: components,
usedComponents: [components[0], components[2], components[3]],
};
this.selectComponentHandler = this.selectComponentHandler.bind(this);
}
components is an imported array of objects that each look something like this:
{
componentType: "headerLogoNavigation",
componentName: "Header 02",
padding: "small",
fontSize: "small",
fontColor: "#1f1f1f",
fontFamily: "Sans-serif",
backgroundColor: "#ffffff",
image: placeholderLogo,
selected: false,
isEditing: false,
margins: false,
roundCorners: "none",
mobile: "false"
}
In my Page component I'm trying to pass a selectComponentHandler prop to my UsedComponents component that should select a component based on a value of a selected radio button and set its state to selected: true. For an added bonus it should set the state of any previously selected component to selected: false.So far I managed to figure out how to select the component but I'm not able to update its state. My final attempt to create this handler before I gave up looks like this:
selectComponentHandler = event => {
this.setState(prevState => {
let selected = prevState.usedComponents.filter(item => item.componentType === event.target.value);
selected.selected = 'true';
return { selected };
});
};
and it's an attempt to filter the prevState inside the setState for the componentType that matches event.target.value of the radio button and set it's state, but I messed up the logic or the syntax and my head is about to explode so I can't figure out what I did wrong.
Can someone help me figure this out?

I figured it out. It's a bit hacky but it works.
selectComponentHandler = event => {
const value = event.target.value;
this.setState(prevState => {
let selected = prevState.usedComponents.filter(item => item.componentType === value).shift();
let unSelected = prevState.usedComponents.filter(item => item.selected === true).shift();
if(unSelected) {
unSelected.selected = false;
}
selected.selected = true;
return { unSelected, selected };
});
};

Related

How to set value for the checkbox in ReactJS

I try to show my value using checkbox. Value always comes for the console log. But it didn't set for the checkbox. Here is the code and image for my problem:
var NotePage = createClass({
addTags(e) {
console.log("id****************", e.target.id);
let id = e.target.id;
let selectedTags = this.state.selectedTags;
if (selectedTags.includes(id)) {
var index = selectedTags.indexOf(id)
selectedTags.splice(index, 1);
} else {
selectedTags.push(id);
}
console.log("id****************selectedTags", selectedTags);
this.setState({
selectedTags: selectedTags
})
},
render: function () {
assignStates: function (note, token, tagCategories) {
let fields = [];
fields["title"] = note.title_en;
fields["body"] = note.body_en;
let selectedFileName = null
if (note.file_url_en != "") {
console.log("note.file_url_en ", note.file_url_en);
selectedFileName = note.file_url_en
}
let selectedTags = [];
let n = 0;
(note.note_tag).forEach(tag => {
selectedTags.push(tag.id.toString());
n++;
});
console.log("id****************first", selectedTags);
let initial_values = {
note: note,
id: note.id,
api: new Api(token),
message: "",
title: note.title_en,
body: note.body_en,
fields: fields,
isEdit: false,
selectedTags: selectedTags,
tagCategories: tagCategories,
selectedFileName: selectedFileName,
}
return initial_values;
},
const { selectedTags } = this.state;
{(tagCategory.tags).map((tag) => (
<div className="col-3">
<div>
<input
type="checkbox"
value={selectedTags.includes(tag.id)}
id={tag.id}
onChange={this.addTags} />
<label style={{ marginLeft: "10px", fontSize: "15px" }}>
{tag.name_en}
</label>
</div>
</div>
))
}
})
Image related for the problem
You've an issue with state mutation. You save a reference to the current state, mutate it, and then save it back into state. This breaks React's use of shallow reference equality checks during reconciliation to determine what needs to be flushed to the DOM.
addTags(e) {
let id = e.target.id;
let selectedTags = this.state.selectedTags; // reference to state
if (selectedTags.includes(id)) {
var index = selectedTags.indexOf(id)
selectedTags.splice(index, 1); // mutation!!
} else {
selectedTags.push(id); // mutation!!
}
this.setState({
selectedTags: selectedTags // same reference as previous state
});
},
To remedy you necessarily return a new array object reference.
addTags(e) {
const { id } = e.target;
this.setState(prevState => {
if (prevState.selectedTags.includes(id)) {
return {
selectedTags: prevState.selectedTags.filter(el => el !== id),
};
} else {
return {
selectedTags: prevState.selectedTags.concat(id),
};
}
});
},
Use the "checked" attribute.
<input
type="checkbox"
value={tag.id}
checked={selectedTags.includes(tag.id)}
id={tag.id}
onChange={this.addTags} />
also, about the value attribute in checkboxes:
A DOMString representing the value of the checkbox. This is not displayed on the client-side, but on the server this is the value given to the data submitted with the checkbox's name.
Note: If a checkbox is unchecked when its form is submitted, there is
no value submitted to the server to represent its unchecked state
(e.g. value=unchecked); the value is not submitted to the server at
all. If you wanted to submit a default value for the checkbox when it
is unchecked, you could include an inside the
form with the same name and value, generated by JavaScript perhaps.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#value
I think you should use checked property instead of value.
For reference check react js docs here
You are mutating state variable directly with selectedTags.splice(index, 1); and selectedTags.push(id);
What you need to do is make a copy of the state variable and change that:
addTags(e) {
let id = e.target.id;
if (this.state.selectedTags.includes(id)) {
this.setState(state => (
{...state, selectedTags: state.selectedTags.filter(tag => tag !== id)}
))
} else {
this.setState(state => (
{...state, selectedTags: [...state.selectedTags, id]}
))
}
}

React checkboxes. State is late when toggling the checkboxes

I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.

How to update nested(object of object value) state in react

I have state like this -
state = {
modal: {
status: {
visible: true
}
}
};
and i tried this
handleShow = e => {
let newState = this.state.modal;
newState.status.visible = false;
this.setState({ modal: newState });
};
First time it makes true->false but i want toggle like(true to false and false to true) when i click.
Here is complete Sample code
class App extends React.Component {
state = {
modal: { status: { visible: true } }
};
handleShow = e => {
let newState = this.state.modal;
newState.status.visible = false;
this.setState({ modal: newState });
};
render() {
return (
<>
<div>
{this.state.modal.status.visible ? (
<div style={showStyle}>"it's showing"</div>
) : (
"It's Hidden"
)}
</div>
<button onClick={this.handleShow}>
{this.state.modal.status.visible ? "Hide" : "Show"}
</button>
</>
);
}
}
const showStyle={
backgroundColor: 'coral',
height: '100px'
}
Can anybody help me with this?.
Thanks.
there is a few things going on here. You are technically setting a reference to the state object when you do let newState = this.state.modal;, so you're not actually creating "new" state. then when you set to false you're just setting the original state object to false and it never toggles back as there is no re render/logic to make it re render and change the state back to false. The most straight forward approach you could try is
handleShow = e => {
this.setState({ modal: { status: { visible: !this.state.modal.status.visible } } });
};
as every time you click it you will be setting the boolean to whatever it was not the last time, essentially doing the toggle I think you're looking for. Look into state and how it is immutable. You should be making a copy of the state and returning a new object technically
// if you were to go more towards the pattern you had going already
handleShow = e => {
let newState = { ...this.state, modal: { status: { visible: this.state.modal.status.visible } } };
this.setState(newState);
}
this spread operator is essentially spreading all the properties from the original state into a new object, then you change the value you want on the newState, then you set the state to that new object which contains all the other parts of the state not modified as well as what you want modified.
there is also something to be said about modifying state the relies on previous state and react setState method being asynchronous. the more solid approach to set state using previous state if you're not using spread would be to pass a callback function to setState instead of an object with the current state as a variable
this.setState(currentState => {
return { modal: { status: { visible: !currentState.modal.status.visible } } }
});
Instead of this newState.status.visible = false; you can try newState.status.visible = !newState.status.visible;
Write newState.status.visible = !newState.status.visible; instead to make it false
handleShow = e => {
let newState = this.state.modal;
newState.status.visible = !newState.status.visible;
this.setState({ modal: newState });
};
It is a bad idea to mutate state.
The best practice is to create a new copy of the object and update it instead and then update the state.
The ... spread operator creates shallow copy of the object. So you would need to spread the properties so that we are keeping it untouched ( not a big deal here as each nesting has only one property, but would break when there is more than 1 property )
handleShow = e => {
const {
modal
} = this.state;
const updatedState = {
...modal,
status: {
...modal.status,
status: {
visible: !modal.status.visible
}
}
}
this.setState(updatedState);
};
Changing State should be coming only from setState function.
by doing const x = obj;
you are making x === obj (both point to the same pointer). therefore changing x will change obj aswell.
you should make a copy of the state you want to change first, and then assign the changed copy to the state.
With ES6 you can copy an object with the spread (...)
Good Luck!

Interupt code and wait for user interaction in a loop - React

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

Why is my React show/hide label not updating correctly?

The label is editable: When click on the label, input text field will be shown and label field is hidden. After the text field has lost focus, the label field will be shown and text field will be hidden. I am having issue where label does not update with the new text input value.
The add component button will create a new component and place it on top of the list. Having issue where the newly created component is place below the list which has input text shown and label hidden.
After added multiple new components, when I click on one of the label, the text field is automatically updated with other text. I have tried to debug it but cannot resolve it.
import React from 'react';
import FontAwesome from 'react-fontawesome';
export default class Dynamic extends React.Component {
constructor() {
super();
this.state = {
arr: [],
text:"LABEL",
saveDisabled: true,
editing: []
};
}
handleSort(sortedArray) {
this.setState({
arr: sortedArray
});
}
save(){
}
closePopup() {
}
handleAddElement() {
this.textInput.value : 'LABEL';
this.state.arr.unshift('LABEL');
this.setState({
saveDisabled: false,
});
}
handleRemoveElement(index) {
const newArr = this.state.arr.slice();
newArr.splice(index, 1);
this.setState({
arr: newArr,
saveDisabled: false
});
}
changeLabel(index){
this.setState({
saveDisabled: false
});
console.log(index);
this.state.editing[index] = true;
console.log("changelabel");
}
textChanged(index) {
console.log("txtval: "+this.textInput.value);
this.setState({ text: this.textInput.value});
this.state.arr[index] = this.textInput.value;
this.setState({
arr: arr
});
console.log(this.state.arr);
}
inputLostFocus(index) {
this.state.editing[index] = false;
}
keyPressed(event) {
if(event.key == 'Enter') {
this.inputLostFocus();
}
this.inputLostFocus();
console.log("key");
}
render() {
function renderItem(num, index) {
return (
<DemoItem className="dynamic-item" >
<FontAwesome className='th' name=' th' onClick={this.handleRemoveElement.bind(this, index)}/>
<div name="name" className={(index==0)||this.state.editing[index] ? "hideElement": "displayElement"} onClick={this.changeLabel.bind(this,index)}>{this.state.arr[index]}</div>
<input autofocus name="name" type="text" className={(index==0)||this.state.editing[index] ? "displayElement": "hideElement"} onChange={this.textChanged.bind(this, index)} onBlur={this.inputLostFocus.bind(this,index)}
onKeyPress={this.keyPressed.bind(this,index)} defaultValue={this.state.arr[index]} ref={(input) => {this.textInput = input;}} />
<FontAwesome className='trash-o' name='trash-o' onClick={this.handleRemoveElement.bind(this, index)}/>
</DemoItem>
)
}
return (
<div className="demo-container">
<div className="dynamic-demo">
<h2 className="demo-title">
Tasks
<button disabled={this.state.saveDisabled} onClick={::this.save}>Save</button>
<button onClick={::this.handleAddElement}>Add Component</button>
</h2>
<Sortable className="vertical-container" direction="vertical" dynamic>
{this.state.arr.map(renderItem, this)}
</Sortable>
</div>
</div>
);
}
}
displayElement {
display: inline;
}
.hideElement{
display: none;
}
It looks like your bug is in your textChanged function, try this instead:
textChanged(index) {
console.log("txtval: " + this.textInput.value);
// this.state.arr[index] = this.textInput.value; <= bug
const newArray = [...this.state.arr];
newArray[index] = this.textInput.value;
this.setState({
arr: newArray,
text: this.textInput.value
});
// console.log(this.state.arr); <= don't check here, check in your render method
}
Two changes:
Modify the state via this.setState, not via this.state.arr.
Setting state in one this.setState action for cleaner code.
Commenting out console log of this.state since the state hasn't fully updated yet until the next life cycle. Instead, console log the state in your render method.

Resources