Trying to create a controlled switcher button - reactjs

I've picked up this code online (it's high up in the google results) :
https://codepen.io/BuiltByEdgar/pen/jWOVYQ
but I've grown annoyed with it's code.
I'm trying to have a duo of these buttons where default state is on and having both off is not allowed. The UI behavior I've chosen for that is to simply re-enable the other if it's sibling is being turned off :
onONEClick() {
let {
TWO,
ONE,
} = this.state;
if (TWO) {
ONE = !ONE;
} else if (ONE) {
TWO = !TWO;
ONE = !TWO;
} else {
ONE = !ONE;
}
this.setState({
ONE,
TWO,
});
this.props.callBack({
ONE,
TWO,
});
}
onTWOClick() {
let {
TWO,
ONE,
} = this.state;
if (ONE) {
TWO = !TWO;
} else if (TWO) {
TWO = !TWO;
ONE = !ONE;
} else {
TWO = !TWO;
}
this.setState({
ONE,
TWO,
});
this.props.callBack({
ONE,
TWO,
});
}
and the render:
<div>
<div className="switch-container">
<label>
<input
onChange={this.onONEClick}
type="checkbox"
className="switch"
value={this.state.ONE}
checked={this.state.ONE}
/>
<div>
<span><g className="icon icon-toolbar grid-view" /></span>
<span><g className="icon icon-toolbar ticket-view" /></span>
<div />
</div>
</label>
</div>
ONE
</div>
<div>
<div className="switch-container">
<label>
<input
onChange={this.onTWOClick}
type="checkbox"
className="switch"
value={this.state.TWO}
checked={this.state.TWO}
/>
<div>
<span><g className="icon icon-toolbar grid-view" /></span>
<span><g className="icon icon-toolbar ticket-view" /></span>
<div />
</div>
</label>
</div>
TWO
</div>
simple enough right?
problem is react hates that my switcher is an input of type "checkbox" :
and as you can see I've done my best to avoid this happening but I think it simply isn't in the cards to have an input type and change it's checked state other than by a human.
So I'm thinking maybe I can solve the issue by reconstructing the checkbox with just divs. (the label tag is also in violation of ESLint)
ANSWERED BY JLAITIO :
and here's what I added (top part of the file with props declaration and constructor) :
const propTypes = {
value: React.PropTypes.object,
type: React.PropTypes.string,
callBack: React.PropTypes.func,
ONE: React.PropTypes.bool,
TWO: React.PropTypes.bool,
};
const defaultProps = {
callBack: () => {},
onChange: () => {
},
value: { type: '' },
ONE: true,
TWO: true,
};
class ProviderInfosComponent extends Component {
constructor(props) {
super(props);
const initialValue = props.value;
this.state = {
...initialValue,
ONE: this.props.ONE,
TWO: this.props.TWO,
};
this.onONEClick = this.onONEClick.bind(this);
this.onTWOClick = this.onTWOClick.bind(this);
}

You get this error, if at any point your value attribute is not set, or set to a value that is undefined, and then subsequently it is an existing React state value.
My best guess would be that your constructor does not initialize ONE and TWO, hence the value being undefined to start with. Then when you actually have a value, the component transfers from being an uncontrolled (="component state independent of React") to controlled(="component state tied directly to React state").

As an addition to #jlaitio's answer,
You don't have to set a value for a checkbox input. You can just use checked prop of the component. Also a small improvement for your changed functions might be useful to follow up the values easier.
constructor(props) {
super(props);
this.state = {
ONE: true,
TWO: true
};
}
onONEChange = (event) => {
this.setState((prevState) => {
ONE: event.target.checked,
TWO: (event.target.checked === false ? true : prevState.TWO) // change state of TWO if ONE is false else just leave it as is
}, () => {
this.props.callBack({ ONE: this.state.ONE, TWO: this.state.TWO });
});
}
onTWOChange = (event) => {
this.setState((prevState) => {
ONE: (event.target.checked === false ? true : prevState.ONE), // change state of ONE if TWO is false else just leave it as is
TWO: event.target.checked
}, () => {
this.props.callBack({ ONE: this.state.ONE, TWO: this.state.TWO });
});
}

Related

Update object from input without setState

I'm new to React JS (and JS, in general). Now I'm trying to code a simple task tracker.
So, I have all tasks in state element of MyTodoList class. There I draw each task separately with Task constant.
I want to implement adding a new task with 2 inputs: name and description.
I do it in MyTodoList with creating a new object (newTask), so that I can add it to state list later. However, I guess that I'm writing onChange method for input incorrectly. newTask seems to be updating inside the function (logged it in console), but it does not change outside (in input space there are no changes with typing). Obviously I cannot use setState as I want to update a non-state object (object is mutable, so I do not understand why it won't change).
I'm not sure whether I'm updating the object wrongly or whether my whole concept of adding new task is wrong. Would be grateful if you could explain me my mistakes.
Here's the code:
const TaskAdd = ({value, onChange, placeholder, name}) => {
return (
<input value={value} onChange={onChange} placeholder={placeholder} name={name}/>
)
}
const Task = ({id, name, description, completed}) => {
const handleClick = () => {
}
return (
<div className='task'>
<h3>{name}</h3>
<div>{description}</div>
<div>{completed}</div>
<button onClick={handleClick} className='button1'>CLICK</button>
</div>
)
}
class MyTodoList extends React.Component {
state = {
tasks: [
{
id: 1,
name: 'Walk the dog',
description: 'Have to walk the dog today',
completed: false,
},
]
}
maxId = this.state.tasks[this.state.tasks.length - 1].id;
newTask = {
id: this.maxId,
name: '',
description: '',
completed: false,
}
handleChange = (event) => {
const {value, name} = event.currentTarget
this.newTask[name] = this.newTask[name] + value
}
render () {
return(
<div>
<header><h1>TO-DO</h1></header>
<div className='addTask'>
<h2>Let's add something new</h2>
<TaskAdd value={this.newTask.name} onChange={this.handleChange}
placeholder='Name' name='name'/>
<TaskAdd value={this.newTask.description} onChange={this.handleChange}
placeholder='Description' name='description'/>
<p> {this.newTask.name}</p>
<button className='button1'><h3>Add</h3></button>
</div>
<div>{this.state.tasks.map(task => <Task id={task.id} name={task.name}
description={task.description} completed={task.completed}/>)}
</div>
</div>
)
}
}
const App = () => {
return (
<MyTodoList />
)
}
export default App;
Obviously I cannot use setState as I want to update a non-state object
If you want the screen to update you have to use state. The setState function is the only* way to tell react that something change and it needs to rerender.
So, expand your state to have new task in it:
state = {
tasks: [
{
id: 1,
name: 'Walk the dog',
description: 'Have to walk the dog today',
completed: false,
},
]
newTask: {
id: 2,
name: '',
description: '',
completed: false,
}
}
With that you'll need to update your render function to access it in state, as in:
<TaskAdd
value={this.state.newTask.name}
onChange={this.handleChange}
placeholder='Name'
name='name'
/>
And then when you set state, make a copy instead of mutating:
handleChange = (event) => {
const {value, name} = event.currentTarget
this.setState({
newTask: {
...this.state.newTask,
[name]: this.state.newTask[name] + value
}
});
}
Your code didn't include an implementation for the add button, but when you do, you'll probably take this.state.newTask and add it to the end of this.state.tasks (you'll make a copy of the array, not mutate it), and then create a new object to replace this.state.newTask
*ok, technically there's forceUpdate, but don't use that.

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.

In ReactJS how do I keep track of the state of a group of checkboxes?

I am trying to keep track of which boxes are checked in my local state(you can check multiple boxes). I want to be able to check and uncheck the boxes and keep track of the ids of the boxes that are checked. I will do something with the values later. This is what I have so far:
import React, { Component } from 'react'
import './App.css'
import CheckBox from './CheckBox'
class App extends Component {
constructor(props) {
super(props)
this.state = {
fruits: [
{id: 1, value: "banana", isChecked: false},
{id: 2, value: "apple", isChecked: false},
{id: 3, value: "mango", isChecked: false},
{id: 4, value: "grape", isChecked: false}
],
fruitIds: []
}
}
handleCheckChildElement = (e) => {
const index = this.state.fruits.findIndex((fruit) => fruit.value === e.target.value),
fruits = [...this.state.fruits],
checkedOrNot = e.target.checked === true ? true : false;
fruits[index] = {id: fruits[index].id, value: fruits[index].value, isChecked: checkedOrNot};
this.setState({fruits});
this.updateCheckedIds(e);
}
updateCheckedIds = (e) => {
const fruitIds = [...this.state.fruitIds],
updatedFruitIds= fruitIds.concat(e.target.id);
this.setState({updatedFruitIds});
}
render() {
const { fruits } = this.state;
if (!fruits) return;
const fruitOptions = fruits.map((fruit, index) => {
return (
<CheckBox key={index}
handleCheckChildElement={this.handleCheckChildElement}
isChecked={fruit.isChecked}
id={fruit.id}
value={fruit.value}
/>
);
})
return (
<div className="App">
<h1>Choose one or more fruits</h1>
<ul>
{ fruitOptions }
</ul>
</div>
);
}
}
export default App
So basically I am able to check and uncheck the boxes, but I cannot seem to update and store the fruitIds. Here is my checkbox component also:
import React from 'react'
export const CheckBox = props => {
return (
<li>
<input key={props.id}
onChange={props.handleCheckChildElement}
type="checkbox"
id={props.id}
checked={props.isChecked}
value={props.value}
/>
{props.value}
</li>
)
}
export default CheckBox
Also if you have a cleaner ways to do this than the way I am doing it, I would love to see it.
This is what if I were to approach it I will do. I will create a one dimensional array that holds the id's of the fruits when A fruit if clicked(checked) I will add it id to the array and when its clicked the second time I check if the array already has the id I remove it. then the presence of id in the array will mean the fruit is checked otherwise its not checked So I will do something like below
this.state={
fruitsIds: []
}
handleCheckChildElement=(id) => {
//the logic here is to remove the id if its already exist else add it. and set it back to state
const fruitsIds = this.state.fruitsIds;
this.setState({fruitsIds: fruitsIds.contains(id) ? fruitsIds.filter(i => i != id) : [...fruitsIds, id] })
}
then I render the checkboxes like
<CheckBox key={index}
handleCheckChildElement={this.handleCheckChildElement}
isChecked = { this.state.fruitsIds.contains(fruit.id)}
id={fruit.id}
/>
This is because you can always use the id to get all the other properties of the fruit so there is absolutely no need storing them again.
then the checkbox component should be as follows
export const CheckBox = props => {
return (
<li>
<input key={props.id}
onChange={() => props.handleCheckChildElement(props.id)}
type="checkbox"
id={props.id}
checked={props.isChecked}
value={props.value}
/>
{props.value}
</li>
)
}
The reason you are not getting your ids updated because:
You are trying to concat a non array element to an array.
concat is used for joining two or more arrays.
updatedFruitIds = fruitIds.concat(e.target.id);
You are not updating your actual fruitIds state field. I dont know why you are using "updatedFruitIds" this variable but due to above error it will always result into a single element array.
this.setState({ updatedFruitIds });
updateCheckedIds = e => {
const fruitIds = [...this.state.fruitIds],
updatedFruitIds = fruitIds.concat([e.target.id]);
this.setState({ fruitIds: updatedFruitIds });
};
OR
updateCheckedIds = e => {
const fruitIds = [...this.state.fruitIds, e.target.id],
this.setState({ fruitIds });
};

Multiple dropdowns without repeating code in ReactJS

I have created dropdown onMouseOver with help of state. So far its working good enough. Because i don't have much knowledge about ReactJS i'm not sure is it possible to make multiple dropdowns with this or different method without writing all code over and over again.
Here is my code:
..........
constructor(props) {
super(props);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.state = {
isHovering: false
}
}
handleMouseOver = e => {
e.preventDefault();
this.setState({ isHovering: true });
};
handleMouseLeave = e => {
e.preventDefault();
this.setState({ isHovering: false })
};
............
<ul className="menu">
<li onMouseOver={this.handleMouseOver} onMouseLeave={this.handleMouseLeave}>Categories
{this.state.isHovering?(
<ul className="dropdown">
<li>Computerss & Office</li>
<li>Electronics</li>
</ul>
):null}
</li>
</ul>
............
So if I want to add one more dropdown I need to make new state and 2 more lines in constructor() and 2 functions to handle MouseOver/Leave.So repeating amount would be about this:
constructor(props) {
super(props);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.state = {
isHovering: false
}
}
handleMouseOver = e => {
e.preventDefault();
this.setState({ isHovering: true });
};
handleMouseLeave = e => {
e.preventDefault();
this.setState({ isHovering: false })
};
I will have maybe 10+ dropdowns and at the end will be load of codes. So is there any possibility to not repeat code ? Thank You!
You should use your event.target to achieve what you want. With this, you'll know which dropdown you're hovering and apply any logic you need. You can check for example if the dropdown you're hovering is the category dropdown like this:
if(e.target.className === "class name of your element")
this.setState({hoveredEl: e.target.className})
then you use it this state in your code to show/hide the element you want.
you can check an example on this fiddle I've created: https://jsfiddle.net/n5u2wwjg/153708/
I don't think you're going to need the onMouseLeave event, but if you need you can follow the logic I've applied to onMouseOver
Hope it helps.
1. You need to save the state of each <li> item in an array/object to keep a track of hover states.
constructor(props) {
super(props);
...
this.state = {
hoverStates: {} // or an array
};
}
2. And set the state of each item in the event handlers.
handleMouseOver = e => {
this.setState({
hoverStates: {
[e.target.id]: true
}
});
};
handleMouseLeave = e => {
this.setState({
hoverStates: {
[e.target.id]: false
}
});
};
3. You need to set the id (name doesn't work for <li>) in a list of menu items.
Also make sure to add key so that React doesn't give you a warning.
render() {
const { hoverStates } = this.state;
const menuItems = [0, 1, 2, 3].map(id => (
<li
key={id}
id={id}
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
className={hoverStates[id] ? "hovering" : ""}
>
Categories
{hoverStates[id] ? (
<ul className="dropdown menu">
<li>#{id} Computerss & Office</li>
<li>#{id} Electronics</li>
</ul>
) : null}
</li>
));
return <ul className="menu">{menuItems}</ul>;
}
4. The result would look like this.
You can see the working demo here.
Shameless Plug
I've written about how to keep a track of each item in my blog, Keeping track of on/off states of React components, which explains more in detail.

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