React-Formz
High performance react form library built on top of zustand. Inspired by:
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 returnFormProvider
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 - 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
API
- creation. More on validation schema)
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
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
// 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
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
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
- usesuseField
under the hood. Main advantage is that you can passas
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
<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
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:
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
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
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
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 asuseForm
(FormArgs
) and few additional.
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
type CustomFields = {
type: "boxPicker";
name: string;
}
const customRender = (field: FieldSchema & CustomFields, key: string) => {
if (field.type === "boxPicker") {
return <BoxPickerField name={field.name} key={key}>
}
}
FieldSchema
Default form fields defalts with type 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:
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 egproductName -> products.0.productName
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
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 asdiv
element
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.
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 andContainerField
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 insidecustomRender
function
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 onFieldConfig
array.
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 traverseFieldConfig
array and return all names inside it
const fields = [...];
const names = getFieldNames(fields);
-
getField
- return particular field config fromFieldConfig
array by provided name
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:
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:
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:
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:
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";