npm package: @finalapp/react-formz
Description: High performance react form library built on top of [zustand](https://github.com/pmndrs/zustand). Inspired by:
Author: finalapp
Latest version: 2.6.0

# React-Formz

High performance react form library built on top of [zustand](https://github.com/pmndrs/zustand). Inspired by:

- [final-form](https://github.com/final-form/final-form)
- [react-hook-form](https://github.com/react-hook-form/react-hook-form)

## Installation

`yarn add @finalapp/react-formz`

or

`npm i @finalapp/react-formz`

if you will use `yup` - run:

`yarn add yup` or `npm i yup`

## Form

Main module, contain form create function, basic hooks to get part of he form state and other core functionality

You can create form in 3 ways

- use `Form` component - it will create form and return form provider, any form related fields must go as its child.
  Most classical way to create form
- use hook `useForm` and return `FormProvider` by yourself. Main advatage is that way you can use form functionality
  returned from hook not inside form provider children but instantly isnide component whcich will return form
- Form schema - use `FormSchema` component - it renders form by provided form schema, description of it will be in
  [Schema section](#schema)
- Inside form module we have `createCalculation` which is used internally to watch fpr fields change an based on that -
  update other fields, more on that will be inside [Field section](#field)

### API

---

- creation. More on [validation schema](#resolvers))

```tsx
type FormArgs<T = any> = {
  initial?: T; // initial values, Default - empty object
  onSubmit: OnSubmit<T>; // Required - function will be called with values and form state on form submission
  mode?: Mode;  // when validate field which is without error yet  "onChange" | "onSubmit" | "onBlur" | "onTouched". Default "onSubmit",
  reinvalidateMode?: ReinvalidateMode; // when to reinvalidate input if it has an error "onChange" | "onSubmit" | "onBlur". Default `onChange`
  validationSchema?: ValidationSchema; // validation schema. Default undefied
  initialErrors?: Record<string, any>; // initial erros object or undefined. Default undefined
  unsetEmpty?: boolean; // default `true` - if field return empty string its unsetted from from values
};

const formArguments: FormArgs<any> = {...};

// You can create hook then render form provider and fields within it
const form = useform(formArguments);
<FormProvider form={form}>...</FormProvider>

// You can also create Form without hook, you cant then use form before render it
<Form {...formArguments}>...</Form>
```

---

- useFormData - get some data from form state, can Be done through `FormData` render props component too

```tsx
type FormDataTypes =
  | "isValid"
  | "isSubmittig"
  | "isValidating"
  | "isDirty"
  | "active"
  | "submit"
  | "reset"
  | "validateForm"
  | "validateField"
  | "setField"
  | "values"
  | "errors"
  | "touched"
  | "data";

const data = useFormData(type: FormDataTypes);
// or
<FormData type="isValid">
  {(isValid) => isValid ? null : <div>Not valid</div>}
</FormData>
```

---

## Resolvers

- resolvers is created to provide schema validation for form values. Currently there is only one resolver `yupResolver`
- Resolver is a function typed as

````ts
// SCHEMA - some schema defined by resolver
// SCHEMAPart - its schema nat for whole object byt for particular field only
type SingleFieldError = string | string[] | null | undefined | false;

type ValidationSchema = {
  validate: (values: any, errors: any) => any | Promise<any>;
  validateField: (value: any, values: any, name: string) => SingleFieldError | Promise<SingleFieldError>;
};

---

## Components

- we have 2 helper components

- Preffix - to preffix fields names within it

```tsx
<FieldPrefix prefix="address">
  {/* nested children */}
  <PrefixedField name="streetName" /> {/* name inside form will be with preffix so it will be `address.streetName` */}
</FieldPrefix>
````

- Condition - display conditionaly anything - basen on some field state

```tsx
type FieldDataKey = "errors" | "touched" | "values" | "data";

export type ConditionProps = {
  when: string;
  is: string | number | ((v: any) => boolean);
  scope?: FieldDataKey;
};

<Condition when="lastName" is="Smith" scope="values">
  {(condition) => (condition ? "Hi!" : "Not Smith")}
</Condition>;
```

---

## Field

- `useField` - hook to create field within form

```ts
type TypedInputTypes = "checkbox" | "radio" | "select" | "select-multiple";

type UseFieldOptions<T> = {
  // function to validate field eg (age) => age > 20 ? null : "too young"
  validate?: ValidateField;

  // format value from form before pass it to component
  format?: (v: any) => T;

  // parse values form inpput before set inside form
  parse?: (v: T) => any;
  // same as above but parse field value only on blur event
  parseOnBlur?: (v: any) => T;

  // subs is flags to subscribe field state from form
  errorSub?: boolean;
  touchSub?: boolean;
  dataSub?: boolean;
  activeSub?: boolean;
  valueSub?: boolean;

  value?: string; // for radio only
  type?: TypedInputTypes | string; // for checbox and radio valueKey and default format
  multiple?: boolean; // for select only
  defaultValue?: T;
  calculation?: FieldCalculation; // watch of field change and update other form values, more on that - at the end of this section
  data?: FieldDataArg<any>; // any additional data
};

type UseFieldResult = {
  data?: any;
  touched?: boolean;
  error?: TError;
  checked?: boolean; // checked is returned it compoennt type is either radio or checkbox, otherwise we return `value`
  value?: any;
  active?: boolean;
  onFocus: () => void;
  onBlur: () => void;
  onChange: (event: any) => void;
};

type useField = <T>(name: string, data: UseFieldOptions<T>) => UseFieldResult;

const someData = { valueSub: true };

const field: useField = useField("lastName", someData);
```

- `FieldData` - uses `useField` under the hood. Main advantage is that you can pass `as` prop to it to display render
  some component without declate it by yourself. You can pass custom component, string
  (`"input" | "select" | "textarea"`) or render props

```tsx
<Field name="lastName" {...someData} as="select" />
<Field name="lastName" {...someData} as={CustomInput} />
<Field name="lastName" {...someData}>
  {(field) => <input {...field} />}
</Field>
```

- `useFieldData` | `FieldData` - to get some field state (value, error, touched). You can use either hook or render prop
  component

```tsx
interface UseFieldData {
  <T>(name: string, type: "values"): T;
  (name: string, type: "errors"): TError;
  (name: string, type: "touched"): TTouched;
  <T>(name: string, type: "data"): T;

  <T, R>(name: string, type: "values", parse: (value: T) => R): R;
  <R>(name: string, type: "errors", parse: (value: TError) => R): R;
  <R>(name: string, type: "touched", parse: (value: TTouched) => R): R;
  <T, R>(name: string, type: "data", parse: (value: T) => R): R;
}

// examples
const lastNameValue = useFieldData("lastName", "values"); // get valu of lastName field
const hasErrors = useFieldData("lastName", "errors", (err) => Boolean(err)); // get errors of field and parsed it to boolean - that way we get only information if field has error

<FormData name="lastName" type="data" parse={(d) => Boolean(d)}>
  {(hasData) => (hasData ? "last name field have custom data" : "")}
</FormData>;
```

- calculation - watch for field value to update some others form fields. Types:

```ts
type FieldNamePattern = string | RegExp | string[];

type UpdatesByName<T = any> = {
  [FieldName: string]: (value: any, allValues?: T, prevValues?: T) => any;
};

type UpdatesForAll<T = any> = (
  value: any,
  field: string,
  allValues?: T,
  prevValues?: T,
) => { [FieldName: string]: any } | Promise<{ [FieldName: string]: any }>;

type Updates<T = any> = UpdatesByName<T> | UpdatesForAll<T>;

type FieldCalculation = {
  field?: FieldNamePattern; // if you dont pass on which field calculation must watch - it will watch current declared field
  isEqual?: (a: any, b: any) => boolean; // custom equality checker, default is reference equality `a === b`
  updatesOnBlur?: boolean | ((fieldName: string) => boolean); // watch only on field blur? can be funciton to check on which field we check updates - its convinient to use when fieldName is a RegExp to watch for group of fields and some of them we want to get only onBlur and others not
  updates: Updates;
};
```

updates can be object with fields names to updates as keys and functions wchihc will return value to set or function
types above witch must return object with field names as keys and values attached to them

---

## Field array

hooks and helpers to work with array fields

- `useFieldArray` - basic hook to create array field. Accept partial data from UseFieldOptions -
  `defaultValue | calculation | data`

```tsx
type UseArrayFieldOptions = Pick<UseFieldOptions, "defaultValue" | "calculation" | "data">;
type ArrayFieldResult = {
  fields: string[]; // array of fields anme within array. eg ["products.0", "products.1"]
  // helper functions to work with array
  swap;
  append;
  prepend;
  remove;
  insert;
  move;
};

type useFieldArray = (anme: string, options: ArrayOptions) => ArrayFieldResult;

const options = { defaultValue: [{ name: "chocolate" }] };

useFieldArray("products", options);
```

- `useFieldArrayState` - get array field state info. It accepts array field name and options which all flags to
  subscribe to state values

```tsx
const { active } = useFieldArrayState("products", {
  activeSub: true,
  dataSub: false,
  valueSub: false,
  errorSub: false,
  touchSub: false,
});
```

- `useManipulateArray` - get functions to work with array field. It accept array field name and returns function wich
  accepts function that will operate on the array values, after call it we must call returned function with arumentes
  that operator fn needs

```tsx
const manipulateArray = useManipulateArray("products");

// later inside code
manipulateArray(swap)(1, 3);
manipulateArray(insert)({ name: "bread" }, 2);
```

---

## Schema

Other way to create form as schema, that way you can declare form schema as array of objects and dont need to render
fields etc by yourself. To do it you must declare fields and pass it to `SchemaForm` component

- `SchemaForm` component. It accepts for props same values as `useForm` (`FormArgs`) and few additional.

```tsx
type FormProps<FORM_DATA = AnyObject> = FormArgs<FORM_DATA> & {
  // Loading component which can be renderen while form fields will be computed. Default null
  LoadingComponent?: React.FC;
  // function to render inputs that you will create or override default ones
  customRender?: CustomRenderInput<any>;
  // fields, later on we will discuss them more
  fields: FieldSchema[];
} & (
    | // accept children as render prop which accepts form returner by `useForm`
    {
        formProps?: never;
        SubmitComponent?: never;
        children: (dom: React.ReactNode, form: TFormContext<FORM_DATA>) => React.ReactNode;
      }
    // or children as components (will be rendered within `form` tag) and then you can pass `formProps` to that `form` tag and custom SubmitComponent
    | {
        children?: React.ReactNode;
        formProps?: JSX.IntrinsicElements["form"];
        // Devault is <button>submit</button>
        SubmitComponent?: React.FC | null;
      }
  );

<SchemaForm {...schemaFormProps}>...</SchemaForm>;
<SchemaForm {...schemaFormPropsRenderProps}>{(form) => <form onSubmit={form.submit}>...</form>}</SchemaForm>;
```

- `customRender` - function that render component based on field from schema. Function can return undefiend or null, if
  undefined then rest default renderField functio will try to render field based on its type

```tsx
type CustomFields = {
  type: "boxPicker";
  name: string;
}

const customRender = (field: FieldSchema & CustomFields, key: string) => {
  if (field.type === "boxPicker") {
    return <BoxPickerField name={field.name} key={key}>
  }
}
```

---

### Default form fields defalts with type `FieldSchema`

---

#### Value fields - render some inputs etc to an user

- all fields if are rendered within array field - are rendered with prop `arrayIndex: number`, otherwise this prop not
  exists

- defalult value fields:

```ts
type FieldCommonsData = UseFieldOptions<any> & {
  // UseFieldOptions is options passed to `useField` hook
  name: string; / field name
};

type CommonField = FieldCommonsData & {
  type: "text" | "number" | "checkbox" | "radio" | "textarea"; // that kind of inputs is provided by default
};
```

---

#### Special fields - render some meta fields which has special meaning

- `ArrayField` - field to render array of fields. IMPORTANT - fields inside array will have preffixed name with array
  field name and index eg `productName -> products.0.productName`

```tsx
type ArrayField = UseArrayFieldOptions<any> & {
  name: string;
  type: "array";
  fields: FieldSchema[];
  Component: React.FC<{ fieldArrayName: string }>;
  Row?: React.FC<{
    fieldArrayName: string;
    index: number;
  }>;
};

const arrayField = {
  type: "array",
  name: "products",
  fields: [...],
  Component: ArrayProducts, //custom component that will accept fieldArrayName and children (rendered fields) as props
  Row: ProductRow, // custom component that will accept fieldArrayName, row index and children (rendered fields) as props
}
```

- `ConditionField` - display children fields behind some cindirion based on some form value

```tsx
type ConditionField = ConditionProps & {
  type: "condition";
  fields: FieldSchema[];
};

const conditionField = {
  type: "condition",
  when: "age",
  is: 20, // can be function, number or string
  fields: [...],
}
```

- `ContainerField` - is dumb container element, has no meaning for form state, only to provide some structure to html or
  styling. By default is rendered as `div` element

```tsx
type ContainerField<T = JSX.IntrinsicElements["div"]> = T & {
  type: "container";
  Component?: React.FC<T>;
  fields?: FieldSchema[];
};

const containerField = {
  type: "container",
  fields: [...],
  Component: "section", // can be Custom component, or string that represetns html tag
  ...additionalProps // props that your `Component` accepts
}
```

- `LiveField` - field which accept any fieldConfig and useConfig as hook that can change field confign on runtime.
  Inside hook you can use whatever you want - fetch for data, custom functions, some context.

```tsx
type LiveField = {
  Fallback?: React.ComponentType<FieldSchema>; // if cnfig is not ready - renter it as a placeholder
  useConfig: (
    fieldConfig: FieldSchema,
  ) => [config: FieldSchema, ready: boolean]; // must return config and ready boolean flag. accepts default confing as arument
  fieldConfig: FieldSchema;
  type: "live";
  name: string; // name of the field created
};

const liveField = {
  type: "live",
  Fallback: () => <div>loading</div>
  useConfig: (config) => {
    return [{...config, disabled: true}, true];
  },
  fieldConfig: {...},
  name: "description"
};
```

- `CustomField` - you can render completly custom field, difference betweent that one and `ContainerField` id that
  container is not form field, only component to provide some structure etc but this one accepts field prosp which you
  can use inside component and its name is known durning parsing fields. Taht one is some simple escape hatch, you dont
  need to use it if you render custom declared fields inside `customRender` function

```tsx
type CustomField<T = { key?: string }> = FieldCommonsData &
  T & {
    type: "custom";
    Component: React.FC<T>;
    fields?: FieldSchema[];
  };

const customField = {
  type: "custom",
  Component: CustomInputWithBackground,
  fields: [...],
  ...additionalProps // props that your `Component` accepts
}
```

---

### Additional helpers and hooks

- `useFormGenerator` - hook to create react elements based on `FieldConfig` array.

```tsx
import { renderInput as renderInputBase } from "form/schema";
const fields = [...];
const customRenderInput = ...;
const renderInput = renderInputBase(customRenderInput);

// you dont need to pass renderInput arument - default will be base renderInput with default fields provided by this module
// so if you didnt declare custom fields - just leave second argument empty
const dom: React.ReactNode[] = useFormGenerator(fields, renderInput);

...
return <div>{dom}</div>
```

- `getFieldNames` - helper to traverse `FieldConfig` array and return all names inside it

```ts
const fields = [...];
const names = getFieldNames(fields);
```

- `getField` - return particular field config from `FieldConfig` array by provided name

```ts
const fields = [...];
const lastNameField = getField(fields, "lastName");
```

---

## Validation

React-Formz comes with builtin validation module which cover almost all cases and escape-hatches by providing custom validation functions.

typings:

```ts
type CustomError = string | ((value: any) => string);
type FormData = Pick<TForm<any>, "values" | "errors" | "touched" | "data">;

type CommonValidators = {
  required?: true;
  custom?: (value: any, formData: FormData) => null | boolean | string;
  // type error is only key that custom error of it must be declared not as message tuple
  typeErr?: CustomError;
};

type StringValidator = CommonValidators & {
  type: "string";
  length?: number;
  minLength?: number;
  maxLength?: number;
  equalTo?: string | string[];
  notEqualTo?: string | string[];
  pattern?: RegExp | string;
};

type NumberValidator = CommonValidators & {
  type: "number";
  min?: number;
  max?: number;
  equalTo?: number | number[];
  notEqualTo?: number | number[];
};

type BooleanValidator = CommonValidators & {
  type: "boolean";
  equalTo?: boolean;
  notEqualTo?: boolean;
};

type ArrayValidator = CommonValidators & {
  type: "array";
  empty?: false;
  length?: number;
  minLength?: number;
  maxLength?: number;
  of?: Validator;
};

type TupleValidator = CommonValidators & {
  type: "tuple";
  empty?: false;
  of?: Validator[];
};

type ObjectValidator = CommonValidators & {
  type: "object";
  shape?: Record<string, Validator>;
};

// ValidatorRich is all of above alidations - enriched with custom error tuple and reference types
type LazyValidator = (value: any, formData: FormData) => ValidatorRich | null | string;

// its type for field `validate` key
type ValidateField = ValidatorRich | LazyValidator;
```

---

**Custom error string:**
Any validation key exept `"of" | "shape" | "type"` - can be written as tuple - `[T, CustomError]` where first element is type of key and second is string with as custom error or function that receive value of the field and returns error string.
Example:

```ts
import { isEmail } from "someLibrary";

const validation: EnrichValidator<StringValidator> = {
  type: "string",
  // type error can be as message tuple so its decalted as special key `typeErr`
  typeErr: "incorrect type!",
  length: [10, "must have 10 characters"],
  custom: [isEmail, "value is not valid email"],
  notEqualTo: [["some string", "some other string"]], // one caveat is when value is array with length of 2 - if you dont want second argument be treated as error string - pass it as tuple with one element
};
```

---

**References:**
Any validation key value exept `"of" | "shape" | "type" | "custom"` can be passed as reference to some form data which will be get at validation time.
It also works with custom error tuple. Inside ref you can pass any path from `"values" | "errors" | "touched" | "data"` form scopes.
Example:

```ts
import { getRef } from "@finalapp/react-formz";

const validation: EnrichValidator<StringValidator> = {
  type: "string",
  length: getRef("values.characters"),
  equalTo: [getRef("data.field.meta"), "some error"],
};
```

---

**Lazy validator:**
You can also use lazy validator - function that will receive form data and current vfield value, and which returns from above `Validation` types.
you can also return null that indicates taht fields has no validation and so - no error, or string as error message - whichout return any validator.
Example:

```ts
const lazyValidator: LazyValidator = (fieldValue) =>
  typeof fieldValue === "number"
    ? {
        type: "number",
        min: 5,
      }
    : {
        type: "string",
        minLength: 10,
      };

// if value is not of type `number` - we dont want validation
const lazyValidatorEmpty: LazyValidator = (fieldValue) =>
  typeof fieldValue === "number"
    ? {
        type: "number",
        min: 5,
      }
    : null;

// if value is not of type `number` - we returning error immediately, without validator creation and running
const lazyValidatorError: LazyValidator = (fieldValue) =>
  typeof fieldValue === "number" ? null : "provide any number";
```