text: /*eslint no-multi-assign: "warn"*/
  /**
      @arg {string} name - form state will appear in this.state[name]
      @arg {object} instance - `this` for the component
      @arg {array} fields - ['username', 'save', ...]
      @arg {object} initialValues required for checkboxes {save: false, ...}
      @arg {function} validation - values => ({ username: ! values.username ? 'Required' : null, ... })
  */
  export default function reactForm({
   name, instance, fields, initialValues, validation = () => {}
  }) {
      if (typeof instance !== 'object') throw new TypeError('instance is a required object');
      if (!Array.isArray(fields)) throw new TypeError('fields is a required array');
      if (typeof initialValues !== 'object') throw new TypeError('initialValues is a required object');
  
      // Give API users access to this.props, this.state, this.etc..
      validation = validation.bind(instance);
  
      const formState = (instance.state = instance.state || {});
      formState[name] = {
          // validate: () => setFormState(instance, fields, validation),
          handleSubmit: (submitCallback) => (event) => {
              event.preventDefault();
              const { valid } = setFormState(name, instance, fields, validation);
              if (!valid) return;
              const data = getData(fields, instance.state);
              let formValid = true;
              const fs = instance.state[name] || {};
              fs.submitting = true;
  
              // User can call this function upon successful submission
              const updateInitialValues = () => {
                  setInitialValuesFromForm(name, instance, fields, initialValues);
                  formState[name].resetForm();
              };
  
              instance.setState({ [name]: fs }, () => {
                  // TODO, support promise ret
                  const ret = submitCallback({ data, event, updateInitialValues }) || {};
                  // Look for field level errors
                  // eslint-disable-next-line no-restricted-syntax
                  for (const fieldName of Object.keys(ret)) {
                      const error = ret[fieldName];
                      if (error) {
                          const value = instance.state[fieldName] || {};
                          value.error = error;
                          value.touched = true;
                          if (error) formValid = false;
                          instance.setState({ [fieldName]: value });
                      }
                  }
                  fs.submitting = false;
                  fs.valid = formValid;
                  instance.setState({ [name]: fs });
              });
          },
          resetForm: () => {
              // eslint-disable-next-line no-restricted-syntax
              for (const field of fields) {
                  const fieldName = n(field);
                  const f = instance.state[fieldName];
                  const def = initialValues[fieldName];
                  f.props.onChange(def);
              }
          },
          clearForm: () => {
              // eslint-disable-next-line no-restricted-syntax
              for (const field of fields) {
                  const fieldName = n(field);
                  const f = instance.state[fieldName];
                  f.props.onChange();
              }
          },
      };
  
      // eslint-disable-next-line no-restricted-syntax
      for (const field of fields) {
          const fieldName = n(field);
          const fieldType = t(field);
          const fs = (formState[fieldName] = {
              value: null,
              error: null,
              touched: false,
          });
  
          // Caution: fs.props is expanded <input {...fieldName.props} />, so only add valid props for the component
          fs.props = { name: fieldName };
  
          {
              const initialValue = initialValues[fieldName];
              if (fieldType === 'checked') {
                  fs.value = toString(initialValue);
                  fs.props.checked = toBoolean(initialValue);
              } else if (fieldType === 'selected') {
                  fs.props.selected = toString(initialValue);
                  fs.value = fs.props.selected;
              } else {
                  fs.props.value = toString(initialValue);
                  fs.value = fs.props.value;
              }
          }
  
          fs.props.onChange = (e) => {
              const value = e && e.target ? e.target.value : e; // API may pass value directly
              const v = { ...(instance.state[fieldName] || {}) };
              const initialValue = initialValues[fieldName];
  
              if (fieldType === 'checked') {
                  v.touched = toString(value) !== toString(initialValue);
                  v.value = v.props.checked = toBoolean(value);
                  v.value = value;
              } else if (fieldType === 'selected') {
                  v.touched = toString(value) !== toString(initialValue);
                  v.value = v.props.selected = toString(value);
              } else {
                  v.touched = toString(value) !== toString(initialValue);
                  v.value = v.props.value = toString(value);
              }
  
              instance.setState({ [fieldName]: v }, () => {
                  setFormState(name, instance, fields, validation);
              });
          };
  
          fs.props.onBlur = () => {
              // Some errors are better shown only after blur === true
              const v = { ...(instance.state[fieldName] || {}) };
              v.blur = true;
              instance.setState({ [fieldName]: v });
          };
      }
  }
  
  function setFormState(name, instance, fields, validation) {
      let formValid = true;
      let formTouched = false;
      const v = validation(getData(fields, instance.state));
      // eslint-disable-next-line no-restricted-syntax
      for (const field of fields) {
          const fieldName = n(field);
          const validate = v[fieldName];
          const error = validate ? validate : null;
          const value = { ...(instance.state[fieldName] || {}) };
          value.error = error;
          formTouched = formTouched || value.touched;
          if (error) formValid = false;
          instance.setState({ [fieldName]: value });
      }
      const fs = { ...(instance.state[name] || {}) };
      fs.valid = formValid;
      fs.touched = formTouched;
      instance.setState({ [name]: fs });
      return fs;
  }
  
  function setInitialValuesFromForm(name, instance, fields, initialValues) {
      const data = getData(fields, instance.state);
      // eslint-disable-next-line no-restricted-syntax
      for (const field of fields) {
          const fieldName = n(field);
          initialValues[fieldName] = data[fieldName];
      }
  }
  
  function getData(fields, state) {
      const data = {};
      // eslint-disable-next-line no-restricted-syntax
      for (const field of fields) {
          const fieldName = n(field);
          data[fieldName] = state[fieldName].value;
      }
      return data;
  }
  
  /*
      @arg {string} field - field:type
      <pre>
          type = checked (for checkbox or radio)
          type = selected (for seelct option)
          type = string
      </pre>
      @return {string} type
  */
  function t(field) {
      const [, type = 'string'] = field.split(':');
      return type;
  }
  
  /**
      @return {string} name
  */
  function n(field) {
      const [name] = field.split(':');
      return name;
  }
  
  const hasValue = (v) => (v == null ? false : (typeof v === 'string' ? v.trim() : v) === '' ? false : true);
  const toString = (v) => (hasValue(v) ? v : '');
  const toBoolean = (v) => (hasValue(v) ? JSON.parse(v) : '');
  