Closed JakeLaCombe closed 6 years ago
We have several different options we can implement for doing our form validations.
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
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).
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.
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 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/).
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.
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
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.
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
My recommendation is the following
+1 to @JakeLaCombe's recommendation.
+1 to @JakeLaCombe's recommendation as well.
+1 to @JakeLaCombe's recommendation.
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.
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?
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.
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.
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.
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.
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`;
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
I have some questions:
With those specialized field examples, what is the Field type? A Terra Field or react-final-form Field?
What package would these specialized fields exist in?
I can see the value in TextAreaField but I'll need some convincing on the value of having TextField & NumberField when really we can just have an InputField where we pass in type=number
as part of the inputAttrs
to flex the input's type. Are there other things besides input type set with NumberField that could not be passed in as part of inputAttrs
?
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.
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
.
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.
I have created a tech design for the InputField that is requested. https://github.com/cerner/terra-core/issues/1285
+1 to updated design w/ react-final-form.
+1 to updated design w/ react-final-form and creating InputField
, TextareaField
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.
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.
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.
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