0

I'm having issues with updating the state of a nested object from an input.

Updated with more code. I am trying to build a multi step form. First page takes team information, second page takes player information.

Parent component:

export class MainForm extends Component {
state = {
    step: 1,
    teamName: '',
    teamManagerName: '',
    teamManagerEmail: '',
    player: [{
        firstName: '',
        lastName: '',
        email: '',
        height: ''
    }]
}

// proceed to next step
nextStep = () => {
    const { step } = this.state;
    this.setState({
        step: step + 1
    })
}

// go to previous step
prevStep = () => {
    const { step } = this.state;
    this.setState({
        step: step - 1
    })
}

// handle fields change
handleChange = input => e => {
    this.setState({[input]: e.target.value});
}

render() {
    const { step } = this.state;
    const { teamName, teamManagerName, teamManagerEmail, player: { firstName, lastName, email, height}} = this.state;
    const values = {teamName, teamManagerName, teamManagerEmail, player: { firstName, lastName, email, height}};

    switch(step) {
        case 1:
            return (

                <FormTeamDetails
                    nextStep={this.nextStep}
                    handleChange={this.handleChange}
                    values={values}
                />
                )
        case 2:
            return (
               <FormPlayerDetails
                    nextStep={this.nextStep}
                    prevStep={this.prevStep}
                    handleChange={this.handleChange}
                    values={values}
               />
            )
        case 3:
            return (
                <Confirm
                    nextStep={this.nextStep}
                    prevStep={this.prevStep}
                    values={values}
           />
            )
        case 4:
            return (
                <h1>Scuccess!</h1>
            )
    }

}

}

This next code snippet is the first page of the form and one of the child components. The handleChange function successfully does its job here.

export class FormTeamDetails extends Component {
continue = e => {
    e.preventDefault();
    this.props.nextStep();
}

render() {
    const { values, handleChange } = this.props;
    console.log(this.props)
    return (
        <Container>
            <Form>
                <FormGroup>
                    <Label for="teamName">Team Name</Label>
                    <Input 
                        type="teamName" 
                        name="teamName" 
                        onChange={handleChange('teamName')} 
                        defaultValue={values.teamName} />
                    <Label for="teamManagerName">Team Manager Name</Label>
                    <Input 
                        type="teamManagerName" 
                        name="teamManagerName" 
                        onChange={handleChange('teamManagerName')} 
                        defaultValue={values.teamManagerName}
                        />
                    <Label for="teamManagerEmail">Team Manager Email</Label>
                    <Input 
                        type="teamManagerEmail" 
                        name="teamManagerEmail" 
                        onChange={handleChange('teamManagerEmail')} 
                        defaultValue={values.teamManagerEmail} />

                    <Button 
                        label="Next"
                        // primary="true"
                        onClick={this.continue}
                        style={{float: 'right'}}>Next</Button>
                </FormGroup>
            </Form>
        </Container>

    )
}

}

This is the second page of the form, and where I'm having issues:

export class FormPlayerDetails extends Component {
continue = e => {
    e.preventDefault();
    this.props.nextStep();
}

back = e => {
    e.preventDefault();
    this.props.prevStep();
}

render() {
    const { values: { player }, handleChange, values } = this.props;

    return (
        <Container>
            <h3>Add players</h3>
            <Form>
            <Row form>
                <Col md={3}>
                <FormGroup>
                    <Input type="firstName" 
                    name="firstName" 
                    id="firstName" 
                    placeholder="First Name" 
                    defaultValue={values.player.firstName} 
                    onChange={handleChange('values.player.firstName')} 
                    />
                </FormGroup>
                </Col>
                <Col md={3}>
                <FormGroup>
                    <Input type="lastName" name="lastName" id="lastName" placeholder="Last Name"  />
                </FormGroup>
                </Col>
                <Col md={3}>
                <FormGroup>
                    <Input type="email" name="email" id="email" placeholder="Email" />
                </FormGroup>
                </Col>
            </Row>

            <Row form>
                <Col>
                <Button 
                        label="Back"
                        // primary="true"
                        onClick={this.back}
                        style={{float: 'left'}}>Back</Button>
                </Col>
                <Col>
                <Button 
                        label="Next"
                        // primary="true"
                        onClick={this.continue}
                        style={{float: 'right'}}>Next</Button>
                </Col>
            </Row>

            </Form>
        </Container>

    )
}

}

I'm unable to update the player property with the input values. Also, I would like to add multiple player input fields in this component. I would like at least 5 players to be added to the team, hence the player array in the parent component. Please let me know if I should go about this in another way. Thank you in advance!

5
  • 1
    You are passing a string instead of the variable to your handleChange function. Commented May 11, 2020 at 4:00
  • I changed it to: onChange={handleChange(values.player.firstName)} , but am still not able to set it Commented May 11, 2020 at 4:11
  • Also, apart from my first comment, your method for updating the state seems wrong. In this current shape, it creates a player first name with the given value from the input. It does not update the player array in your state. So, please share some more code with us. How do you pass the props and what is your intention with that input? Commented May 11, 2020 at 4:12
  • What do you mean not working? Is the function not called / it is passing the wrong value so that the state is updated wrongly / the value is correct but the state is not updated? You can add a console.log in handleChange to check. :) Commented May 11, 2020 at 4:12
  • Updated the main post with more code :) thank you! Commented May 11, 2020 at 4:26

