@finalapp/react-formz
TypeScript icon, indicating that this package has built-in type declarations

2.6.0 • Public • Published

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 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
  • 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


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 - 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
<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 as useForm (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}>
  }
}

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:

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
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 as div 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 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
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.
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
const fields = [...];
const names = getFieldNames(fields);
  • getField - return particular field config from FieldConfig 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";

Readme

Keywords

none

Package Sidebar

Install

npm i @finalapp/react-formz

Weekly Downloads

3

Version

2.6.0

License

MIT

Unpacked Size

685 kB

Total Files

135

Last publish

Collaborators

  • finalapp-dev