2

I am new to React and Javascript.

I am trying to have a user fill in a form that describes what a "Mob" should look like. When the user hits submit, I expect handleSubmit() (passed in through a parent) to modify the parent's state, which is an object. However, this behavior is not happening.

Here is the parent component, called App.

class App extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            mob: new Mob("", "")
        };

        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleSubmit(event) {
        event.preventDefault();
        alert("A name was submitted: " + this.state.vnum + " event value: " + event.state.vnum);
        const newMob = new Mob(event.state.vnum, event.state.shortDesc);

        this.setState({
            mob: newMob
        });
    }

    render() {
        return (
            <div>
                <MobForm mob={this.state.mob} onSubmit={() => this.handleSubmit} />
                {console.log("parsed mob vnum: " + this.state.mob.vnum)}
            </div>
        );
    }
}

The child component, called MobForm

class MobForm extends React.Component {
    render() {
        return (
            <div>
                <form onSubmit={this.props.onSubmit}>
                    <CreateStringInputField
                        name="vnum"
                        label="vnum:"
                    />
                    <CreateStringInputField
                        name="shortDesc"
                        label="Short Desc:"
                    />
                    <input type="submit" value="Submit" />
                </form>
                {console.log(this.state)}
            </div>
        );
    }
}

Which is calling CreateStringInputField()

function CreateStringInputField(props) {
    return (
        <div name="row">
            <label>
                <b>{props.label}</b>
                <br />
                <input
                    type="text"
                    name={props.name}
                    label={props.label}
                />
            </label>
        </div>
    );
}

And, in case it matters, here is what "Mob" looks like.

class Mob {
    constructor(vnum, shortDesc) {
        this.vnum = vnum;
        this.shortDesc = shortDesc;
    };
}

I expect to see {console.log("parsed mob vnum: " + this.state.mob.vnum)} print out the vnum as entered by a user. Instead, I see nothing. How can I achieve this expected output?

cftools
  • 23
  • 4

3 Answers3

0

In this line

<MobForm mob={this.state.mob} onSubmit={() => this.handleSubmit} />

you are defining an anonymous function that returns your handleSubmit function.

In your form

<form onSubmit={this.props.onSubmit}>

onSubmit will execute the this.props.onSubmit which just returns the handleSubmit function but it wont execute it. To fix it just change MobForm to pass handleSubmit directly instead of passing it in an anonymous function:

<MobForm mob={this.state.mob} onSubmit={this.handleSubmit} />

To handle the submission correctly you need to convert your form inputs to managed components. See docs here

Something like this would be a good start:

class MobForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            vnum: '',
            shortDesc: '',
        };

        this.handleChangeVnum = this.handleChangeVnum.bind(this);
        this.handleChangeShortDesc = this.handleChangeShortDesc.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChangeVnum(event) {
        this.setState({vnum: event.target.value});
    }

    handleChangeShortDesc(event) {
        this.setState({shortDesc: event.target.value});
    }

    handleSubmit(event) {
        this.props.onSubmit(this.state);
        event.preventDefault();
    }

    render() {
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <CreateStringInputField
                        name="vnum"
                        label="vnum:"
                        value={this.state.vnum}
                        onChange={this.handleChangeVnum}
                    />
                    <CreateStringInputField
                        name="shortDesc"
                        label="Short Desc:"
                        value={this.state.shortDesc}
                        onChange={this.handleChangeShortDesc}
                    />
                    <input type="submit" value="Submit" />
                </form>
                {console.log(this.state)}
            </div>
        );
    }
}

And update CreateStringInputField()