2 Answers 2

2

Your player property in your state is an array but you are treating it as an object. So, I assume there will be one player and it will be an object. Here is one solution for your case. But it feels like you are struggling with many unnecessary things there like passing values, etc since you have already name attribute for your inputs. I don't know, maybe you are thinking about other situations.

Here is the handleChange part:

handleChange = input => e => {
  const { target } = e;
  if (input === "player") {
    this.setState(prev => ({
      player: {
        ...prev.player,
        [target.name]: target.value
      }
    }));
  } else {
    this.setState({ [input]: target.value });
  }
};

Here is the input part:

<Input
  type="firstName"
  name="firstName"
  id="firstName"
  placeholder="First Name"
  defaultValue={values.player.firstName}
  onChange={handleChange("player")}
/>

As you can see, I'm passing the handleChange the player property and get the name from your input. Then in the handleChange method, using the functional state and spread syntax, I'm updating the player.

class App extends React.Component {
  state = {
    step: 1,
    teamName: "",
    player: {
      firstName: "",
      lastName: "",
      email: ""
    }
  };

  handleChange = input => e => {
    const { target } = e;
    if (input === "player") {
      this.setState(prev => ({
        player: {
          ...prev.player,
          [target.name]: target.value
        }
      }));
    } else {
      this.setState({ [input]: target.value });
    }
  };

  render() {
    return (
      <div>
        <Input handleChange={this.handleChange} />
        <div>{JSON.stringify(this.state)}</div>
      </div>
    );
  }
}

class Input extends React.Component {
  render() {
    const { handleChange } = this.props;
    return (
      <div>
        <div>
          <span>firstName</span>
          <input
            type="firstName"
            name="firstName"
            id="firstName"
            onChange={handleChange("player")}
          />
        </div>
        <div>
          <span>team</span>
          <input
            type="teamName"
            name="teamName"
            onChange={handleChange("teamName")}
          />
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, 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>
<div id="root" />

Here is the updated version of the players as an array (be careful about the plural name). So, in this naive version, we keep a players array with some unique id's on them. This is how we are going to target the updated player. Check each input, there is an id now and we set it to the player's one. In the handleChange function, this time we are mapping the players (so a new array is returned), then if the id matches with our player's one, we are updating its relevant property. If the id does not match, we are returning the original player without doing anything.

The handleChange part is the one you need to focus on. We are mapping and rendering the inputs by using each player's properties in the Input component. It seems a little bit confusing but if you look into it deeply you can understand. We are using Object.entries to map the properties but before that, we are cleaning the id one since we don't want to display it.

You can find the working snippet below.

class App extends React.Component {
  state = {
    step: 1,
    teamName: "",
    players: [
      {
        id: 1,
        firstName: "foo",
        lastName: "foo's lastname",
        email: "[email protected]"
      },
      {
        id: 2,
        firstName: "bar",
        lastName: "bar's lastname",
        email: "[email protected]"
      }
    ]
  };

  handleChange = input => e => {
    const { target } = e;
    if (input === "players") {
      this.setState({
        players: this.state.players.map(player => {
          if (player.id === Number(target.id)) {
            return { ...player, [target.name]: target.value };
          }

          return player;
        })
      });
    } else {
      this.setState({ [input]: target.value });
    }
  };

  render() {
    return (
      <div>
        <Input handleChange={this.handleChange} players={this.state.players} />
        <div>
          {this.state.players.map(player => (
            <div>
              <h3>Player {player.id}</h3>
              <div>First Name: {player.firstName}</div>
              <div>Last Name: {player.lastName}</div>
              <div>Email: {player.email}</div>
            </div>
          ))}
        </div>
      </div>
    );
  }
}

class Input extends React.Component {
  render() {
    const { handleChange, players } = this.props;
    return (
      <div>
        {players.map(player => {
          const { id, ...playerWithoutId } = player;
          return (
            <div key={player.id}>
              <h3>Player {player.id}</h3>
              {Object.entries(playerWithoutId).map(([key, value]) => (
                <div>
                  <span>{key}</span>
                  <input
                    name={key}
                    id={player.id}
                    onChange={handleChange("players")}
                    defaultValue={value}
                  />
                </div>
              ))}
            </div>
          );
        })}
      </div>
    );
  }
}

ReactDOM.render(<App />, 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>
<div id="root" />

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you! However, firstName is still not getting updated from the input value after handleChange is executed. Also, can you explain to me how ...prev.player works? I'm still very new to js. Thank you!
You can see my snippet. It is working for my case. Just change the related parts according to that. Don't forget to change the player in your state. It should be an object, not an array. prev part is related to functional update for setState and ... is the spread syntax. Just google them :) If you have still questions, I'll try to explain it.
Thank you! Your solution worked! Originally I had player defined as an array instead of an object, because in my PlayerForm, I wanted the user to have the ability to enter information for multiple players. With player as an object, how would I be able to take inputs for multiple players?
You can't do it if you keep the player info as an object. I've updated my answer and added one version using an array for players.
Your solutions worked seamlessly. Thank you so much for your help!
0

For FormPlayerDetails, you dont need to pass complete values, instead you just need to pass values.players. And since your players attribute is a table... use a map function instead

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.