ffxsam
ffxsam

Reputation: 27713

React - How to get a form to communicate with its children (form inputs)

My concept is to make a Form that contains FormElements, and if any required FormElements are not filled, their owner, Form, will know about it. As I'm new to React, I'm having trouble wrapping my head around how to get them to communicate. This is a bit of what I have so far:

Form = React.createClass({
  getInitialState() {
    return {
      validated: false
    }
  },

  render() {
    return (
      <form id={this.props.id}>
        {this.props.children}
      </form>
    )
  },

  componentDidMount() {
    React.Children.forEach(this.props.children, function (el) {
      if (el.props.type === 'submit') {
        console.log(el);
        $(el).prop('disabled', true);
      }
    });
  }
});

FormElement = React.createClass({
  propTypes: {
    id: React.PropTypes.string.isRequired,
    label: React.PropTypes.string.isRequired,
    type: React.PropTypes.string,
    required: React.PropTypes.bool
  },

  getDefaultProps() {
    return {
      type: 'text',
      required: false
    }
  },

  getInitialState() {
    return {
      focused: false,
      filled: false,
      touched: false
    }
  },

  handleFocus(focused) {
    this.setState({focused, touched: true});
  },

  handleKeyUp(event) {
    this.setState({filled: event.target.value.length > 0});
  },

  render() {
    let formElement;

    if (_.contains(['text', 'email', 'password'], this.props.type)) {
      formElement = (
        <div className="form-group">
          <label className={this.state.focused || this.state.filled ? "focused" : ""}
                 htmlFor={this.props.id}>{this.props.label}</label>
          <input type={this.props.type}
                 className="form-control"
                 id={this.props.id}
                 onFocus={this.handleFocus.bind(null, true)}
                 onBlur={this.handleFocus.bind(null, false)}
                 onKeyUp={this.handleKeyUp} />
        </div>
      );
    } else if (this.props.type === 'submit') {
      formElement = (
        <button type="submit"
                ref="submitButton"
                className="btn btn-primary">{this.props.label}</button>
      );
    }

    return formElement;
  }
});

And then the form is used as such:

  <Form id="login-form">
    <FormElement id="email" label="Email Address" type="email" required={true} />
    <FormElement id="password" label="Password" type="password" required={true} />
    <FormElement id="login-button" label="Log In" type="submit" />
  </Form>

You can see there's a React.Children.forEach loop in Form where I'm trying to see if the submit button is there, and disable it by default, but I'm not sure how to operate on the objects in that loop. The jQuery call does nothing. This might also be the totally wrong approach.

The code is not yet complete (as far as validation goes). The idea will be that if the FormElement is required, touched === true and filled === false, then the parent Form will somehow know about that state and keep the submit button disabled.

Upvotes: 1

Views: 1732

Answers (2)

icktoofay
icktoofay

Reputation: 129011

I think you might be trying to put too much logic into the form elements, and when using React, you oughtn’t be trying to touch the DOM, as you are with your loop with some jQuery manipulations in it.

The way I’d do it, the form would have some state associated with it, namely the contents of all its fields. render would pass those values to its FormElements, which would pass them to its inputs. The FormElement would give an onChange to the inputs which delegated to its onChange, which would be in turn provided by Form, which would handle the change and update its state. This gives you the basic functionality of the form.

Since the form now knows about what data is currently filled in, it can directly deduce whether or not the submit button should be enabled or not. It can then pass that as a disabled attribute to the FormElement in its render function, and the FormElement can pass that on to the input.

Simplified example:

<script src="https://fb.me/react-0.13.3.min.js"></script>
<script src="https://fb.me/JSXTransformer-0.13.3.js"></script>
<div id="application"></div>
<script type="text/jsx">
    var Form = React.createClass({
        getInitialState: function getInitialState() {
            return {
                firstName: "",
                lastName: ""
            };
        },
        render: function render() {
            return <form>
                <FormElement label="First name" value={this.state.firstName} onChange={this.firstNameChanged} />
                <FormElement label="Last name" value={this.state.lastName} onChange={this.lastNameChanged} />
                <SubmitButton enabled={!!this.state.firstName.trim() && !!this.state.lastName.trim()} />
            </form>;
        },
        firstNameChanged: function(newFirstName) {
            this.setState({firstName: newFirstName});
        },
        lastNameChanged: function(newLastName) {
            this.setState({lastName: newLastName});
        }
    });
    var FormElement = React.createClass({
        render: function render() {
            return <p><label>{this.props.label}: <input type="text" value={this.value} onChange={this.changed} /></label></p>;
        },
        changed: function changed(e) {
            if(this.props.onChange) {
                this.props.onChange(e.target.value);
            }
        }
    });
    var SubmitButton = React.createClass({
        render: function render() {
            return <p><input type="submit" value="Submit" disabled={!this.props.enabled} /></p>;
        }
    });
    React.render(<Form />, document.getElementById("application"));
</script>

In a comment you said you wanted Form to have no knowledge of any particular fields. Well, you can do that too: store the data in your LoginForm or what-have-you and again just let the data trickle down and offer a way to provide callbacks to the parent, e.g. (here discarding FormElement because these snippets are getting long and I don’t want to detract from the point):

<script src="https://fb.me/react-0.13.3.min.js"></script>
<script src="https://fb.me/JSXTransformer-0.13.3.js"></script>
<div id="application"></div>
<script type="text/jsx">
    var NameForm = React.createClass({
        getInitialState: function getInitialState() {
            return {
                firstName: "",
                lastName: ""
            };
        },
        render: function render() {
            return <Form fields={this.getFields()} onFieldChange={this.fieldChanged} canSubmit={this.canSubmit()} />;
        },
        getFields: function getFields() {
            return [
                {id: "firstName", label: "First name", value: this.state.firstName},
                {id: "lastName", label: "Last name", value: this.state.lastName}
            ];
        },
        fieldChanged: function fieldChanged(which, newValue) {
            if(which === "firstName") {
                this.setState({firstName: newValue});
            }else if(which === "lastName") {
                this.setState({lastName: newValue});
            }
        },
        canSubmit: function canSubmit() {
            return !!this.state.firstName && !!this.state.lastName;
        }
    });
    var Form = React.createClass({
        render: function render() {
            return <form>
                {this.props.fields.map(function(field) {
                    return <p key={field.id}><label>{field.label}: <input type="text" value={field.value} onChange={this.fieldChanged.bind(null, field.id)} /></label></p>;
                }.bind(this))}
                <input type="submit" value="Submit" disabled={!this.props.canSubmit} />
            </form>;
        },
        fieldChanged: function(which, e) {
            if(this.props.onFieldChange) {
                this.props.onFieldChange(which, e.target.value);
            }
        }
    });
    React.render(<NameForm />, document.getElementById("application"));
</script>

Upvotes: 1

gapvision
gapvision

Reputation: 1029

When it comes to communication between React elements, I finally came to Flux as it is designed for that purpose. Basically you create a single state machine (a store) which contains all the logic for validating the user input, any may also have methods for configuring what is necessary/which fields are required. Every field/child of Form registers than at the store, telling it "hey, Im here and I offer this/that information". Finally on submit, the store checks wether all registered/required fields have reported a valid user input

Upvotes: 0

Related Questions