Handle Input with Same State Value - javascript

I'm building a shopping cart application and I ran into a problem where all my inputs have the same state value. Everything works fine but when I type in one input box, it's the same throughout all my other inputs.
I tried adding a name field to the input and setting my initial state to undefined and that works fine but the numbers don't go through.
How do we handle inputs to be different when they have the same state value? Or is this not possible / dumb to do?
class App extends Component {
state = {
items: {
1: {
id: 1, name: 'Yeezys', price: 300, remaining: 5
},
2: {
id: 2, name: 'Github Sweater', price: 50, remaining: 5
},
3: {
id: 3, name: 'Protein Powder', price: 30, remaining: 5
}
},
itemQuantity: 0
},
render() {
return (
<div>
<h1>Shopping Area</h1>
{Object.values(items).map(item => (
<div key={item.id}>
<h2>{item.name}</h2>
<h2>$ {item.price}</h2>
{item.remaining === 0 ? (
<p style={{ 'color': 'red' }}>Sold Out</p>
) : (
<div>
<p>Remaining: {item.remaining}</p>
<input
type="number"
value={ itemQuantity }
onChange={e => this.setState({ itemQuantity: e.target.value})}
placeholder="quantity"
min={1}
max={5}
/>
<button onClick={() => this.addItem(item)}>Add To Cart</button>
</div>
)}
</div>
))}
</div>
)
}
}

If you are using same state key for all input, All input take value from one place and update to one place. To avoid this you have to use separate state. I suppose you are trying to show input for a list of item.
To achive you can create a component for list item and keep state in list item component. As each component have their own state, state value will not conflict.
Here is an example
class CardItem extends Component {
state = {
number: 0
}
render() {
render (
<input type="text" value={this.state.number} onChange={e => this.setState({ number: e.target.value })} />
)
}
}
class Main extends Component {
render () {
const list = [0,1,2,3,4]
return (
list.map(item => <CardItem data={item} />)
)
}
}

This is a solution which the problem is loosely interpreted, but it does work without having to create another component. As you know, you needed to separate the state of each items in the cart. I did this by dynamically initializing and setting the quantity states of each item. You can see the state changes with this example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = { quantities: {} }
}
componentDidMount() {
let itemIDs = ['1', '2', '3', 'XX']; //use your own list of items
itemIDs.forEach(id => {
this.setState({quantities: Object.assign(this.state.quantities, {[id]: 0})});
})
}
render() {
let list = Object.keys(this.state.quantities).map(id => {
return (
<div>
<label for={id}>Item {id}</label>
<input
id={id}
key={id}
type="number"
value={this.state.quantities[id]}
onChange={e => {
this.setState({quantities: Object.assign(this.state.quantities, {[id]: e.target.value})})
}}
/>
</div>
);
})
return (
<div>
{list}
<div>STATE: {JSON.stringify(this.state)}</div>
</div>
);
}
}
ReactDOM.render(<App/>, 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 modify the state structure to your liking.

Here is how I usually handle this scenario. You say that you get an array of items? Each item object should contain a key to store the value (count in my example). You can use a generic onChange handler to update an individual item in the array. So now, your state is managing the list of items instead of each individual input value. This makes your component much more flexible and it will be able to handle any amount of items with no code changes:
const itemData = [
{ id: 0, count: 0, label: 'Number 1' },
{ id: 1, count: 0, label: 'Number 2' },
{ id: 2, count: 0, label: 'Number 3' },
{ id: 3, count: 0, label: 'Number 4' }
];
class App extends React.Component {
state = {
items: itemData
}
handleCountChange = (itemId, e) => {
// Get value from input
const count = e.target.value;
this.setState( prevState => ({
items: prevState.items.map( item => {
// Find matching item by id
if(item.id === itemId) {
// Update item count based on input value
item.count = count;
}
return item;
})
}))
};
renderItems = () => {
// Map through all items and render inputs
return this.state.items.map( item => (
<label key={item.label}>
{item.label}:
<input
type="number"
value={item.count}
onChange={this.handleCountChange.bind(this, item.id)}
/>
</label>
));
};
render() {
return (
<div>
{this.renderItems()}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
label {
display: block;
}
<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't use the same state for the both inputs. Try to use a different state for each one like that:
class App extends Component {
state = {
number: ""
}
render() {
return (
<div>
<input
type="number"
value={this.state.number}
onChange={e => this.setState({ number: e.target.value })}
/>
<input
type="number"
value={this.state.number2}
onChange={e => this.setState({ number2: e.target.value })}
/>
</div>
)
}
}

Related

How to add multiple items to array with reactjs

I have two classes. One holds the array, the other holds the array props. These are my classes:
//PARENT CLASS:
constructor() {
super()
this.state = {
items: []
}
this.addItem = this.addItem.bind(this)
}
componentDidMount(){
this.setState({
items: [{
name: 'Sebastian',
num: '001'
},{
name: 'Josh',
num: '002'
}]
})
}
addItem() {
??????
}
render() {
return(
<div>
<MethodA items={this.state.items} addItem={this.addItem}/>
</div>
)
}
//CHILD CLASS:
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => { props.addItem() }}>ADD ITEM</button>
</div>
)
}
Current result is like this:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
Then after the "ADD ITEM" button was hit, this will be the new result:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
<div>
<span>New Name</span>
<span>New Num</span>
</div>
I'm not sure whether what and how to use between push() or concat() or both. Any ideas?
Firstly, there's no need to set the initial state in componentDidMount, you can do it directly in constructor.
constructor(props) {
super(props);
this.state = {
items: [
{
name: "Sebastian",
num: "001"
},
{
name: "Josh",
num: "002"
}
]
};
this.addItem = this.addItem.bind(this);
}
To add an item you can use functional form of setState and you'll need to pass that item into callback from the child component.
addItem(item) {
this.setState(state => ({
items: [...state.items, item]
}));
}
// Child class
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => props.addItem(item)}>ADD ITEM</button> // Pass item to the parent's method
</div>
)
}
Here's the deal. The difference between push() and concat() is in immutability.
If you use push on an array, it will mutate the original array and add a new value to that array (wrong).
If you use concat, it will create a new array for you, leaving the old array untouched (correct).
So you might want to do something along these lines:
addItem(item)
this.setState(state => {
const items = state.items.concat(item);
return {
items,
};
});
}