function CreateStringInputField(props) {
    return (
        <div name="row">
            <label>
                <b>{props.label}</b>
                <br />
                <input
                    type="text"
                    name={props.name}
                    label={props.label}
                    value={props.value}
                    onChange={props.onChange}
                />
            </label>
        </div>
    );
}
adam_th
  • 397
  • 3
  • 11
  • That generates an error in App.handleSubmit() on the line `alert("A name was submitted: " + this.state.vnum + " event value: " + event.statement.vnum);` -- the error is `Cannot read property 'vnum' of undefined` – cftools Jul 12 '19 at 00:56
  • Have a look at the value of `event`. It looks like it doesn't have a property called `statement`. You also have not defined `vnum` on `state`. – adam_th Jul 12 '19 at 00:59
  • You are right, event doesn't have `.statement`. However it also doesn't seem to hold any values. I have tried event.vnum, event.name, etc. How can I properly extract these values in handleSubmit()? – cftools Jul 12 '19 at 01:06
  • Try `console.log(event)` to see what `onSubmit` is returning – adam_th Jul 12 '19 at 01:12
  • I suspect that what you are trying to do will require a little bit more work. The react docs explain how to achieve what you want quite well [here](https://reactjs.org/docs/forms.html) but I think it's beyond the scope of your initial question. – adam_th Jul 12 '19 at 01:15
  • I have been referencing that document trying to debug my problem, to no luck so far. The end of the "I expect to see {console.log("parsed mob vnum: " + this.state.mob.vnum)} print out the vnum as entered by a user." which was meant to be what I was asking for help on. Do you think that is still out of scope? – cftools Jul 12 '19 at 01:19
  • Thank you, I want to upvote your answer but I do not have enough reputation yet. However, the solution you provided is what I had before deciding I wanted to pass `Mob` as a prop to `MobForm`. I do not believe your solution allows for a parent component to manage 'Mob'. Please correct me if I'm wrong. – cftools Jul 12 '19 at 03:31
0

With React you won't need to work with plain classes. Instead, the class extends a provided React component (Component or PureComponent) or if you don't need state, then'll use plain functions that just return some JSX.

Working example: https://codesandbox.io/s/simple-form-kdh3w


index.js

import React from "react";
import { render } from "react-dom";
import MobForm from "./components/MobForm";

// simple function that returns "MobForm" and it gets rendered by ReactDOM
function App() {
  return <MobForm />;
}

// applies "App" to a <div id="root"></div> in the public/index.html file
render(<App />, document.getElementById("root"));

components/MobForm/index.js (stateful parent component)

import React, { Component } from "react";
import Form from "../Form";

const initialState = {
  vnum: "",
  shortDesc: ""
};

// a stateful parent that manages child state
class MobForm extends Component {
  constructor(props) {
    super(props);
    this.state = initialState;

    // since the class fields are normal functions, they'll lose context
    // of "this" when called as a callback. therefore, they'll need 
    // to be bound to "this" -- via bind, "this" is now referring to 
    // the Class, instead of the global window's "this")
    this.handleChange = this.handleChange.bind(this);
    this.handleReset = this.handleReset.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  // a reusable class field that stores an input's value via its "name"
  // for example: [vnum]: "12345", [shortDesc]: "A number"
  // using object destructuring for shorter syntax:
  // [event.target.name]: event.target.value
  handleChange({ target: { name, value } }) {
    this.setState({ [name]: value });
  }

  // a class field to reset state
  handleReset() {
    this.setState(initialState);
  }

  // a class field to "submit" the form and alert what's currently in state
  handleSubmit(event) {
    // preventDefault prevents page refreshes
    event.preventDefault(); 

    // JSON.stringify allows you to print the contents of an object
    // otherwise, you'll just see [object Object]
    alert(JSON.stringify(this.state, null, 4)); 

    // clears state after submitting form
    this.handleReset(); 
  }

  render() {
    return (
      // passing down state via the spread operator, shorthand for 
      // "vnum={this.state.vum}" and "shortDesc={this.state.shortDesc}",
      // as well as, passing down the class fields from above
      <Form
        {...this.state}
        handleChange={this.handleChange}
        handleReset={this.handleReset}
        handleSubmit={this.handleSubmit}
      />
    );
  }
}

export default MobForm;

components/Form/index.js (a child function that returns some form JSX)

import React from "react";
import PropTypes from "prop-types";
import Input from "../Input";

// using object destructuring to pull out the MobForm's passed down
// state and fields. shorthand for using one parameter named "props" 
// and using dot notation: "props.handleChange", "props.handleReset", etc
function Form({ handleChange, handleReset, handleSubmit, shortDesc, vnum }) {
  return (
    <form style={{ width: 200, margin: "0 auto" }} onSubmit={handleSubmit}>
      <Input name="vnum" label="vnum:" value={vnum} onChange={handleChange} />
      <Input
        name="shortDesc"
        label="Short Desc:"
        value={shortDesc}
        onChange={handleChange}
      />
      <button type="button" onClick={handleReset}>
        Reset
      </button>{" "}
      <button type="submit">Submit</button>
    </form>
  );
}

// utilizing "PropTypes" to ensure that passed down props match 
// the definitions below
Form.propTypes = {
  handleChange: PropTypes.func.isRequired,
  handleReset: PropTypes.func.isRequired,
  handleSubmit: PropTypes.func.isRequired,
  shortDesc: PropTypes.string,
  vnum: PropTypes.string
};

export default Form;

components/Input/index.js (a reuseable input function)

import React from "react";
import PropTypes from "prop-types";

// once again, using object destructuring to pull out the Form's 
// passed down state and class fields. 
function Input({ label, name, value, onChange }) {
  return (
    <div name="row">
      <label>
        <b>{label}</b>
        <br />
        <input
          type="text"
          name={name}
          label={label}
          value={value}
          onChange={onChange}
        />
      </label>
    </div>
  );
}

// utilizing "PropTypes" to ensure that passed down props match 
// the definitions below
Input.propTypes = {
  label: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
  value: PropTypes.string,
  onChange: PropTypes.func.isRequired
};

export default Input;
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51
  • This is a very helpful answer. Ultimately, my goal with this this project is to take the form input and spit it out in a specific format, which would probably be handled by a new component. I was planning on using Mob as the object to store data from MobForm and pass into this new component which would then have a render() that formats the fields as I'd like. If you could describe how your solution can be adapted to this, I will gladly accept your answer. – cftools Jul 12 '19 at 04:55
  • There are two ways to approach this: 1. Use redux, which is a high-level state container that can share `state` across components (which I don't recommend) or 2. Normalize/validate the field on input change and on form submission. Here's an example of normalizing an input onChange and validating it in onSubmit: https://stackoverflow.com/questions/55988065/react-how-to-format-phone-number-as-user-types/55989037#55989037. For input normalization, you may want use separate `handleChange` functions OR build a function that checks which field `name` was changed and normalize accordingly. – Matt Carlotta Jul 12 '19 at 14:17
  • That said, if you're just referring to persisting these fields on the client-side and displaying the values with some styles, then you can just utilize `localStorage`: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage – Matt Carlotta Jul 12 '19 at 14:28
  • If you don't want to use storage, then yet another possibility is conditional rendering: https://reactjs.org/docs/conditional-rendering.html. For example, using `state` to toggle between the `Form` and a `Review` component: https://codesandbox.io/s/simple-form-3k301 (fill out the form and hit submit) – Matt Carlotta Jul 12 '19 at 14:59
  • I have read your sources and come up with a solution that solves the problem as I described it with the desired solution. I am curious about your opinion on the solution. I have posted it as an answer. – cftools Jul 12 '19 at 15:57
0

I was able to get my desired behavior by passing a function to MobForm which updates this.state.mob.

App

class App extends React.Component {
    state = {
        mob: new Mob("", "")
    };

    updateMob = newMob => {
        this.setState({
            mob: newMob
        });
    };

    render() {
        return (
            <div>
                <MobForm mob={this.state.mob} onSubmit={this.updateMob} />
            </div>
        );
    }
}

I then made MobForm maintain vnum, shortDesc state that I could use in my onChange()

MobForm

    state = { vnum: "", shortDesc: "" };

    handleSubmit = event => {
        event.preventDefault();
        const mob = new Mob(this.state.vnum, this.state.shortDesc);
        this.props.onSubmit(mob);
    };

        render() {
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <CreateStringInputField
                        name="vnum"
                        value={this.state.vnum}
                        onChange={event => this.setState({ vnum: event.target.value })}
                    />
                    <CreateStringInputField
                        name="short desc"
                        value={this.state.shortDesc}
                        onChange={event => this.setState({ shortDesc: event.target.value })}
                    />
                    <input type="submit" value="Submit" />
                </form>
            </div>
        );
    }
}
cftools
  • 23
  • 4
  • I'm not sure why you're storing your state into a class. With the current implementation, I don't see any benefit. If you want the fields to be stored as an object, then you can simply utilize an object: https://codesandbox.io/s/simple-form-czfnr – Matt Carlotta Jul 12 '19 at 17:05