I have a form component that has a state containing an array of items.
I am having a hard time trying to update the state of the form when one of the item inputs gets updated.
At first I was creating a state on the items themselves and updating the values using the following code:
class ItemRow extends Component{
constructor(props){
super(props)
this.state = this.props.item;
}
updateItem(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
.....
render(){
return (
<FormControl
type="text"
name="name"
value={this.state.name}
onChange={this.updateItem}
/>
<FormControl
type="text"
name="price"
value={this.state.price}
onChange={this.updateItem}
/>
.....
)
}
}
This worked fine for updating the value of the of the inputs, however the state was local to the item and not reflected or accessible by the form
I am trying to figure out how to keep the state in the form and have the item update the state of the form
I think this is the right approach but I can't figure out how to get it to work.
At this point I have something similar the following:
class Form extends Component{
this.state = {
items: [
{ name: 'soup', price: 7, quantity: 1 }
{ name: 'salad', price: 5, quantity: 2 }
]
}
updateItem(e) {
// Not sure how to handle updating
}
removeItem(item) {
let items = this.state.items;
items.splice(items.indexOf(item), 1);
this.setState({items: items})
}
render(){
return(
<ItemTable items={this.state.items} updateItem={this.updateItem} removeItem={this.removeItem} />
)
}
}
ItemTable:
class ItemTable extends Component {
removeItem(item){
this.props.removeItem(item)
}
render(){
let items = [];
this.props.items.forEach((item) => {
items.push(<ItemRow item={item} key={item.id} removeItem={this.removeItem.bind(this,item)} updateItem={this.props.updateItem}/>);
});
return(
{items}
)
}
}
ItemRow:
class ItemRow extends Component {
removeItem(item){
this.props.removeItem(item)
}
render() {
return (
<FormControl
type="text"
name="name"
value={this.props.item.name}
onChange={this.updateItem}
/>
<FormControl
type="text"
name="quantity"
value={this.props.item.quantity}
onChange={this.updateItem}
/>
<FormControl
type="text"
name="price"
value={this.props.item.price}
onChange={this.updateItem}
/>
<Button bsStyle="warning" onClick={this.removeItem}><Glyphicon glyph="trash"/></Button>
)
}
}
You're very close to the solution.
If you need to have a state shared between components, you should have it in the most parent component that should be aware of the state (in your case the Form component).
You pass down as props the method "updateItem" from the Form to the ItemTable and then ItemRow (like you're doing)
At this stage, inside the ItemRow you can use the method by calling 'this.props.updateItem' and you can run the function defined in Form, passing some parameters, if you need to.
Related
//App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
formVal: {},
showMaker: true,
showTemplate: false,
uniMakerArr: [],
uniTemplateArr: [],
this.handleChange = this.handleChange.bind(this);
this.onClickMaker = this.onClickMaker.bind(this);
this.onClickTemplate = this.onClickTemplate.bind(this);
this.addUniFn = this.addUniFn.bind(this);
handleChange = (e) => {
let formVal = this.state.formVal;
let name = e.target.name;
let val = e.target.value;
formVal[name] = val;
this.setState({formVal})
};
addUniFn(e) {
const {uniMakerArr, uniTemplateArr} = this.state;
uniMakerArr.push(<UniMaker key={uniqid()} />);
uniTemplateArr.push(<UniTemplate key={uniqid()} />)
this.setState({
uniMakerArr, uniTemplateArr
});
};
onClickMaker(e) {
this.setState({
showMaker: true,
showTemplate: false,
});
};
onClickTemplate(e) {
this.setState({
showMaker: false,
showTemplate: true,
});
};
render() {
const {firstName, lastName} = this.state.formVal;
const {showMaker, showTemplate, uniMakerArr, uniTemplateArr} = this.state;
return (
<div>
<header className="header">
Curriculum Vitae!
</header>
<nav className="nav">
<a onClick={this.onClickMaker}>Maker</a>
<div className='vl'></div>
<a onClick={this.onClickTemplate}>Preview</a>
</nav>
<div>
{showMaker ? <Maker
handleChange={this.handleChange} add={this.addUniFn}
uniMakerState={uniMakerArr} /> : null}
{showTemplate ? <Preview
uniMakerArr={uniMakerArr}
uniTemplateArr={uniTemplateArr}
//state value for object "formVal"
firstName={firstName}
lastName={lastName}/> : null}
</div>
</div>
);
}
}
export default App;
Here is component Maker:
class Maker extends Component {
constructor(props) {
super(props);
}
render() {
const {handleChange, formVal, add, uniMakerState} = this.props;
return(
<div className='maker'>
<p className="headerText">Personal Information</p>
<form className='personalInp'>
<label htmlFor='firstName'></label>
<input type="text" name="firstName" value={formVal} onChange={handleChange} placeholder="First Name"></input>
<label htmlFor='lastName'></label>
<input type="text" name="lastName" value={formVal} onChange={handleChange} placeholder="Last Name"></input>
{uniMakerState.map((component) => {
return <UniMaker formVal={formVal} handleChange={handleChange}
key={component.key} }/>
})}
<button type="button" className="addBtn" onClick={add}>Add</button>
</form>
<div/>
)
}
}
export default Maker;
Here is component Preview:
class Preview extends Component {
constructor(props) {
super(props);
}
render() {
const {firstName, lastName,uniTemplateArr} = this.props;
return (
<div className='template'>
<p className="name">{firstName || 'Your'} {lastName || 'Name'}</p>
{uniTemplateArr.map(component => <UniTemplate />)}
</div>
)
}
}
export default Preview;
So what this basically does is add two-component when clicking the add button to Maker and Preview tabs. These two components are child components to Maker and Preview components. And they are identical to these two components. This means They will add two more HTML input elements and the fields where the values can be shown.
But the problem is when I make changes in the form that is in the Maker tab. It only reads and changes the value for the first two fields that are in the Preview tab. But ignores the other dynamically added fields. How can I make changes to my code so that the dynamically added input fields relate to the other component that was also added dynamically and only changes values to the corresponding input elements to the divs field?
I've tried putting unique keys to the input elements. But I don't know how to use those keys to update the values for the correct fields. Lastly, I'm sorry if the code is not so readable and the question is confusing. This is my first question, and I tried to be as thorough and transparent as possible. Thanks!
I am trying to :
generate radio buttons from a constant array using Map in react
let user select one and set the state with handleChange()
With the following code I was able to achieve 1, but for some reason when I try to display with handleChange() I see it is an empty string.
Could you please help me ?
Thanks
import React, { Component } from "react";
const members = ["Araki", "Ibata", "Fukutome", "Woods", "Alex", "Tatsunami"];
export default class MyRadio extends Component {
constructor(props) {
super(props);
this.state = {
lastName: "",
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
console.log("handleChange() e:" + e.target.value);
this.setState({
[e.target.name]: e.target.value,
});
}
render() {
console.log("render");
return (
<form>
<div>
{members.map((item) => (
<React.Fragment>
<label htmlFor={item.name}> {item}</label>
<input
name="lastName"
key={item.name}
id={item.name}
value={item.name}
type="radio"
onChange={this.handleChange}
/>
</React.Fragment>
))}
</div>
<div></div>
</form>
);
}
}
To make this workable solution you have to change the members as follow
const members = [{name: "Araki", name: "Ibata", ...}];
array should be a object array with each object has name property because in the map you are expecting name should be there as item.name.
Or either you have to change the loop without item.name you have to use item
{members.map((item) => (
<React.Fragment>
<label htmlFor={item}> {item}</label>
<input
name="lastName"
key={item}
id={item}
value={item}
type="radio"
onChange={this.handleChange}
/>
</React.Fragment>
))}
How can I do a 2 way binding of a variable from the parent component (Form.js), such that changes occurred in the child component (InputText.js) will be updated in the parent component?
Expected result: typing values in the input in InputText.js will update the state of Form.js
In Form.js
render() {
return (
<div>
<InputText
title="Email"
data={this.state.formInputs.email}
/>
<div>
Value: {this.state.formInputs.email} // <-- this no change
</div>
</div>
)
}
In InputText.js
export default class InputText extends React.Component {
constructor(props) {
super(props);
this.state = props;
this.handleKeyChange = this.keyUpHandler.bind(this);
}
keyUpHandler(e) {
this.setState({
data: e.target.value
});
}
render() {
return (
<div>
<label className="label">{this.state.title}</label>
<input type="text" value={this.state.data} onChange={this.handleKeyChange} /> // <-- type something here
value: ({this.state.data}) // <-- this changed
</div>
)
}
}
You can manage state in the parent component itself instead of managing that on child like this (lifting state up):
In Form.js
constructor(props) {
super(props);
this.handleKeyChange = this.keyUpHandler.bind(this);
}
keyUpHandler(e) {
const { formInputs } = this.state;
formInputs.email = e.target.value
this.setState({
formInputs: formInputs
});
}
render() {
// destructuring
const { email } = this.state.formInputs;
return (
<div>
<InputText
title="Email"
data={email}
changed={this.handleKeyChange}
/>
<div>
Value: {email}
</div>
</div>
)
}
In InputText.js
export default class InputText extends React.Component {
render() {
// destructuring
const { title, data, changed } = this.props;
return (
<div>
<label className="label">{title}</label>
<input type="text" value={data} onChange={changed} />
value: ({data})
</div>
)
}
}
You can also make your InputText.js a functional component instead of class based component as it is stateless now.
Update: (How to reuse the handler method)
You can add another argument to the function which would return the attribute name like this:
keyUpHandler(e, attribute) {
const { formInputs } = this.state;
formInputs[attribute] = e.target.value
this.setState({
formInputs: formInputs
});
}
And from your from you can send it like this:
<input type="text" value={data} onChange={ (event) => changed(event, 'email') } />
This assumes that you have different inputs for each form input or else you can pass that attribute name also from parent in props to the child and use it accordingly.
You would need to lift state up to the parent
parent class would look something like
onChangeHandler(e) {
this.setState({
inputValue: e.target.value // you receive the value from the child to the parent here
})
}
render() {
return (
<div>
<InputText
title="Email"
onChange={this.onChangeHandler}
value={this.state.inputValue}
/>
<div>
Value: {this.state.inputValue}
</div>
</div>
)
}
children class would look something like
export default class InputText extends React.Component {
constructor(props) {
super(props);
this.state = props;
}
render() {
return (
<div>
<label className="label">{this.state.title}</label>
<input type="text" value={this.state.value} onChange={this.props.onChange} />
value: ({this.state.value})
</div>
)
}
}
You can simply pass a callback from Form.js to InputText and then call that callback in InputText on handleKeyChange
I try to pass state to the child, to update the list of objects.
When I add an entry, it's not rendered in the child component.
I also checked that state.contacts actually gets replaced with new array, but it didn't work.
constructor(props) {
this.super(props);
}
removeContact(event) {
this.setState((state) => {
state.contacts = state.contacts.filter((contact) => contact.key !== event.target.key )
return state;
})
}
render() {
return (
<Fragment>
<span>{this.props.contact.name}</span>
<span>{this.props.contact.phone}</span>
<span>{this.props.contact.adress}</span>
<a href="#" onClick={this.removeContact}>X</a>
</Fragment>
)
}
}
class Contacts extends Component {
constructor(props) {
super(props);
this.state = { contacts: props.contacts };
}
render() {
console.log(this.state.contacts); // always displays empty array
return (
<div>
{this.state.contacts.map((contact, index) =>
<div>
<Contact key={index} contact={contact} contacts={this.state.contacts}/>
</div>
)}
</div>
)
}
}
class App extends Component {
state = {
time: new Date(),
name: "",
phone: "",
adress: "",
contacts: []
}
change = (event) => {
let nameOfField = event.target.name;
this.setState({[nameOfField]: event.target.value})
}
// click = () => {
// this.setState((state) => {
// state.time = new Date();
// return state;
// })
// }
addContact = () => {
let name = this.state.name;
let phone = this.state.phone;
let adress = this.state.adress;
this.setState((state) => {
return {contacts: [ ... state.contacts.concat([{name, adress, phone}])]}
});
}
render() {
return (
<div className="App">
<Timestamp time={this.state.time}/>
<Contacts contacts={this.state.contacts}/>
<input name="name" value={this.state.name} onChange={this.change} placeholder="Name"/>
<input name="phone" value={this.state.phone} onChange={this.change} placeholder="Phone"/>
<input name="adress" value={this.state.adress} onChange={this.change} placeholder="Adress"/>
<button onClick={this.addContact}>Add contact</button>
</div>
)
}
}
ReactDOM.render(<App time={Date.now().toString()}/>, document.getElementById('root'));
If values are passed to Components you should render them as props. There is no need to copy into the child component state:
class Contacts extends Component {
render() {
console.log(this.props.contacts); // use props instead of state
return (
<div>
{this.props.contacts.map((contact, index) =>
<div>
<Contact key={index} contact={contact} contacts={this.props.contacts}/>
</div>
)}
</div>
)
}
}
Using this.props is good practice because it allows React to deterministically render (If the same props are passed, the same render result is returned).
You are currently modifying the state in Contacts from it's child component Contact. You can't update a parents state directly from within a child component.
What you could do is create a removeContact function in your Contacts component and pass the entire function down to your Contact component. That way when you call removeContact in your child component, it will actually call it from the parent, modify the parents state, and update all it's children with the new state.
I have the following parent component:
class NewPetForm extends React.Component {
state = {
name: '',
age: '',
animal: '',
breed: ''
};
render() {
return (
<StyledNewPetForm >
<Input label="name" />
<Input label="age" />
<Input label="animal"/>
<Input label="breed"/>
<Button type="submit" />
</StyledNewPetForm>
);
}
}
And the following child component:
class Input extends React.Component {
render() {
return (
<StyledWrapper>
<StyledInput
value={this.props.test}
type="text"
/>
</StyledWrapper>
);
}
}
Is it possible to listen to changes in all inputs in children components and update the state accordingly?
What I want to achieve is basically passing handlers to different Inputs and update state dynamically, so there is a possibility to reuse Input component.
changeHandler = (event,stateName) =>{
this.setState({[stateName]:event.target.value]})
}
<Input changed={changeHandler(event,'name')}></Input>
<Input changed={changeHandler(event,'age')}></Input>
<Input changed={changeHandler(event,'breed')}></Input>
// Inside Input
<input onChange={this.props.changed}/>
Thanks for help!
Yes you can do that, you can pass a function to the child element as a property and call it when a change occurs in the child.
Here is an example:
class NewPetForm extends React.Component {
state = {
name: ''
};
onValueChange(key, event) {
this.setState({[key]: event.target.value})
}
render() {
return (
<StyledNewPetForm >
<Input value={this.state.name} onValueChange={this.onValueChange.bind(this, 'name')} />
</StyledNewPetForm>
);
}
}
and the child
class Input extends React.Component {
render() {
return (
<StyledWrapper>
<StyledInput
onChange={this.props.onValueChange}
value={this.props.value}
type="text"
/>
</StyledWrapper>
);
}
}
{[key]: event.target.value} may seem confusing, is just the new syntax that lets you use a string as a property name in an object literal.
The important part is onChange={this.props.onValueChange}, here, I'm calling the parent function NewPetForm.onValueChange when the input's value changes.