Skip to content

Latest commit

 

History

History
429 lines (330 loc) · 13.5 KB

updatingFormState.md

File metadata and controls

429 lines (330 loc) · 13.5 KB

Updating form state

The standard change handler

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.

Standard change handler callback

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.

Introduction to the FieldState API

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.

Validation and the FieldState API

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;
  }
}

Introduction to the UnitOfWork API

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.

UnitOfWork.createModel

Transforms

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';
    // ...
  }
}

Controlling the call to setState

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
  }
}

Retrieving an invalid model

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);

Model output depends on rendered inputs

This was already covered here

revalidateOnSubmit

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.

End of walkthrough

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.