React checkbox feature with only single selection

I want to figure out whether my code is wrong or a bug. I think there is no problem, but it does not work...
The code I used is:
https://codepen.io/cadenzah/pen/wvwYLgj?editors=0010
class ItemView extends React.Component {
constructor(props) {
super(props)
this.state = {
options: [{
id: 1,
name: "Item 1"
},
{
id: 2,
name: "Item 2"
}],
optionSelected: 2
}
}
toggleCheckbox(e) {
console.log(e.target.id)
if (this.state.optionSelected === e.target.id) {
this.setState({
optionSelected: undefined
})
} else {
this.setState({ optionSelected: e.target.id })
}
}
render() {
return (
<div className="container">
<div className="row">
<ItemList
options={this.state.options}
optionSelected={this.state.optionSelected}
toggleCheckbox={(e) => this.toggleCheckbox(e)} />
</div>
</div>
)
}
}
const ItemList = ({ options, optionSelected, toggleCheckbox }) => {
return (
<div className="col s12">
{
options.map((option, index) => (
<Item
key={index}
option={option}
checked={(optionSelected === (index + 1) ? true : false)}
toggleCheckbox={toggleCheckbox} />
))
}
</div>
)
}
const Item = ({ option, checked, toggleCheckbox }) => {
return (
<div className="card">
<div className="card-content">
<p><label htmlFor={option.id}>
<input
className="filled-in"
type="checkbox"
id={option.id}
onChange={toggleCheckbox}
checked={(checked ? "checked" : "")} />
<span>{option.id}. {option.name}</span>
</label></p>
</div>
</div>
)
}
Code explaination:
React code, with materialize-css used.
It is a simple checkbox feature with multiple items, restricted to select only one item. So, if I check one of them, every item except for what I just selected will be unchecked automatically. If I uncheck what I just checked, every item will stay unchecked.
The core logic is: in <ItemList /> component, there is a conditional props that determines whether each item has to be checked or not. It compares the id, and hand in true or false into its children. That checked props is used in <Item /> component to set the checked attribute of <input>.
Strange thing is, as I set default choice in the initial state, when I just run the application, the check feature works as I expected. But if I click one of them, it does not work.
What is the problem of it?
You can check if the selected option is the checked one like this:
checked={optionSelected === option.id}
And then you simply get it into your input like this:
<input checked={checked} />
Also, make sure to change your state ids into strings (the DOM element id is of type string):
options: [{
id: '1',
name: "Item 1"
},
{
id: '2',
name: "Item 2"
}],
optionSelected: '2'
https://codepen.io/AndrewRed/pen/gOYBVPZ?editors=0010
class ItemView extends React.Component {
constructor(props) {
super(props)
this.state = {
options: [{
id: 1,
name: "Item 1"
},
{
id: 2,
name: "Item 2"
}],
optionSelected: 2
}
}
toggleCheckbox(e) {
this.setState({
optionSelected : e.target.id
})
}
render() {
return (
<div className="container">
<div className="row">
<ItemList
options={this.state.options}
optionSelected={this.state.optionSelected}
toggleCheckbox={(e) => this.toggleCheckbox(e)} />
</div>
</div>
)
}
}
const ItemList = ({ options, optionSelected, toggleCheckbox }) => {
return (
<div className="col s12">
{
options.map((option, index) => (
<Item
key={index}
option={option}
checked={(optionSelected === (index + 1) ? true : false)}
toggleCheckbox={toggleCheckbox}
optionSelected = {optionSelected}
/>
))
}
</div>
)
}
const Item = ({ option, checked, toggleCheckbox,optionSelected }) => {
return (
<div className="card">
<div className="card-content">
<p><label htmlFor={option.id}>
<input
className="filled-in"
type="checkbox"
id={option.id}
onChange={toggleCheckbox}
checked={option.id == optionSelected ? "checked" : ""} />
<span>{option.id}. {option.name}</span>
</label></p>
</div>
</div>
)
}
function tick() {
ReactDOM.render(
<ItemView />,
document.getElementById('root')
);
}
tick()
COPY PASTE AND RUN
e.target.id is a string while index is a number. When you do a === comparison the type is also checked and these are not the same. This results in checked always being false after the initial state (which you set yourself as an int)

Filter state in React without removing data

I'm trying to make a react component that can filter a list based on value chosen from a drop-down box. Since the setState removes all data from the array I can only filter once. How can I filter data and still keep the original state? I want to be able to do more then one search.
Array list:
state = {
tree: [
{
id: '1',
fileType: 'Document',
files: [
{
name: 'test1',
size: '64kb'
},
{
name: 'test2',
size: '94kb'
}
]
}, ..... and so on
I have 2 ways that I'm able to filter the component once with:
filterDoc = (selectedType) => {
//way #1
this.setState({ tree: this.state.tree.filter(item => item.fileType === selectedType) })
//way#2
const myItems = this.state.tree;
const newArray = myItems.filter(item => item.fileType === selectedType)
this.setState({
tree: newArray
})
}
Search component:
class SearchBar extends Component {
change = (e) => {
this.props.filterTree(e.target.value);
}
render() {
return (
<div className="col-sm-12" style={style}>
<input
className="col-sm-8"
type="text"
placeholder="Search..."
style={inputs}
/>
<select
className="col-sm-4"
style={inputs}
onChange={this.change}
>
<option value="All">All</option>
{this.props.docTypes.map((type) =>
<option
value={type.fileType}
key={type.fileType}>{type.fileType}
</option>)}
</select>
</div>
)
}
}
And some images just to get a visual on the problem.
Before filter:
After filter, everything that didn't match was removed from the state:
Do not replace original data
Instead, change what filter is used and do the filtering in the render() function.
In the example below, the original data (called data) is never changed. Only the filter used is changed.
const data = [
{
id: 1,
text: 'one',
},
{
id: 2,
text: 'two',
},
{
id: 3,
text: 'three',
},
]
class Example extends React.Component {
constructor() {
super()
this.state = {
filter: null,
}
}
render() {
const filter = this.state.filter
const dataToShow = filter
? data.filter(d => d.id === filter)
: data
return (
<div>
{dataToShow.map(d => <span key={d.id}> {d.text}, </span>)}
<button
onClick={() =>
this.setState({
filter: 2,
})
}
>
{' '}
Filter{' '}
</button>
</div>
)
}
}
ReactDOM.render(<Example />, document.getElementById('root'))
<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>
<body>
<div id='root' />
</body>
Don't mutate local state to reflect the current state of the filter. That state should reflect the complete available list, which should only change when the list of options changes. Use your filtered array strictly for the view. Something like this should be all you need to change what's presented to the user.
change = (e) => {
return this.state.tree.filter(item => item.fileType === e.target.value)
}

In React, how to bind an input's value when rendering a list of inputs?

I'm rendering a list of inputs and I want to bind each input's value to a link's href. My current attempt renders https://twitter.com/intent/tweet?text=undefined:
class App extends React.Component {
tweets = [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
];
render() {
return (
<div>
{this.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={e => tweet.text = e.target.value} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.text}`}>Tweet</a>
</div>
)}
</div>
);
}
}
This probably needs to involve setState but I have no idea how to achieve that when rendering a list. I've tried to do some research on this but didn't found anything helpful.
JSFiddle: https://jsfiddle.net/nunoarruda/u5c21wj9/3/
Any ideas?
You can move the tweets variable to the state to maintain consistency in that array.
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
tweets: [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
]
};
};
setTweets = index => e => {
const { tweets } = this.state
tweets[index].text = e.target.value
this.setState({ tweets })
}
render() {
const { tweets } = this.state
return (
<div>
{tweets.map((tweet, index) =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={this.setTweets(index)} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.text}`}>Tweet</a>
</div>
)}
</div>
);
}
}
Updated Jsfiddle: https://jsfiddle.net/u5c21wj9/6/
You can reach the desired result using state.
return (
<div>
{tweets.map(({ id, link }) =>
<div key={id}>
<input type="text" placeholder="text" onChange={({ target }) => this.setState({ [id]: target.value })} />
<a href={`https://twitter.com/intent/tweet?text=${this.state[id] || link}`}>Tweet</a>
</div>
)}
</div>
);
Note: I would move tweets outside the component and implement few ES6 features.
Updated Jsfiddle: https://jsfiddle.net/u5c21wj9/7/
You really should use a state here and make your tweets variable be part of it. To do that, add a constructor:
constructor() {
super();
this.state = {
tweets: [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
]
};
}
Then you need to mutate each linkwhenever you type in one of the inputs. There are a few pitfalls here, so let me go through them one-by-one:
changeTweet = (id, e) => {
let arr = this.state.tweets.slice();
let index = arr.findIndex(i => i.id === id);
let obj = Object.assign({}, arr[index]);
obj.link = e.target.value;
arr[index] = obj;
this.setState({tweets: arr});
}
First, you need to create a copy of your state variable. This gives you something to work with, without mutating the state directly which is anti-pattern. This can be done with slice().
Since you are sending in the id of the object to modify, we need to find it in our array (in case the items are unordered). This is done with findIndex(). You might want to handle the scenario in which such index is not found (I have not done that).
Now we know where in the array the object with the given id key is. Now, create a copy of that item (which is an object). This is also to prevent mutating the state directly. Do this with Object.assign().
Now change the link to the input value we typed in. Replace the old item object with the new one (obj) and replace the old tweets array with the new one (arr).
Here's the full example:
class App extends React.Component {
constructor() {
super();
this.state = {
tweets: [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
]
};
}
changeTweet = (id, e) => {
let arr = this.state.tweets.slice();
let index = arr.findIndex(i => i.id === id);
let obj = Object.assign({}, arr[index]);
obj.link = e.target.value;
arr[index] = obj;
this.setState({tweets: arr});
}
render() {
return (
<div>
{this.state.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={(e) => this.changeTweet(tweet.id, e)} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.link}`}>Tweet</a>
</div>
)}
</div>
);
}
}
ReactDOM.render(<App />, 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 need to save the text from the input in the state (using setState), not in the tweets array. Then you can render it getting the text from the state.
class App extends React.Component {
tweets = [
{ id: 1, link: 'example.com' },
{ id: 2, link: 'example2.com' }
];
state = {
tweetsText :{}
}
handleTextChange = (event, tweetId) => {
const tweetsTextCopy = Object.assign({}, this.state.tweetsText)
tweetsTextCopy[tweetId] = event.target.value
this.setState({tweetsText: tweetsTextCopy})
}
render() {
return (
<div>
{this.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={e => this.handleTextChange(e, tweet.id)} />
<a href={`https://twitter.com/intent/tweet?text=${this.state.tweetsText[tweet.id]}`}>Tweet</a>
</div>
)}
</div>
);
}
}
Links info is in the link property of your tweets array. The property text is not defined.
So, your render function should look like this
render() {
return (
<div>
{this.tweets.map(tweet =>
<div key={tweet.id}>
<input type="text" placeholder="text" onChange={e => tweet.text= e.target.value} />
<a href={`https://twitter.com/intent/tweet?text=${tweet.link}`}>Tweet</a>
</div>
)}
</div>
);
}

