npm package: @speedlo/xform
Description: Heavily inspired by Formik, but using MobX for performance and less buggy behavior.
Latest version: 1.1.3

# 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.

```tsx
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.

```tsx
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']`.

```tsx
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 use `observer` HOC or `Observer` 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.

```js
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](https://github.com/jquense/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.

```js
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.

```js
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)