ark-form
ark-form copied to clipboard
React form validation library
Ark Form validation library
Table of Contents
-
Overview
- Codesandbox demos
- Installation
- Motivation
- Collaboration
-
Top-level architecture
- The general data flow
- Field state evaluation logic
- Validation
-
<ArkForm/>
component -
<ArkField/>
component - How manually set the field state
- Connecting to more complex elements
Overview
- small, ultra fast and flexible
react
based form validation library; - predictable and synchronous validation flow, clear and fast test suits
- allows granularly fine-tune each field validation trigger. E.g., consider you need the 1-st field to be validated after
onChange
event occurred and the second field only afteronBlur
event; - no external dependencies;
- fully written in
typescript
; - 2.6 kb minified & gzipped;
- compatible with
React v16.8+
;
Codesandbox demos
Installation
npm install ark-form --save
or yarn add ark-form
Motivation
Why not formik?
- extra re-renders, e.g., one field value changes, all other fields within same form undergo re-render;
- can't granularly fine-tune each field validation trigger. All fields within the form are subject to the same validation trigger's rules(
validateOnBlur
,validateOnChange
exposed only on a top form level); -
formik
asynchronous validation nature requires the use ofawait
constructs: example1, example2, example3, example4. - bigger lib size: > ~12kb minified & gzipped
- no
dirty/pristine
indicators' native support for a particular field(you need to resort to custom statefieldMeta.touched && fieldMeta.initialValue !== fieldMeta.value
constructs);
Collaboration
Library source files are located at ./ark-forms/src
.
Tests reside at ./ark-forms/__tests__
and ./web/__tests__
.
web
- next.js and web-cra
- cra projects are sandboxes of real-world use.
Top-level architecture
ark-from
library is based on several components:
The general data flow
All data flow except form submitting) flows start at <ArkField/>
components which listen for proper event type.
-
change
orblur
event happens to theinput
wrapped in a field component<ArkField/>
; - Calculating new field state with
fieldReducer
; - Dispatching new field state to
formReducer
, triggering entire form state re-evaluation; - Propagating new form & field states using
FormContext
downwards;
Field state evaluation logic
when a change
event occurs:
when a blur
event occurs:
Validation
All validation depends on auxiliary function validate
which executed within Calculate field validity
stage(field state evaluation logic).
interface BasicInput<ET> {
// ...
validate?: (value?: string) => ValidityStateInterface;
// ...
}
interface ValidityStateInterface extends Record<string, any> {
valid: boolean;
className?: string;
errorMessage?: string;
}
<ArkForm/>
component
- holds inner
<form/>
element &<Field>
components; - manages form state, configuration, creates
<FormContext/>
- distributes it through
<FormContext/>
between inner<ArkField>
components.
Hooking-up managed state with <form/>
elem happens through setting-up name
, onSubmit
, onChange
, onBlur
props on your elem. However there's shortcut, through spread operator {...formProps}
:
<ArkForm>
{({ state, formProps }) => (
<form name={name} {...formProps}>
{children}
</form>
)}
</ArkForm>
<ArkForm/> props:
Props | Description | Default Value |
---|---|---|
name | <form/> name | none |
onSubmit | onsubmit event handler | none |
onChange | onchange event handler, called on any inner field change |
none |
validateOnBlur | Runs fields validation on blur | true |
validateOnChange | Runs fields validation on change | false |
<ArkField/>
component
- encapsulates input field state
- uses children render prop technique in order to share managed state with user's components
- implicitly connected to parent form state through
FormContext
Hooking-up managed state with html input elem happens through setting-up value
, ref
, onChange
, onBlur
, onFocus
props on your input elem:
<ArkField>
{({ fieldProps, fieldState, formContext }) => (
<div>
<input id='field1' type='text' {...fieldProps} />
<label htmlFor='field1'>Field 1</label>
</div>
)}
</ArkField>
<ArkField/> props:
Prop | Description | Default |
---|---|---|
name | Field name | none |
initialValue | Field initial value | none |
onChange | onchange event handler | none |
onFocus | onfocus event handler | none |
onBlur | onblur event handler | none |
validate | your own validator callback | none |
How manually set the field state
First, you need to hook up to a form context:
export interface FormContextInterface {
state: FormState;
dispatch: React.Dispatch<FormAction>;
setFieldState: (name: string, setState: (currState: FieldState) => DeepPartial<FieldState>) => void;
setFieldValue: (name: string, value: string, configuration?: Partial<FieldConfiguration>) => void;
}
Within <ArkForm/>
, you can call for the form context:
const formContext = useFormContext();
Outside of <ArkForm/>
, pass ref obj:
...
const contextRef = useRef();
return <ArkForm formContextRef={contextRef}>
{({ formContext, formProps }) => (
<form name={name} {...formProps}>
{children}
</form>
)}
</ArkForm>
Once you get formContext reference, you're free to use formContext.dispatch
, method to alter the form state in any imaginative way. Internally, all components operate only through dispatch
method and formReducer
, fieldReducer
reducers.
Here's implementations of setFieldState
, setFieldValue
helper methods exposed publicly to cover most of user's needs:
const setFieldState: FormContextInterface['setFieldState'] = (name, setNewState) => {
const newState = setNewState(getFieldState(name));
const mergedNewState = mergeState(getFieldState(name), newState);
const validatedState = fieldReducer(mergedNewState, { type: 'validate' });
dispatch({
type: 'setField',
fieldState: validatedState,
});
};
const setFieldValue = (name: string, value: string, configuration?: Partial<FieldConfiguration>) => {
const state = getFieldState(name);
const newFieldState = fieldReducer(state, {
value: value,
type: 'change',
configuration: { ...state.configuration, ...configuration, validateOnChange: true },
});
dispatch({
type: 'change',
fieldState: newFieldState,
});
};
You can peek more
setFieldState
,setFieldValue
usages examples at/web/components/TestSuit.tsx
.
Setting field valid:
formContext.setFieldState(name, () => ({
configuration: {
validate: value => ({valid: true}),
},
}))
Setting field dirty:
formContext.setFieldState(name, () => ({ dirty: true, pristine: false }))
Setting field pristine:
formContext.setFieldState(name, () => ({ dirty: false, pristine: true }))
Consider you having some custom and complex validation logic described at:
const checkValidity = (
value?: string,
pattern?: {
regexp: RegExp;
message?: string;
},
required?: boolean
): ValidityStateInterface => {
const result: ValidityStateInterface = {
valid: true,
};
if (required && !value) {
result.className = FieldStateClassNames.requiredError;
result.valid = false;
return result;
}
if (pattern && value && !pattern.regexp.test(value)) {
result.className = FieldStateClassNames.patternError;
result.valid = false;
result.errorMessage = pattern.message || 'Invalid value';
return result;
}
return result;
};
export const TextInput = ({ initialValue = '', name, label, pattern, required, readOnly, ...rest }) => {
return (
<ArkField
name={name}
validate={value => checkValidity(value, pattern, required)}
initialValue={initialValue}
{...rest}
>
{({ fieldProps, fieldState, formContext }) => {
const id = (formContext.state.configuration.name || '') + '-' + name;
let ErrorMessage = null;
if (
fieldState.validity.errorMessage &&
!fieldState.validity.valid &&
(fieldState.dirty || formContext.state.submitted)
) {
ErrorMessage = <span className='error'>{fieldState.validity.errorMessage}</span>;
}
return (
<div>
<div
title={`${name} field`}
className={`txo-input-container ${classnames(
{
[FieldStateClassNames.filled]: fieldState.filled,
[FieldStateClassNames.pristine]: fieldState.pristine,
[FieldStateClassNames.dirty]: fieldState.dirty,
[FieldStateClassNames.invalid]: !fieldState.validity.valid,
[FieldStateClassNames.valid]: fieldState.validity.valid,
},
{
[fieldState.validity.className]: fieldState.validity.className && !fieldState.validity.valid,
}
)}`}
>
<input id={id} type='text' readOnly={readOnly} {...fieldProps} />
<label htmlFor={id}>{label}</label>
</div>
{ErrorMessage}
</div>
);
}}
</ArkField>
);
};
, then in order to maintain all existing validation rules except mandatory requirement rule you will just need to update your custom validator checkValidity
arguments:
formContext.setFieldState(name, () => ({
configuration: {
validate: value => checkValidity(value, pattern, false),
},
}))
Resetting field state:
formContext.setFieldState(name, () => ({
...defaultFieldState,
configuration: {
validate: value => checkValidity(value, pattern, required),
},
}))
Setting field value:
formContext.setFieldValue(name, 'Some new value')
Connecting to more complex elements
Plain and simple examples on how to create and connect with a form validation more complex input elements. Original source code is under ./web/components/**
.