Count checked checkboxes in React.js

I'm using a document structure like this
render() {
return (
<div className="MyComponent">
<ul className="">
{parseRecommendations(this.props.recommendations)}
</ul>
</div>
);
}
function parseRecomendations(recommendations) {
return recommendations.map((recommendation, key) => {
return (<Recommendation data={recommendation} key={key} />);
});
}
Where each Recommendation is its own component containing a checkbox
class Recommendation extends Component {
const recommendation = this.props.data;
const pageUrl = recommendation.url;
return (
<li className="article-item" key={key}>
<div className="article-info">
<input type="checkbox" defaultChecked="checked" aria-described-by={recommendation.pii} />
<a className="journal-title" href={pageUrl} id={recommendation.pii}>{recommendation.title}</a>
</div>
</li>
);
I'd like to have a title saying [Download (x) PDFs], where x is the number of selected checkboxes. How do I find the value of x in this case?
You need to store information about whether input is "checked" in your data. Then, simply count items with truthy "checked" flag.
Here is my solution. You should be able to get principle here and modify your code.
const data = [
{ checked: false, value: 'document 1' },
{ checked: true, value: 'document 2' },
{ checked: true, value: 'document 3' },
{ checked: false, value: 'document 4' },
{ checked: false, value: 'document 5' },
];
const Item = props => (
<div>
<input type="checkbox" checked={props.checked} onChange={props.onCheckChange} />
{ props.value }
</div>
)
var Hello = React.createClass({
getInitialState() {
return {
items: this.props.items.concat(),
};
},
onCheckChange(idx) {
return () => {
const items = this.state.items.concat();
items[idx].checked = !items[idx].checked;
this.setState({items});
}
},
totalChecked() {
return this.state.items.filter(props => props.checked).length;
},
render() {
return (
<div>
{ this.state.items.map((props, idx) => (
<Item {...props} key={idx} onCheckChange={this.onCheckChange(idx)} />
)) }
Total checked: { this.totalChecked() }
</div>
);
}
});
ReactDOM.render(
<Hello items={data} />,
document.getElementById('container')
);
If you just want to get the number of selected check-boxes you can try this
let checkedBoxes = document.querySelectorAll('input[name=chkBox]:checked');
Then get the total checked boxes via checkedBoxes.length
Edit:
Instead of querying whole document. You can get the nearest possible parent via getElementsByClassName or getElementById and then apply querySelectorAll on that element.
e.g
let elem = document.getElementsByClassName("MyComponent");
let checkedBoxes = elem.querySelectorAll('input[name=chkBox]:checked');
You also could obtain the total of selected checkboxes by element type. The "console.log(totalSelectedCheckboxes)" will print them when the state of totalSelectedCheckboxes change using useEffect Hook.
import React, { useState, useEffect } from 'react';
const RenderCheckboxes = () => {
const [totalSelectedCheckboxes, setTotalSelectedCheckboxes] = useState(0);
function handleChk() {
setTotalSelectedCheckboxes(document.querySelectorAll('input[type=checkbox]:checked').length);
}
useEffect(() => {
console.log(totalSelectedCheckboxes);
}, [totalSelectedCheckboxes]);
return (<div>
<div>
<input type="checkbox" value={1} onChange={() => handleChk()} />Chk1
</div>
<div>
<input type="checkbox" value={2} onChange={() => handleChk()} />Chk2
</div>
<div>
<input type="checkbox" value={2} onChange={() => handleChk()} />Chk2
</div>
</div>);
}
export default RenderCheckboxes;

Categories