I have a Form component with a title input and a dynamic number of ingredient inputs (the user can add and remove them with corresponding buttons). Most of the form functions properly. However, I receive a warning that I have passed a value prop to a form field without an onChange handler for the ingredient input element(s).
In render() for my Form component, this part should dynamically generate inputs (EDIT: by mapping from an array of ingredients in my Form's state):
{this.state.ingredients.map((element, i) => {
return (
<div key={i}>
<input
type='text'
placeholder='new ingredient'
value={element || ''}
onChange={this.handleIngredientChange.bind(this)}
/>
</div>
);
})}
I thought these inputs would connect to my handleIngredientChange method to update the state of the ith ingredient name when the user changes the input value:
handleIngredientChange(i, event) {
let ingredients = [...this.state.ingredients];
ingredients[i] = event.target.value;
this.setState({ingredients});
}
And that this method would appropriately allow react to control each input element.
I seem to be misunderstanding this and/or .bind(), because I clearly assigned onChange={this.handleIngredientChange.bind(this)} to the ingredient input element in my map function. When the user types in one of these inputs, we get `TypeError: undefined is not an object (evaluating 'event.target').
Perhaps the root of the problem is that when the user types in the input, handleIngredientChange is not correctly set up to get the user's desired input value, but I cannot see where the error is.
I have looked at a lot of similar questions and have tried to implement their answers to no avail. Could anyone point me in the right direction on why I have not handled the onChange event properly? I'd be more than happy to explain my intentions further if you need me to. Many thanks in advance.
Full code:
export default class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
title: '',
ingredients: [],
};
}
handleTitleChange(event) {
let title = this.state.title;
title = event.target.value;
this.setState({title});
}
handleIngredientChange(i, event) {
let ingredients = [...this.state.ingredients];
ingredients[i] = event.target.value;
this.setState({ingredients});
}
removeClick(i) {
let ingredients = [...this.state.ingredients];
ingredients.splice(i, 1);
this.setState({ingredients});
}
addClick() {
let ingredients = [...this.state.ingredients];
ingredients.push('');
this.setState({ingredients});
}
handleSubmit(event) {
console.log('submit request logged')
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
placeholder='recipe title'
value={this.state.title || ''}
onChange={this.handleTitleChange.bind(this)}
/>
{this.state.ingredients.map((element, i) => {
return (
<div key={i}>
<input
type='text'
placeholder='new ingredient'
value={element || ''}
onChange={this.handleIngredientChange.bind(this)}
/>
</div>
);
})}
<input
type='button'
value='add'
onClick={this.addClick.bind(this)}
/>
<input
type='Submit'
value='save'
onClick={this.handleSubmit.bind(this)}
/>
</form>
)
}
}
I'm not sure if this helps
try change
<input
type="text"
placeholder="new ingredient"
value={element || ""}
id={i}
onChange={this.handleIngredientChange.bind(this)}
/>
and then alter
handleIngredientChange(event) {
let ingredients = [...this.state.ingredients];
ingredients[event.target.id] = event.target.value;
this.setState({ ingredients });
}
https://codesandbox.io/s/react-playground-forked-yf7rpm
Related
I'm currently new in react js and I want to implement a Search in react on array. What I'm trying to do is implement search functionality so that when the user enters in 'E' in "textbox", he/she should see only the names that contain the character 'E' and luckily its working fine till here. Ideally I want that When a user start taking a step back by pressing Backspace or Deleting entered character(s), the list of names should get loaded back with all non matching values and vice versa. Apart, I also I want to disable spacebar in textbox with an alert message when entering spacebar in textbox. And on clicking 'OK' of an alert message the screen that comes after should be same like as it was before entering spacebar(With all names loaded and "textbox" asking user to enter something).
What my code is providing only names matching with the first character even "textbox" is empty and In case of spacebar, its allowing to take next input(just after spacebar) after clicking on alert message's 'OK'. I think its taking spacebar as input.
My code's functionality is neither rolling back of names on Backspace or Deleting a character nor a screen is setting to the same as it was before entering spacebar. I'm getting my desired results by refreshing a tab which I don't want to do. Please help me to resolve the issue
My code is something like as below:
import React from 'react';
import NameItem from "./component/NameItem";
class App extends React.Component {
constructor(props) {
super(props)
this.state = { term: '', names: ["Elon Musk","Bill Gates","Tim Cook","Richard Branson","Jeff Bezos","Warren Buffet","The Zuck","Carlos Slim","Bill Gates","Larry Page","Harold Finch","Sergey Brin","Jack Ma","Steve Ballmer","Phil Knight","Paul Allen","Woz"], filteredData:[] };
}
renderData(filteredData) {
if(filteredData) {
this.setState({names: filteredData});
}
return filteredData.map(item => <NameItem item={item}></NameItem>);
}
filterNames(namePass) {
if(namePass && this.state.names) {
if(namePass === ' ') {
alert('please dont enter space.')
return;
}
let filteredData = this.state.names.filter((item) => {
return item.toLowerCase().startsWith(namePass.toLowerCase())
});
console.log(filteredData);
this.setState({filteredData: filteredData});
if (filteredData) {
this.renderData(filteredData);
}
}
}
render() {
return (
<>
<div>
<label>Search Person: </label>
<input type="text" id="searchEmp"
placeholder="Enter Person's Name"
onChange={(event) => {
this.setState({term: event.target.value});
this.filterNames(event.target.value);
}}/><br/>
</div>
<ul>
<NameItem item={this.state.names}></NameItem>
{
}
</ul>
</>
);
}
}
export default App;
I would recommend that you don't store the filteredData in the state but that's just a preference. You already have the term stored so you can map to the filtered values in your render().
What you absolute cannot do is override this.state.names with just the filtered names, which you are doing here:
if(filteredData) {
this.setState({names: filteredData});
}
When you do that you lose every name which isn't in the filtered array, so you cannot possibly go backwards and show the full list again.
There's no point in setting this.state.name to filteredData because you just render filteredData directly.
class App extends React.Component {
// don't actually need a constructor if there are no props
// can initialize state like this
state = {
term: "",
names: ["Elon Musk","Bill Gates","Tim Cook","Richard Branson","Jeff Bezos","Warren Buffet","The Zuck","Carlos Slim","Larry Page","Harold Finch","Sergey Brin","Jack Ma","Steve Ballmer","Phil Knight","Paul Allen","Woz"]
};
render() {
const filteredData = this.state.names.filter(
(name) =>
// match all names if term is empty
this.state.term === "" ||
// otherwise see if the name starts with the term
name.toLowerCase().startsWith(this.state.term.toLowerCase())
);
return (
<>
<div>
<label>Search Person: </label>
<input
type="text"
id="searchEmp"
placeholder="Enter Person's Name"
value={this.state.term} // make this a controlled input, prevents space from showing
onChange={(event) => {
if ( event.target.value.endsWith(' ')) {
alert('Please don\'t enter space.');
} else {
this.setState({ term: event.target.value });
}
}}
/>
</div>
<div>Showing {filteredData.length} Matches</div>
<ul>
{filteredData.map((name) => (
<li
key={name} // should have a key when using map
>
{name}
</li>
))}
</ul>
</>
);
}
}
Note: I had some strange behavior due to using the name as the key because you had "Bill Gates" in the list twice. I deleted the duplicate item and now it's fine. But keys must be unique, so do not use the name as the key if there is a chance of duplicates.
I'm building a mortgage calculator app and I want to have it update in real-time in front of the user. To do this I need to update state whenever input is changed so that the component rerenders.
I plan to do this using the onChange event on each input field, my question is should the onChange event call a different function for each input to update that state property, or is there a way to have one function, and change the state property that is being updated.
For example here is my class Component with the form, state, and onInputChange function
class Calculator extends Component {
state = {
price: 250000,
term: 20
};
onInputChange = (event) => {
this.setState({ price: event.target.value })
};
render() {
return (
<div>
<form>
<label htmlFor="propertyPrice">Property Price</label>
<input
type="number"
id="propertyPrice"
name="propertyPrice"
value={this.state.price}
onChange={this.onInputChange}
/>
<label htmlFor="term">Mortgage Term</label>
<input
type="term"
id="term"
name="term"
value="3"
/>
</form>
</div>
);
}
}
As you can see on the Property Price input I'm using the onChange event to call the function onInputChange, which directly updates the price in the state. Do I need a new function for the mortgage term input, or can I use the one function and just change which state property I'm updating, if so, how can I determine which state property is being updated in the function?
I do it this way:-
Create a single onInputChange() like this
onInputChange = (name, value) = {
this.setState({[name]: value})
}
Now create any number of inputs but make sure your onChange handler receives a function like this
onChange = {(e) => onInputChange(nameOfInput, e.target.value)}
Make your form input tags name same as state name and then you can add this for every input's onChange handler.
const handleChange = (e) => {
this.setState({[e.target.name]: e.target.value});
}
I have 3 sections of the application, one is form for filling about watches, data is being submitted by submitHandler function.
Second part is entered watches from the first part of the application. When I click on buy of the each watch, should be added to the third part of the application, with the restriction that the same watches(with the same name) should be only added once - should be unique, and i should spot it by application, and not to enable adding to third part of the same watches.
Below is my code with 2 functions, parent function buyWatchHandler that is invoking add function.
buyWatchHandler function is receiving input parameters(watch details) from form fields collected by submitHandler and that works perfectly. In buyWatchHandler i have let arr that is invoking add functions, and in arr variable should be set the resulting array from add function.
When we enter in add function, the input parameters are well forwarded. There in I am setting arr to value of state variable this.state.selectedWatchList.
Here emerges the problem. I am using "some" method for arrays to check if the value of name of the watch received as selectedWatchName(input) is the same as the one in object of arr as el.selectedWatchName. If yes it is returned true, and I want not forward to invoking function. The logical result(true or false) is set into found. If found is false, or to be clear - value of element is not the same as the one from arr(unique watch), I want to concatenate to variable arr the value of the selectedWatchName and the values of the other input parameters of add function.
In invoking function we should receive in let arr the forwarded array, and should be added to state variable selectedWatchList.
When I click on buy button first time in console is being printed false for found method which is logical, as the el.selectedWatchName is empty and different from selectedWatchName(input), arr from add function is being empty printed and should be not empty as well as this.state.selectedWatchList in buyWatchHandler is being printed empty. On second click selectedWatchList is filled with data, found is true which is ok, as i am trying to add the same watch to array of selectedWatchList. On third click the application is breaking, and I do not know the reason, Can anyone spot what is wrong, and why is breaking? The warning is the following: App.js:66 Uncaught TypeError: Cannot read property 'some' of undefined. The buy button should be functioning also on third click and every next, and should add each new unique name of the watch to selectedWatchList.
import React, { Component } from 'react';
import EnteredWatches from '.././components/EnteredWatches/EnteredWatches';
class App extends Component {
constructor(props) {
super(props)
this.state = {
watchName: '',
watchDescription: '',
watchUrl: '',
watchPrice: '',
watchId: '',
enteredWatchList: [],
selectedWatchName: '',
selectedWatchDescription: '',
selectedWatchUrl: '',
selectedWatchPrice: '',
selectedWatchId: '',
selectedWatchList: []
}
}
submitHandler = (event) => {
event.preventDefault();
let watchId = Math.floor((Math.random() * 100) + 1);
let watchName = this.state.watchName;
let watchDescription = this.state.watchDescription;
let watchUrl = this.state.watchUrl;
let watchPrice = this.state.watchPrice;
this.setState({
enteredWatchList: this.state.enteredWatchList.concat({watchName, watchUrl, watchDescription, watchPrice, watchId})
})
console.log(this.state.enteredWatchList);
}
add = (selectedWatchName, selectedWatchUrl, selectedWatchDescription, selectedWatchPrice, index) => {
let arr = this.state.selectedWatchList;
let found = arr.some(el => {
return el.selectedWatchName === selectedWatchName;
});
console.log(found);
if (!found) {
return arr.concat({selectedWatchName, selectedWatchUrl, selectedWatchDescription, selectedWatchPrice, index});
}
console.log(arr);
}
buyWatchHandler = (selectedWatchName, selectedWatchUrl, selectedWatchDescription, selectedWatchPrice, index) => {
let arr = this.add(selectedWatchName, selectedWatchUrl, selectedWatchDescription, selectedWatchPrice, index);
this.setState({
selectedWatchName: selectedWatchName,
selectedWatchUrl: selectedWatchUrl,
selectedWatchDescription: selectedWatchDescription,
selectedWatchPrice: selectedWatchPrice,
selectedWatchId: index,
selectedWatchList: arr
});
console.log(this.state.selectedWatchList);
}
render() {
const enteredWatches = this.state.enteredWatchList.map((enteredWatch, index) => {
return <EnteredWatches
key={index}
enteredWatch={enteredWatch}
selected={this.buyWatchHandler.bind(this, enteredWatch.watchName, enteredWatch.watchUrl,
enteredWatch.watchDescription, enteredWatch.watchPrice, index)}
/>
});
return (
<div className="App">
<div className="container-fluid">
<div className="container">
<div className="add-product">
<form>
<div>
<label>Product name:</label>
<input
type="text"
placeholder="Casio Watch"
value={this.state.watchName.}
onChange={event => this.setState({watchName: event.target.value})}
/>
</div>
<div>
<label>Product description:</label>
<textarea
placeholder="Sample description..."
value={this.state.watchDescription}
onChange={event => this.setState({watchDescription: event.target.value})}
>
</textarea>
</div>
<div>
<label>Product image:</label>
<input
type="text"
placeholder="http://...jpg"
value={this.state.watchUrl}
pattern="https?://.+" required
onChange={event => this.setState({watchUrl: event.target.value})}
/>
</div>
<div>
<label>Product price:</label>
<input
type="number"
min="0"
placeholder="33.50"
value={this.state.watchPrice}
onChange={event => this.setState({watchPrice: event.target.value})}
/>
</div>
<button
type="submit"
onClick={event => this.submitHandler(event)}
>
Add a new Task
</button>
</form>
</div>
<div className="list-products">
<ul>
{enteredWatches}
</ul>
</div>
<div className="shopping-cart">
<div className="shopping-cart-products">
</div>
<div className="shopping-cart-summary">
<div>Total: <b>$429</b></div>
<div><button>Purchase</button></div>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default App;
In your add method you only return if(!found), yet the expectation in your code is that the add() method will always return an array.
Next, when add does not have an explicit return JavaScript returns undefined by default. Finally, you set the state of selectedWatchList to the value of arr..hence you get .some() trying to access an undefined value whenever a duplicate is entered.
To fix this, the add method should always return this.selectedWatchList given the condition of if(found) or as an else in your current code.
I have a large form with various form elements which is dynamically rendered from a get request. All other types of form (such as text and select) are working fine, however the checkbox is not.
After I check it once, it only stays on (even if I uncheck it), am I missing something or doing something wrong here?
Here is my current relevant code:
class Input extends Component{
render(){
var form;
if (this.props.componentClass=="choice") {
// select form
}
else if (this.props.componentClass=="bool")
form =(<Checkbox id={this.props.controlId} onChange={this.props.onChange}
defaultChecked={this.props.placeholder} >
</Checkbox>);
else
// text form
return (
<div>
<Form inline onSubmit={this.handleSubmit}>
<FormGroup controlId={this.props.controlId}>
<ControlLabel>{this.props.name}</ControlLabel>
{form}
<Panel>
{this.props.description}
</Panel>
<FormControl.Feedback />
</FormGroup>
</Form>
<br/>
</div>
);
}
}
// onChange code (comes from a parent component)
onChange(e){
const form = Object.assign({}, this.state.form);
form[e.target.id] = e.target.value;
this.setState({ form });
console.log('current state: ', this.state);
}
You must bind onChange function as said before, but you should use "checked" instead of "value".
Here is your example modified this way:
https://jsfiddle.net/8d3of0e7/3/
class Input extends React.Component{
constructor(props){
super(props)
this.state = {form:{}}
}
render(){
var form;
if (this.props.componentClass=="choice") {
// select form
}else if (this.props.componentClass=="bool"){
form = (
<ReactBootstrap.Checkbox
id={this.props.controlId}
onChange={this.props.onChange.bind(this)}
checked={this.state.form[this.props.controlId]}
defaultChecked={this.props.placeholder} >
</ReactBootstrap.Checkbox>);
}else{
// text form
}
return (
<div>
<ReactBootstrap.Form inline onSubmit={this.handleSubmit}>
<ReactBootstrap.FormGroup controlId={this.props.controlId}>
<ReactBootstrap.ControlLabel>
{this.props.name}
</ReactBootstrap.ControlLabel>
{form}
<ReactBootstrap.Panel>
{this.props.description}
</ReactBootstrap.Panel>
<ReactBootstrap.FormControl.Feedback />
</ReactBootstrap.FormGroup>
</ReactBootstrap.Form>
<br/>
</div>
);
}
componentDidUpdate(){
console.log('current state: ', this.state);
}
}
function onChange(e) {
const form = Object.assign({}, this.state.form);
form[e.target.id] = e.target.checked;
this.setState({ form });
}
ReactDOM.render(
<Input componentClass='bool' controlId='retired'
name='Is retired?' onChange={onChange}/>,
document.getElementById('root')
)
In this example our state will be: state:{form:{retired:true}}
The issue is that you gave the checkbox an onChange without binding it to a value. Therefor, the initial check is working since that is what the defaultChecked is doing for you, but once you actually interact with it, you have no state bound to it causing react to not rerender it in the checked or unchecked position.
So say you have a child component that returns a number of input fields and takes
a data object as a prop.
class InputFields extends React.Component {
render() {
let data = this.props.data
return (
<div className='input-group'>
<input type='text' defaultValue={data.name} onChange={this.props.update} />
<input type='text' defaultValue={data.age} onChange={this.props.update} />
<input type='text' defaultValue={data.email} onChange={this.props.update}/>
</div>
)
}
}
And the parent component might look something like:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
items: getItemsFromSomewhere()
}
}
update(event) {
/* Somehow update main state here? */
}
render() {
/* imagine this as an array of objects, each with name, age, email ,etc. */
var items = this.state.items.map(item => {
return <InputFields data={item} update={this.update.bind(this)} />
})
return <div className='itemLists'> {items} </div>
}
}
Within this.props.update I want to set the corresponding state for the data object which was passed down. But the onChange function only receives the synthetic event which only provides a reference to the DOM element itself.
Whats the best way to find which state field needs to be updated, e.g., when the input field for data.name is changed, how can I propagate that up to the state within the main parent?
I know that refs could be used, but how can I know which ref to grab on the change event? Do I just have to iterate through all of them?
You could do it like this:
class InputFields extends React.Component {
_onChange(field) {
this.props.update(field, this.props.data);
}
render() {
let data = this.props.data
return (
<div className='input-group'>
<input type='text' defaultValue={data.name} onChange={this._onChange.bind(this, 'name')} />
<input type='text' defaultValue={data.age} onChange={this._onChange.bind(this, 'age')} />
<input type='text' defaultValue={data.email} onChange={this._onChange.bind(this, 'email')}/>
</div>
)
}
}
and then in the update function of the parent component you get the changed field as a string plus the whole data object:
update(field, item) {
// do stuff
var value = item[field];
}
edit: You were right I answered something dumb too quickly. Below is a brand new answer that'll fit your needs I think.
Why not using event.target.value ?
and you just use an event handler ?
In the child:
<input type='text' id="name" defaultValue={data.name} onChange={this.props.update("name")} />`
And the update function:
update(stateToChange) {
var self = this;
return function(e) {
var newState = {};
newState[stateToChange] = e.target.value;
self.setState(newState);
}
}
Hope it helped this time :)