Reputation: 27713
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
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 FormElement
s, which would pass them to its input
s. The FormElement
would give an onChange
to the input
s 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
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