cerner / terra-framework

Terra framework houses composed and higher order react components to help developers quickly build out new applications.
http://terra-ui.com
Apache License 2.0
67 stars 71 forks source link

Form Validation Utilities #9

Closed JakeLaCombe closed 6 years ago

JakeLaCombe commented 6 years ago

Issue Description

Terra Framework needs to have a way to deal with form-validations. This will most likely be an external package that we recommend to consumers to use, but we need to have a common strategy that we can send to developers.

Issue Type

JakeLaCombe commented 6 years ago

Initial Spike

We have several different options we can implement for doing our form validations.

Standard Validity Apis

The first one involves using the standard validity API’s that are provided by most browsers. This essentially involves using a combination of the setValidity apis that are provided. They provide standard feature such as type matching, pattern matching, and event custom errors as well. You can see what an implementation of this would look like here. https://codepen.io/jmalfatto/pen/YGjmaJ

Implementation Details.

If we were going to go with this approach, we would have to add props to the input components that allow consumers to pass in values so that they can interact with setCustomValidity of the input element.

As for reusable validations, we have two options. We can create ValidatableComponents that encapsulate the Field and Input so that the error message can be appropriately passed into the Field. Another approach is similar to to create function that build hashes of error messages that can be passed from individual react components. For an example of this, see this link (https://github.com/skaterdav85/validatorjs).

React Redux Form

https://davidkpiano.github.io/react-redux-form/

React-Redux form has an approach that involves attaching individual validators to the inputs, and then having an errors container that displays the results of the error messages. The errors container isn't required, as the individual forms will contain valid and error states for their individual inputs. That could be used to fill out the error prop of the individual fields that terra provides for form building.

Implementation Details

React-Redux contains methods for building HOC components out of their APIs. This allows us to wrap our field and input elements together, and then apply a wrapper that passes in the props from react-redux form. This allows us to also write validations that are easily reusable across several forms.

While React-Redux provides lots of form validation possibilities, this would require Redux of consuming apps. Redux has a bit of a learning curve to implement, and may seem overkill for applications that are small. This could prove as a barrier for consuming teams. In addition, it's not considered best practice to store non global data inside an application store, which teams would have to do in order to consume this component.

Redux Form

https://redux-form.com/

Redux Form has validation mechanisms as well. They have a variety of different validation techniques that we can leverage, such as validation on form submit (https://redux-form.com/7.2.0/examples/simple/), field level validation (https://redux-form.com/7.1.2/examples/fieldlevelvalidation/), and validation following a backend form submit (https://redux-form.com/7.1.2/examples/submitvalidation/). This also has HOC support for incorporating terra components as well (https://redux-form.com/7.2.0/examples/material-ui/).

Implementation Details

The same things done for React-Redux would have to be done here as well. In addition, this would require consuming teams to have to use Redux in their applications.

React Validation

https://github.com/Lesha-spr/react-validation

React Validation allows us to have an API that can attach validators to individual form inputs. The handleSubmit function passed into the form would only be triggered when all of the validations passed. This integrates with terra components as well. HOC wrappers are provided that allow us to combine fields and inputs so that we can automatically display the error message when it occurs. You can see an implementation of it here. https://gist.github.com/JakeLaCombe/f11bd3ed998cd890ab9c065606c59a4f

Implementation Details

For Terra to use this, we would need to wrap our field and input elements inside one of the HOC wrappers react-validation provides. In addition, custom validations would need to be written on our side for any validations we wish to support.

Custom Validation approach on our side.

https://spin.atomicobject.com/2016/10/05/form-validation-react/ provides an elegant way for writing form validations. This requires the use of forcing controlled inputs on the user, and then passing the inputs to a function that validates the input based on an array of validations. The implementation of this would look like the following. https://gist.github.com/JakeLaCombe/e9642062bd66fa9de281983d91c887bf

JakeLaCombe commented 6 years ago

My recommendation is the following

saedar commented 6 years ago

+1 to @JakeLaCombe's recommendation.

mjhenkes commented 6 years ago

+1 to @JakeLaCombe's recommendation as well.

bjankord commented 6 years ago

+1 to @JakeLaCombe's recommendation.

nickpith commented 6 years ago

I'm a little concerned about rolling our own validation library and the maintenance of that. Are we prepared to handle all the different validation situations necessary?

I would suggest evaluating something like https://github.com/final-form/react-final-form that has no external dependencies and figuring out how it or something similar can just be incorporated into our existing Field components to alleviate the wrapping concern. Although I think we could talk thru the wrapping if that is undesirable.

dcmendez commented 6 years ago

If teams utilizing terra's input components want client side form validation for Q2, is the guidance that we not implement our own validation mechanism and instead wait on enhancements to said components?

JakeLaCombe commented 6 years ago

So I'm including another evaluation of react-final-form in this tech design. I look through it, and we can use it with our existing terra form components.

JakeLaCombe commented 6 years ago

React Final Form Validation

react-final-form is a form validation library that provides hooks for various form validations. Provided hooks include validating on the form, validating on an individual form field, validating on blur, validating synchronously, and validating asynchronously. This package provides several other features as well for registering fields and other validation components.

Usage with Terra.

Final Form is essentially a form wrapper that takes in validation and submission hooks, and attaches them to a provided form. Setting up a form with this component and Terra is relatively simple.

Example Validation

const required = value => (value ? undefined : 'Required')
import React from 'react';
import ReactDOM from 'react-dom';
import I18n from 'terra-i18n';
import { Form, Field } from 'react-final-form';
import TerraField from 'terra-form-field';
import Input from 'terra-form-input';

renderForm({ handleSubmit, reset, submitting, pristine, values }) {
    return (
      <form 
        onSubmit={handleSubmit}
      >
        <h1>Hello People!</h1>
        <Field
          name="description"
          validation={required}
        >
          {({ input, meta, placeholder, ...rest }) => (
            <TerraField
              {...rest}
              label="Description"
              error={meta.submitError}
              isInvalid={meta.submitFailed}
              required
            >
              <Input
                {...input}
                placeholder="Description"
                onChange={(e) => {input.onChange(e.target.value);}}
                value={input.value}
              />
            </TerraField>
          )}
        </Field>
        <button type="submit">
          Submit
        </button>
      </form>
    );
  }

  render() {
    return (
      <Form
        onSubmit={this.submitForm}
        render={this.renderForm} />
    );
  }

Essentially, you just need to wrap your form inside terra-final-form, and then wrap your terra fields inside Field provided by react-final-form.

Ask of Terra

To use react-finalf-form with third party components, you essentially need to wrap your terra form components with a react-final-form field. Terra can simplify this process by providing the wrappers with the appropriate arguments like so.

const TextFieldAdapter = ({ input, meta, placeholder, required, ...rest }) => (
  <Field
    {...rest}
    error={meta.submitError}
    isInvalid={meta.submitFailed}
    required={required}
  >
    <Input
      {...input}
      placeholder={placeholder}
      onChange={(e) => {input.onChange(e.target.value);}}
      value={input.value}
    />
  </Field>
);

While this does make using react-final-form easier for Terra consumers, it essentially locks developers into using react-final-form if they wish to have validation with our react components. A better approach would be to re introduce the NumberField and TextField components we previously provided with terra-form. It would create wrappers like these.

const NumberField = ({ 
  error,
  isInvalid,
  required,
  name,
  onChange,
  value,
  defaultValue,
  meta,
  placeholder,
  required,
  inputAttrs,
  ...rest,
}) => (
  <Field
    {...rest}
    error={error}
    isInvalid={isInvalid}
    required={required}
  >
    <Input
      {...inputAttrs}
      type="number"
      placeholder={placeholder}
      onChange={onChange}
      value={value}
      defaultValue={defaultValue}
    />
  </Field>
);

const TextareaField = ({ 
  error,
  isInvalid,
  required,
  name,
  onChange,
  value,
  defaultValue,
  meta,
  placeholder,
  required,
  inputAttrs,
  ...rest,
}) => (
  <Field
    {...rest}
    error={error}
    isInvalid={isInvalid}
    required={required}
  >
    <Textarea
      {...inputAttrs}
      placeholder={placeholder}
      onChange={onChange}
      value={value}
      defaultValue={defaultValue}
    />
  </Field>
);

This makes it to easier to plug it into various other validation libraries, as most of those require just onChange and value props for updating the input components.

In addition, it would be ideal for Terra to provide basic validation functions for form requirements such as required and minLength functions.

const required = value => (value ? undefined : 'Required')

const minValue = min => value => value.length >= min ? undefined : `Value must have at least ${min} characters`;
bjankord commented 6 years ago

My quick thoughts are I think there is a benefit to bringing back TextField, NumberField, etc. I think they allow us to bake in some accessibility, we can require IDs and label text and wire up the label to the input[s]. They will also make it easier to build forms an interface with libraries like react-final-from. We've talked about bringing those components back in https://github.com/cerner/terra-core/issues/1285

nickpith commented 6 years ago

I have some questions:

JakeLaCombe commented 6 years ago

Field is the react-final-form field. For the terra field, I renamed it to TerraField in the example.

For the specialized field, I envision we would name them to terra-form-textarea-field, terra-form-number-field, etc.

For the different number and text fields, the main purpose was to first class the number specific attributes, the text specific attributes, and then pass them into the input for the field. NumberField attributes would include min, max and step, and TextField attributes would include maxLength and minLength. We had an original implementation of them at http://engineering.cerner.com/terra-core/#/site/components/site/form/number-field-index and http://engineering.cerner.com/terra-core/#/site/components/site/form/text-field-index, but haven't re implemented them since we deprecated terra-form.

nickpith commented 6 years ago

Thanks for the clarifications.

NumberField attributes would include min, max and step, and TextField attributes would include maxLength and minLength

My point is that an InputField already supports min, max & step and maxLength and minLength in the inputAttrs. Why not just mention that inputAttrs support all attributes listed on https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input. If there is a need for NumberField & TextField, that should internally use InputField.

JakeLaCombe commented 6 years ago

I wouldn't be opposed to that. I'll talk to the other terra members to see what they think as well. Another source of inspiration was from the django form components they had (https://docs.djangoproject.com/en/2.0/ref/models/fields/#field-types), but I do think we get more flexibility out of the component if we just move those attributes to inputAttrs.

JakeLaCombe commented 6 years ago

I have created a tech design for the InputField that is requested. https://github.com/cerner/terra-core/issues/1285

saedar commented 6 years ago

+1 to updated design w/ react-final-form.

bjankord commented 6 years ago

+1 to updated design w/ react-final-form and creating InputField, TextareaField

nickpith commented 6 years ago

Do we ever see a point where the terra-framework project includes some opinionated HOCs that composite together all the final-form interaction with Terra form fields to simplify validation for users?

I'm thinking of a component similar to what InputField and TextareaField are to Input and Textarea. So we have something like ValidatedInputField that just provides a convenience component for some of the boilerplate code you have in your examples above. Maybe those components take in the required prop and automatically put in that validation and set the InputField accordingly.

JakeLaCombe commented 6 years ago

I'm not opposed to that idea, but I feel like we would have to see how consumers build their forms using react-final-form before we can land on those opinions and flesh out the components. react-final-form has a ton of features that handle different situations such as erroring out on submit and erroring out dynamically while the user is interacting with the inputs. Scenarios like that would require using different options that react-final-form provides (meta.error vs meta.submitError for example), so we would have to add a custom prop onto that HOC component for every possible behavior we would like our form validations to have.

nickpith commented 6 years ago

I agree and was not saying we do that initially and wait to see the patterns that emerge. I was just curious how we felt long term on if something would be needed.

bjankord commented 6 years ago

Resolved in https://github.com/cerner/terra-framework/pull/165