@speedlo/xform
v1.1.3
Published
Heavily inspired by Formik, but using MobX for performance and less buggy behavior.
Downloads
21
Readme
XForm
Heavily inspired by Formik, but using MobX for performance and less buggy behavior.
XForm is centered around fields which are used to control all form features.
How to setup a form
To setup a form the useXForm
hook is used which provides xform
bag of state, api and handlers.
const MyForm = () => {
const xform = useXForm({
initialValues: {
name: '',
},
onSubmit(values) {
// submit values
},
})
return <XFormRender xform={xform}>{children}</XFormRender>
}
Alternatively for simple forms you can do following which joins xform
and XFormRender
together.
const MyForm = () => {
return <XForm initialValues={{ name: '' }} onSubmit={values => /* ... */}>
{children}
</XForm>
}
The XFormRender
is tiny wrapper around form
element that automatically listens to submit event. There is also XFormProvider
to keep the xform
around the tree. Any form fields has to be rendered within its tree.
The initialValues
are required, but can be empty object. Field value would be undefined
by default so bear that in mind. Values can be nested, there are no restrictions. Arrays are supported as well.
The second required prop is onSubmit
which is executed after successful validation and provides you with values that can be sent out to the server or whatnot.
In case of a failed validation, an optional onSubmitFailed
is called which can be used to collect state.errors
and display those to user. Note it has nothing to do with result of an actual submit which is completely in your control.
Fields usage
To declare a field you have to call useXField
which accepts name of field. In case of nested field either use the.deep[1].field
notation or directly pass an array like ['the', 1, 'deep', 'field']
.
import { useObserver } from 'mobx-react-lite'
const EnabledCheckbox = () => {
const field = useXField('enabled')
return useObserver(() => (
<input
type="checkbox"
id={field.name}
name={field.name}
checked={field.value === true}
onChange={() => field.setValue(!field.value)}
onBlur={field.setTouched}
/>
))
}
Note the
useObserver
is essential otherwise field won't be reactive. You can also useobserver
HOC orObserver
component.
Touching fields
The concept of touched
state represents the scenario where user has somehow interacted with the field. Tracking touched state can be used to show the field state differently, eg. failed validation which would be annoying to display for all fields right away. Simply call field.setTouched
whenever you feel like it's time. Method accepts boolean value in case you want to toggle touched state off again.
When the form is submitted, the field.touched
is automatically turned true
as submitting is considered as a final user action like he has touched everything. However, the actual value of touched state is still available in field.touchedPure
.
Input field
Since the basic input is so common, the hook useXFieldForInput
is exported and can be used like following. It handles onChange
, onBlur
and value
props.
const FormInput = () => {
const [field, getInputProps] = useXFieldForInput < string > fieldName
return useObserver(() => (
<input
{...getInputProps()}
invalid={field.touched && Boolean(field.error)}
/>
))
}
Validation
It's recommended to use form level validation for most of the forms. However, it's possible to declare a field with its own validation function to have a reusable component that does not rely on schema validation.
For a field to be considered for a validation it has to be registered. It is done automatically as a part of useXField
hook, but each field can be registered only once. For the purposes of accessing other fields state, the useXFieldState
can be used.
Generally, the validation runs in low priority mode and is debounced by lowPriorityDebounceIntervalMs
(250ms by default). To force immediate validation of a whole form call api.validateForm
can be called. The same function is used right before form is submitted to ensure everything has the right value.
By default validation is scheduled after field value change and when the field becomes touched (eg. onBlur). In case you want to revalidate on each "blur", it's necessary to field.setTouched(false)
. (eg. onFocus or onChange).
During the validation the state.isValidating
flag is toggled so it can be reflected in UI, especially for async validations.
Form validation
XForm validation is build on top of Yup library for declaring a validation schema. The library comes with XForm and you can use createValidationSchema
exported function to declare schema in the easy way.
const schema = createValidationSchema(yup => ({
name: yup
.string()
.required()
.min(10),
age: yup.number().moreThan(18),
}))
It's recommended for schema to cover all form values. In case you omit some, the warning will be shown in development during validation to prevent typo errors. If you wish to supress these warnings, there is a ignoreUnknownFieldsInValidation
configuration option, but consider yourself warned.
Field validation
The field level validation is a simple function which is expected to return either string with error or nothing. Function can be asynchronous and return Promise
with the same. Don't reject the Promise unless you really mean it.
const validate = value => {
if (!rxDollar.test(value)) {
return "It's not a dollar!"
}
}
const DollarInput = () => {
const field = useXField('price', { validate })
}
Form submission
Is done by either calling api.submitForm
imperatively, or by attaching handlers.handleSubmit
to onSubmit
event of the form
element. This is done automatically in XFormRender
for you.
The state.isSubmitting
is used to track the process of submission.
Loading data asynchronously
It's possible to call api.mergeValues
later when initial values are loaded. In this scenario it's recommended to set initialValidationEnabled
to false
and when data are loaded call api.enableValidation
to avoid validating against empty initial values.
Tracking changes
The state.isDirty
compares current state.values
(deeply) against state.pristineValues
which are set to initialValues
by default. It's possible to change state.pristineValues
to mark a point from which should be changes tracked (eg. after loading data)