If you provide a 'formField' prop to an element nested within a react-formstate 'Form' element, react-formstate considers it an input meant to capture a value and generates additional props for the element.
The 'handleValueChange' prop is of particular importance.
const RfsInput = ({fieldState, handleValueChange, ...other}) => {
return (
<Input
value={fieldState.getValue()}
help={fieldState.getMessage()}
onChange={e => handleValueChange(e.target.value)}
{...other}
/>
);
};
render() {
// A standard change handler generated by react-formstate is
// provided to both the name input and the address.city input.
// The generated handler prop is named 'handleValueChange'
return (
<Form formState={this.formState} onSubmit={this.handleSubmit}>
<RfsInput formField='name' label='Name' required/>
<RfsInput formField='address.city' label='Address City' required/>
<input type='submit' value='Submit'/>
</Form>
);
}
The 'handleValueChange' prop represents the standard change handler. Using the standard handler will normally save you time and effort, but you can always override it if necessary.
To demonstrate, let's build a custom handler and pass it to the 'name' input.
export default class SimpleRfsForm extends Component {
constructor(props) {
//...
this.handleNameChange = this.handleNameChange.bind(this);
}
render() {
return (
<Form formState={this.formState} onSubmit={this.handleSubmit}>
<RfsInput formField='name' label='Name' handleValueChange={this.handleNameChange}/>
<RfsInput formField='address.city' label='Address City'/>
<input type='submit' value='Submit'/>
</Form>
);
}
// override the standard change handler with essentially the standard change handler
handleNameChange(newName) {
const context = this.formState.createUnitOfWork();
const fieldState = context.getFieldState('name');
fieldState.setValue(newName).validate();
context.updateFormState();
}
// ...
}
There are a couple new APIs used in the handler: UnitOfWork and FieldState.
The UnitOfWork API is a simple wrapper around calls to this.setState. It is complementary to the FormState API, which is a simple wrapper around initializing and reading this.state. The main focus of both APIs is essentially to transform data written to, and read from, component state.
As for the FieldState API, from react-formstate's perspective, the "form state" is essentially a collection of individual "field states."
To illustrate, let's look behind the scenes at what the change handler actually does (there is nothing magical happening). Suppose name, a required field, is updated to an empty string. The call to context.updateFormState() then makes a call to this.setState like this:
this.setState(
{
'formState.name':
{
value: '', // empty string
validity: 2, // invalid
message: 'Name is required'
}
}
);
and that's the crux of react-formstate. It's simple, really.
Sophisticated user experiences sometimes require updating form state whenever any input is updated.
It might be handy, then, to be aware of the existence of the 'onUpdate' callback from the standard change handler. (The custom handler above is more or less the implementation of the standard handler, but not entirely.)
An advanced example of using the 'onUpdate' callback is provided here.
If you retrieve a FieldState instance from the FormState API, the instance is read-only. If you retrieve a FieldState instance from the UnitOfWork API, the instance is read/write.
With a read-only field state, most of the time you are only interested in the field's underlying value. We've already seen shortcuts to retrieve this value:
this.formState.get('address.city'); // is shorthand for:
this.formState.getFieldState('address.city').getValue();
this.formState.getu('address.city'); // is shorthand for:
this.formState.getFieldState('address.city').getUncoercedValue();
There are also shortcuts for setting a value. The custom handler could be rewritten as:
handleNameChange(newName) {
const context = this.formState.createUnitOfWork();
context.set('name', newName).validate();
context.updateFormState();
}
As for the 'validate' method, if, for example, you have an input specified as:
<RfsInput formField='name' label='Name' validate={this.validateName}/>
the validate method will call the 'validateName' method and apply the results accordingly.
You can use the FieldState API to assist with validation. For instance, sometimes it is useful to store miscellaneous data as part of field state:
<Input
formField='password'
label='Password'
required
validate={this.validatePassword}
handleValueChange={this.handlePasswordChange}
/>
//
// demonstrate the FieldState API
//
validatePassword(newPassword) {
if (newPassword.length < 8) {
return 'Password must be at least 8 characters';
}
}
handlePasswordChange(newPassword) {
const context = this.formState.createUnitOfWork(),
fieldState = context.set('password', newPassword);
context.set('passwordConfirmation', ''); // clear the confirmation field.
// Validation should normally be performed in dedicated validation blocks.
// Required field validation, in particular, should NEVER be coded into a change handler.
fieldState.validate(); // perform regular validation, including required field validation
if (fieldState.isInvalid()) {
context.updateFormState();
return;
} // else
// Validation that simply warns the user is okay in a change handler.
if (newPassword.length < 12) {
fieldState.setValid('Passwords are ideally at least 12 characters');
fieldState.set('warn', true);
}
context.updateFormState();
}
if (fieldState.get('warn')) {
// ...
}
Note the 'validate' method can also call validation specified via the fluent API. For instance, the above example can be shortened to:
<Input
formField='password'
label='Password'
required
fsv={v => v.minLength(8).msg('Password must be at least 8 characters')}
handleValueChange={this.handlePasswordChange}
/>
handlePasswordChange(newPassword) {
const context = this.formState.createUnitOfWork();
const fieldState = context.set('password', newPassword).validate();
context.set('passwordConfirmation', ''); // clear the confirmation field.
if (fieldState.isValid() && newPassword.length < 12) {
fieldState.setValid('Passwords are ideally at least 12 characters');
fieldState.set('warn', true);
}
context.updateFormState();
}
To guard against an invalid model injected into form state, it is best practice to put all normal, synchronous validation into dedicated validation blocks, since a change handler might never be called. Required field validation, in particular, never makes sense in a change handler.
Although validation that simply warns the user is okay in a change handler, the example could be reworked as:
<Input
formField='password'
label='Password'
required
validate={this.validatePassword}
handleValueChange={this.handlePasswordChange}
/>
validatePassword(newPassword, context) {
if (newPassword.length < 8) {
return 'Password must be at least 8 characters';
}
if (newPassword.length < 12) {
const fieldState = context.getFieldState('password');
fieldState.setValid('Passwords are ideally at least 12 characters');
fieldState.set('warn', true);
}
}
handlePasswordChange(newPassword) {
const context = this.formState.createUnitOfWork();
context.set('password', newPassword).validate();
context.set('passwordConfirmation', ''); // clear the confirmation field.
context.updateFormState();
}
Finally, the validation block could be reworked as:
validatePassword(newPassword, context) {
const fieldState = context.getFieldState('password');
if (newPassword.length < 8) {
fieldState.setInvalid('Password must be at least 8 characters');
return;
}
if (newPassword.length < 12) {
fieldState.setValid('Passwords are ideally at least 12 characters');
fieldState.set('warn', true);
return;
}
}
We've already seen examples for using the following methods from the UnitOfWork API: 'getFieldState', 'get', 'getu', 'set', 'injectModel', 'injectField', 'updateFormState', and 'createModel'.
A reminder that the 'updateFormState' method can receive additional updates to provide to the call to setState:
context.updateFormState({someFlag: true, someOtherStateValue: 1});
// ...
if (this.state.someFlag)
// ...
You can also use the 'getUpdates' method to prepare a call to setState:
// upateFormState is best practice, but you can also use getUpdates:
this.setState(Object.assign(context.getUpdates(), {someFlag: true, someOtherStateValue: 1}));
The 'createModel' method is worthy of its own section.
To save you effort, the 'createModel' method can perform a few common transforms for you:
<RfsInput formField='age' intConvert/>
<RfsInput formField='address.line2' preferNull/>
<RfsInput formField='specialText' noTrim/>
handleSubmit(e) {
e.preventDefault();
const model = this.formState.createUnitOfWork().createModel();
if (model) {
model.age === 8; // rather than '8' due to intConvert prop
model.address.line2 === null; // rather than '' due to preferNull prop
model.specialText === ' not trimmed '; // rather than 'not trimmed' due to noTrim prop
}
}
Of course, you can do your own transforms too:
handleSubmit(e) {
e.preventDefault();
const model = this.formState.createUnitOfWork().createModel();
if (model) {
model.active = !model.disabled;
model.someFlag = model.someRadioButtonValue === '1';
// ...
}
}
We've seen 'createModel' used like this:
handleSubmit(e) {
e.preventDefault();
const model = this.formState.createUnitOfWork().createModel();
if (model) {
alert(JSON.stringify(model)); // submit to your api or store or whatever
}
}
but you can control the call to setState by passing true to 'createModel':
handleSubmit(e) {
e.preventDefault();
const context = this.formState.createUnitOfWork();
const model = context.createModel(true); // <--- pass true
if (model) {
alert(JSON.stringify(model)); // submit to your api or store or whatever
} else {
// do additional work...
context.updateFormState(withAdditionalUpdates); // <--- need to call this yourself now
}
}
If you want to retrieve the current model regardless of whether it's valid, use the 'createModelResult' method:
// This will only work after the initial render.
// During the initial render you can use your initial model,
// or you can delay injection until componentDidMount.
// This will never call setState.
const result = context.createModelResult();
console.log(result.isValid);
console.log(result.model);
// Passing no options is the same as:
const options = {doTransforms: false, markSubmitted: false};
const result = context.createModelResult(options);
This was already covered here
If validation is specified for a form field, and the validation hasn't run, createModel performs the validation before generating the model. However, if the field has already been validated, createModel does not bother to revalidate.
This might be different from what you are used to, but it is entirely consistent with react-formstate's approach, and it should be able to gracefully handle most anything you throw at it, including asynchronous validation.
If you find a need for react-formstate to revalidate a particular field during createModel you could use the 'revalidateOnSubmit' property:
<RfsInput
formField='confirmNewPassword'
type='password'
label='Confirm New Password'
required
validate={this.validateConfirmNewPassword}
revalidateOnSubmit
/>
but consider that between a custom change handler, or the onUpdate callback from the standard handler, there is likely a better solution.
For instance, in the case of a password confirmation:
handlePasswordChange(newPassword) {
const context = this.formState.createUnitOfWork();
context.set('newPassword', newPassword).validate();
context.set('confirmNewPassword', ''); // <--- clear the confirmation field
context.updateFormState();
}
If you find yourself wanting to use revalidateOnSubmit, or wanting to perform additional model-wide validation directly in the onSubmit handler, think hard on whether react-formstate doesn't already provide a better way to solve your problem.
There is a lot more to react-formstate, but this concludes the walkthrough. If it was successful, you should now have a basic understanding of how to make react-formstate work for your purposes. Remaining features are covered through specific examples and documentation.
- Put it all together with a basic example
- Review the advantages of react-formstate.
- Return to the front page