Skip to content

Instantly share code, notes, and snippets.

@jordangarcia
Created August 6, 2018 23:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jordangarcia/527ab9a5c9f807a772dfd80a5dbb3c79 to your computer and use it in GitHub Desktop.
Save jordangarcia/527ab9a5c9f807a772dfd80a5dbb3c79 to your computer and use it in GitHub Desktop.

React Form Abstraction

What problems are we trying to solve?

  1. There is not a generic solution for handling dirty state checking and reverts throughout our codebase.

  2. Validation is a mess, either it's duplicated as inline validation in the "Input" component and in the "Form" component. Other places it exist in the parent component and passed down through props to the "Input", this means our inputs require the parent component to know and do a ton of validation making them no longer easily portable.

Requirements for a good React Form Abstraction

  1. Generalizes and abstracts form dirty state, and reverts
  2. Allows to define validation once for an input while keeping form inputs portable and easily composeable.
  3. A standardized interface for validation functions and how errors are presented both inline and at the form level.

Simple Example

import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';

import { Input, Button } from 'optimizely-oui'

import SectionModule from '../../section_module/';


@Form({
  // Populates the form HoC with initial state for the entity being edited
  // accessible via `props.formState.clean` and `props.form.state.editing`
  initialStateGetter: SectionModule.getters.currentlyEditingExperiment,
})
class MyForm extends React.Component {
  constructor(props) {
    super(props);
    // global validation functino

    // form {FormInterface} is passed to wrapped component as prop
    const { form } = this.props;
    form.addValidationFn({
      keypath: 'name',

      getErrors(val) {
        // these should be composable
        let hasError
        if (val.length === 0) {
          return {
            hasError: true,
            details: {
              message: 'Name is required',
            }
          }
        }

        if (val.length > 63) {
          return {
            hasError: true,
            details: {
              message: 'Name must be less than 64 characters',
            }
          }
        }

        return {
          hasError: false,
          details: {},
        }
      },
    })
  }

  handleSave = () => {
    const { form } = this.props;
    // this call will populate errors and pass them through props to the child components
    // Question: should this return something?
    form.validateAll();
    if (!form.isFormValid()) {
      // you can also use this display global errors
      return;
    }
    const data = form.getValue().toJS();
    // do save operation with data
  }

  render() {
    const {
      form,
      formState,
    } = this.props;

    const nameField = form.field('name');

    return (
      <form>
        <p>
          is dirty:
          <strong>
            { formState.isDirty ? 'true': 'false' }
          </strong>
        </p>

        <Input
          value={ nameField.getValue() }
          onChange={ (e) => nameField.setValue(e.target.value) }
          errors={ nameField.getErrors() }
        />

        <div>
          <Button
            isDisabled={ !formState.isDirty }
            onClick={ form.revert }>
            Revert
          </Button>
          <Button
            onClick={ this.handleSave }>
            Save
          </Button>
        </div>
      </form>
    )
  }
}

Architecture

 +------------------------+
 |        Form HoC        |
 +-------+--------+-------+
         |        |
         +        +
   `form` api   `formState`

+--------------------------+
|                          |
|   Wrapped components     |
|   `props.form`           |
|   `props.formState`      |
+------------+-------------+
             |
             |
             |
             v
         FormInput

Form HoC

  • Receives a getter for initial form state
  • Passes form and formState down as props
  • exposes functionality such as revert, getValue(), setValue(val)

FormInput

  • Any component with the following interface
<FormInput
  form={{
    setValue,
    getValue,
    getErrors,
    addValidationFn,
  }}
 />
 // or for convenience

<FormInput
  form={ props.form.field('name') }
/>

API Reference

FormInterface

Passes as a props.form to the wrapped component of the Form HoC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment