-
There is not a generic solution for handling dirty state checking and reverts throughout our codebase.
-
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.
- Generalizes and abstracts form dirty state, and reverts
- Allows to define validation once for an input while keeping form inputs portable and easily composeable.
- A standardized interface for validation functions and how errors are presented both inline and at the form level.
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>
)
}
}
+------------------------+
| Form HoC |
+-------+--------+-------+
| |
+ +
`form` api `formState`
+--------------------------+
| |
| Wrapped components |
| `props.form` |
| `props.formState` |
+------------+-------------+
|
|
|
v
FormInput
- Receives a getter for initial form state
- Passes
form
andformState
down as props - exposes functionality such as
revert
,getValue()
,setValue(val)
- Any component with the following interface
<FormInput
form={{
setValue,
getValue,
getErrors,
addValidationFn,
}}
/>
// or for convenience
<FormInput
form={ props.form.field('name') }
/>
A quick note on validation
We would like a system where the structure of the error object and the presentational logic for displaying inline error messages is defined in the same place, this allows predictability and better inline validation without cross-coupling form and input components.
Passes as a props.form
to the wrapped component of the Form
HoC.
returns
all of the form data
returns
data for that particular keypath, ex: form.getValue('variations')
Sets the data for the entire form, if validateOnChange
is enabled it will re-validate
Sets data for a particular keypath, if validateOnChange
option is true
then it will revalidate that keypath.
Reverts the editing state to the original "initialState"
Iterates and runs all validation functions, useful for right before submission. Validation results is accessible via
form.isFormValid()
returns
boolean based on the current validation state. Note: this does not do validation, so you must either call
form.validateAll()
validationFn
form.addValidationFn({
keypath: 'name',
getErrors(name, formData) {
// can access `formData` to look at entire contents of form
// must return an object of the shape ErrorObject
return {
hasErrors: false,
details: {},
};
}
})
Creates a scoped FormFieldInterface
scoped to a particular keypath
Example:
const nameField = props.form.field('name');
nameField.setValue('Alan');
nameField.getValue() // returns 'Alan'
nameField.addValidationFn({
getErrors() {
return {
hasErrors: true,
details: {
msg: 'this is fake error'
},
}
},
});
nameField.validate();
const errors = nameField.getErrors(); // would return the `getErrors() result`
In practice it's often used to pass an namespaced object to FormInputs
<FormInput
form={ props.form.field('name') }
/>
returns
the value for the particular field
Sets the data for the particular field
returns
Error object for the particular field
Runs any validation fn registered on this keypath
and puts errors in formState.errors
which is in turned passed down
to components via form.field(fieldName).getErrors()
Adds a validation function for the scoped keypath