Slow Harry
Slow Harry

Reputation: 1897

React get value deep inside DOM

I am not so familiar with react, I try to build the following abstraction over sign in / forms etc.

Take a look at this:

    var SignUpForm = React.createClass({

    handleSubmit: function(e) {
        e.preventDefault();
        console.log(this.refs.iitu_id.getDOMNode().value.trim())

        // iitu_id = this.refs.iitu_id.getDOMNode().value.trim();
        // password = this.refs.password.getDOMNode().value.trim();

        var error = UserValidator.valid({iitu_id: iitu_id, password: password});
        if (error) {
            this.setState({"errors": error });
            // console.log(error);
        } else {
            // console.log(error);
        }

    },

    getInitialState: function() {
        return {
            'errors': {
                iitu_id: null, 
                password: null
            }
        };
    },

    render: function() {
        return (
            /*jshint ignore:start */
            <form className="form-horizontal" onSubmit={this.handleSubmit} >
                <FormGroup label="iitu id" error_msg={this.state.errors.iitu_id} fieldName="iitu_id" fieldType="text" />
                <FormGroup label="password" error_msg={this.state.errors.password} fieldName="password" fieldType="password" />
                <ButtonGroup text="Войти"/>
            </form>
            /*jshint ignore:end */
        );
    }
});

var FormGroup = React.createClass({

    render: function() {
        var formGroupCss = 'form-group';
        if (this.props.error_msg){
            formGroupCss = 'form-group has-error';
        }

        return (
            /*jshint ignore:start */
            <div className={formGroupCss}>
                <Label fieldName={this.props.fieldName}>{this.props.label}</Label>
                <InputField type={this.props.fieldType}>{this.props.label}</InputField>
                <label className="control-label" for="inputError1">{this.props.error_msg}</label>
            </div>
            /*jshint ignore:end */
        );
    }
});



var ButtonGroup = React.createClass({
    render: function () {
        return (
            /*jshint ignore:start */
            <div className="form-group">
                <div className="col-sm-offset-2 col-sm-10">
                    <button className="btn btn-default">{this.props.text}</button>
                </div>
            </div>
            /*jshint ignore:end */
        );
    }
});

var InputField = React.createClass({
    render: function() {

        return (
            /*jshint ignore:start */
            <div className="col-sm-5">
                <input className="form-control" type={this.props.fieldType} placeholder={this.props.children}/>
            </div>
            /*jshint ignore:end */
        );
    }
});

exports.SignUpForm = SignUpForm;

there is a bit too much code, but I think it is pretty easy to read. The question is how can I get the value of my InputField class when I press the submit button (simply get form value)? There is problem that my input tag deep inside DOM. Additional question is it good to have the following code design I mean describe each logical component as new 'class' ?

Upvotes: 1

Views: 6661

Answers (4)

Jonny Buchanan
Jonny Buchanan

Reputation: 62793

If you give your form inputs name attributes, you can use the form's .elements collection to access its inputs.

I recently split the code newforms uses for this out into a reusable get-form-data module, which would let you do this assuming your inputs have name attributes:

var getFormData = require('get-form-data'); // Or just use the browser bundle

var SignUpForm = React.createClass({
  handleSubmit: function(e) {
    e.preventDefault();
    var data = getFormData(e.target, {trim: true});
    var error = UserValidator.valid(data);
    if (error) {
      this.setState({errors: error});
    } else {
      // ...
    }
  },
  // ...

Or, if you need to get input as it's given, you can add an onChange handler to the <form> or some other component which contains all the form inputs, instead of having to do each one individually:

  handleChange: function(e) {
    var form = e.target.form;
    var name = e.target.name;
    var data = getFormData.getNamedFormElementData(form, name, {trim: true});
    // ...
  },

  render: function() {
    return <form onSubmit={this.handleSubmit} onChange={this.handleChange}>
      {/* ... */}
    </form>
  }
});

Upvotes: 7

yonatanmn
yonatanmn

Reputation: 1600

Concerning your second question - it is a bad approach to add a component for every logical element. In your example - all of the components, (besides the first) has no logic, only 'render'. this design adds many useless lines of code and "this.props" repetitions. In your example, I would have just add everything to the render of the first component.

Now, let say you have two components, and want to reach the child from the parent:

var SignUpForm = React.createClass({

  handleSubmit: function(e) {
    e.preventDefault();

    //now - reach your inner comp with refs
    console.log(this.refs.inner.refs.username.getDOMNode().value.trim());      
  },

  render: function() {
    return (
       //and pass the submit function as a callback
       <InnerForm ref="inner" submit={this.props.handleSubmit}/>           
    );
  }
});

var InnerForm = React.createClass({

  render: function() {
    return (
       <form>  
           <input type="text" ref="username"/>
           <input type="submit" onClick={this.props.submit} >
       </form>           
    );
  }
});

In your example there was to many components inside components to do what I did. Still, there are cases when you need long linage of child components. Using this.refs.a.refs.b.refs.c etc - might be pretty ugly, and also prevents re-usability.

in those cases try using any MVC architecture (I'm using reflux and loving it)

Upvotes: 1

Miles
Miles

Reputation: 283

In the absence of a framework like Angular or Flux, shoot data between your components using callbacks.

The technique used here is explained in detail on React's website. Another solution, however, is to use a form library. I took a similar route and tried to build form components from scratch -- it works, but it's a path others have already cleared. Check out newforms and react-forms.

Note that the code I shared is untested, though it should be very close to working.

var SignUpForm = React.createClass({

    handleSubmit: function(e) {
        e.preventDefault();

        console.log(this.iitu_id + ' and ' + this.password + ' should work.');

        var error = UserValidator.valid({iitu_id: iitu_id, password: password});
        if (error) {
            this.setState({"errors": error });
            // console.log(error);
        } else {
            // console.log(error);
        }

    },

    getInitialState: function() {
        return {
            'errors': {
                iitu_id: null, 
                password: null
            }
        };
    },

    updateForm: function(field, value) {
        /*  Or do something like this using underscore.js:

                var form = _.clone(this.form);
                form[field] = value;
                this.setState({ form: form });

            This approach let's you grab the form object on
            submit instead of hunting for the form values
            in SignUpForm's state.
        */

        this.setState({ field: value });
    },

    render: function() {
        return (
            /*jshint ignore:start */
            <form className="form-horizontal" onSubmit={this.handleSubmit} >
                <FormGroup update={this.updateForm} label="iitu id" error_msg={this.state.errors.iitu_id} fieldName="iitu_id" fieldType="text" />
                <FormGroup label="password" error_msg={this.state.errors.password} fieldName="password" fieldType="password" />
                <ButtonGroup text="Войти"/>
            </form>
            /*jshint ignore:end */
        );
    }
});

var FormGroup = React.createClass({

    handleChange: function(event) {
        this.props.update(this.props.fieldName, event.target.value);
    },

    render: function() {
        var formGroupCss = 'form-group';
        if (this.props.error_msg){
            formGroupCss = 'form-group has-error';
        }

        return (
            /*jshint ignore:start */
            <div className={formGroupCss}>
                <Label fieldName={this.props.fieldName}>{this.props.label}</Label>
                <InputField handleChange={this.handleChange} type={this.props.fieldType}>{this.props.label}</InputField>
                <label className="control-label" for="inputError1">{this.props.error_msg}</label>
            </div>
            /*jshint ignore:end */
        );
    }
});


var ButtonGroup = React.createClass({
    render: function () {
        return (
            /*jshint ignore:start */
            <div className="form-group">
                <div className="col-sm-offset-2 col-sm-10">
                    <button className="btn btn-default">{this.props.text}</button>
                </div>
            </div>
            /*jshint ignore:end */
        );
    }
});

var InputField = React.createClass({
    render: function() {

        return (
            /*jshint ignore:start */
            <div className="col-sm-5">
                <input onChange={this.props.handleChange} className="form-control" type={this.props.fieldType} placeholder={this.props.children}/>
            </div>
            /*jshint ignore:end */
        );
    }
});

exports.SignUpForm = SignUpForm;

Specifically, take note of the callback functions being passed from parent to child.

  1. SignUpForm gives updateForm to FormGroup
  2. FormGroup gives handleChange to InputField

Then to get the data back to the top, you just reverse the process.

  1. When <input/> changes, it runs handleChange
  2. When handleChange runs, it passes the FormGroup's fieldName and value to SignUpForm
  3. SignUpForm updates its state with the new field value

Upvotes: 2

Ganonside
Ganonside

Reputation: 1319

I would suggest an onChange event listener to your inputs that use callbacks to relay the information all the way back up to your top level component.

var SignUpForm = React.createClass({
  getInitialState: function() {
    return {
      inputs: {
        id: '',
        password: ''
      }
    };
  },
  render: function() {
    return (
      <form className="form-horizontal" onSubmit={this.handleSubmit} >
        <FormGroup ... callback={this.handleIdChange} />
        <FormGroup ... callback={this.handlePasswordChange} />
        <ButtonGroup text="Войти"/>
      </form>
    );
  },
  handleIdChange: function(newId) {
    this.setState({
      inputs: {
        id: newId.target.value,
        password: this.state.inputs.password
      }
    });
  },
  handlePasswordChange: function(newPassword) { ... }
});

then pass that callback down to the base level form fields and use them for handling the onChange event.

var InputField = React.createClass({
  render: function() {

    return (
      /*jshint ignore:start */
      <div className="col-sm-5">
        <input ... onChange={this.props.callback}/>
      </div>
      /*jshint ignore:end */
    );
  }
});

then with your handleSubmit just output the values in the this.state.inputs object.

Upvotes: 1

Related Questions