Skip to content

Repository files navigation

EFX-Forms

Effector JS forms

📚 Documentation: https://darianstlex.github.io/efx-forms/

Installation

$ npm install efx-forms

Peer dependencies - library depends on:

react effector effector-react lodash

Main Components

Form / Field

import{Form,Field}from'efx-forms';import{FormDataProvider}from'efx-forms/FormDataProvider';import{required,email}from'efx-forms/validators';constInput=({ id, label, error, errors, value, ...props})=>(<div><labelhtmlFor={id}>{label}</label><inputid={id}value={value||''}type="text"{...props}/><span>{error}</span></div>)constTextField=(props)=><FieldField={Input}{...props}/>constvalidators={name: [required()],}constPage=()=>{constsubmit=(values)=>{console.log(values);}return(<Formname="user-form"onSubmit={submit}validators={validators}><TextFieldname="name"label="Name"/><TextFieldname="email"label="Email"type="email"validators={[required({msg: `Hey, email is required`}),email(),]}/>{[0,1,2].map((idx)=>(<TextFieldkey={idx}name={`address[${idx}]`}label={`Address ${idx+1}`}/>))}<FormDataProvider>{({ values })=>(<div><pre>JSON.stringify(values)</pre><pre>JSON.stringify(shapeFy(values))</pre></div>)}</FormDataProvider><buttontype="submit">Submit</button></Form>)}
// Form valuesvalues={'name': 'John','email': 'john@test.com','address[0]': 'First Line','address[1]': 'Second Line','address[2]': 'Postcode',}valuesShape={'name': 'John','email': 'john@test.com','address': ['First Line','Second Line','Postcode',]}

Props

Form component

interfaceForm{// Form name - required, used to get form instance outside of contextname: string,/** * Form submit method - on validation success will be called with * form values. * If skipClientValidation is set - no validation will be applied. * If submit return promise: * - reject - object with errors per field - errors will **replace** all existing errors * (uses replaceErrors, not setErrors - client validation errors are cleared) * { 'user.name': 'Name is already taken', ... } * - resolve - success submit * @param values - IRValues - flat * @example * { 'user.name': 'John', 'user.age': '20' } */onSubmit?: (values: IRValues)=>void|Promise<IRErrors>;// If set, submit will skip client form validation// Default: falseskipClientValidation?: boolean;// Form initial values - field initialValue is in priorityinitialValues?: {fieldName: 'value'}// Keep form data on unmount// Default: falsekeepOnUnmount: boolean;// PROPERTY - serialize - serialize stores// not reactive, initialized only once on form creation// Default: falseserialize?: boolean;// Set fields validation behavior onBlur// Default: truevalidateOnBlur?: boolean;// Set fields validation behavior onChange// Default: falsevalidateOnChange?: boolean;// Disable reinit on initialValue changedisableFieldsReinit?: boolean;// Validators config per field - field validators are in priorityvalidators?: {fieldName: [(value: any,values: Record<string,any>)=>string|false,]};}

Field component

interfaceField{// Field name - required, used to register/get field in the formname: string,// Field initial value - used on initial load and reset// default = ''initialValue?: any;// Transform value before set to storeparse?: (value: any)=>any;// Format value before displayingformat?: (value: any)=>any;// Passive field does not update its active state and configpassive?: boolean;// Validators array - applied on validationvalidators?: TFieldValidator[];// Set validation behaviour onBlur, overrides form value// Default: truevalidateOnBlur?: boolean;// Set validation behaviour onChange, overrides form value// Default: falsevalidateOnChange?: boolean;// Disable reinit on initialValue changedisableFieldReinit?: boolean;// Field component - component to be used as form fieldField: ReactComponent<any>;// Form name - if field belongs to a different form or used outside// of the form contextformName?: string;}

IfFormValues component

Conditional rendering based on form values

interfaceIfFormValues{children?: ReactNode;// Form name - used to get form values,// if not provided will be taken from contextform?: string;// Condition check - accepts form values and return boolean,// if true render childrencheck: (values: IRValues,activeValues: IRValues)=>boolean;// Set fields values on show - { fieldName: 'value' }setTo?: IRValues;// Set fields values on hide - { fieldName: 'value' }resetTo?: IRValues;// Debounce for fields update// Default: 0updateDebounce?: number;// Render prop - accepts form values and return react element// if defined will be used instead of childrenrender?: (values: IRValues)=>ReactElement;}
import{IfFormValues}from'efx-forms/IfFormValues';constConditionalRender=()=>(<IfFormValuescheck={({ age })=>age>21}><div>Hey, I am here</div></IfFormValues>);constConditionalRenderProp=()=>(<IfFormValuescheck={({ age })=>age>21}render={({ age, name })=><div>Hi, I am {name} - {age}</div>}/>);

FormDataProvider component

Subscribe for form values changes

interfaceFormDataProvider{// Render function - provides all subscribed datachildren: (values: ReturnType<typeofuseFormData>)=>ReactNode;// Form name if used outside of context or refers to another formname?: string;}
import{FormDataProvider}from'efx-forms/FormDataProvider';constFormData=()=>(<FormDataProvider>{({ values, errors })=><div>{values} - {errors}</div>}</FormDataProvider>);

IfFieldValue component

Conditional rendering based on field value

interfaceIfFieldValue{children?: ReactNode;// Field namefield: string;// Form name - used to get form values,// if not provided will be taken from contextformName?: string;// Condition check - accepts form values and return boolean,// if true render childrencheck: (value: any)=>boolean;// Render prop - accepts form values and return react element// if defined will be used instead of childrenrender?: (values: any)=>ReactElement;}
import{IfFieldValue}from'efx-forms/IfFieldValue';constConditionalRender=()=>(<IfFieldValuecheck={(age)=>age>21}><div>Hey, I am here</div></IfFieldValue>);constConditionalRenderProp=()=>(<IfFieldValuecheck={(age)=>age>21}render={(age)=><div>Hi, I am {age}</div>}/>);

FieldDataProvider component

Subscribe for field value changes

interfaceFieldDataProvider{// Render function - provides all subscribed datachildren: (values: ReturnType<typeofuseFieldData>)=>ReactNode;// Field name to get stores values fromname: string;// Form name if used outside of context or refers to another formformName?: string;}
import{FieldDataProvider}from'efx-forms/FieldDataProvider';constFieldData=()=>(<FieldDataProvidername="user.name">{({ value, active })=><div>{value} - {active}</div>}</FieldDataProvider>);

Instances

Form Instance

interfaceFormInstance{/** PROPERTY - Form name */domain: Domain;/** PROPERTY - Form name */name: string;/** $$STORE - Form active fields - all fields statuses - flat */$active: Store<Record<string,boolean>>;/** $$STORE - Form active only fields - flat */$activeOnly: Store<Record<string,true>>;/** $$STORE - Form active values - all active / visible fields values - flat */$activeValues: Store<IRValues>;/** $$STORE - Form values - all fields values - flat */$values: Store<IRValues>;/** $$STORE - Form errors - all field errors */$errors: Store<IRErrors>;/** $$STORE - Form errors - fields last error - flat */$error: Store<IRError>;/** $$STORE - Form valid - true if form is valid */$valid: Store<boolean>;/** $$STORE - Form submitting - true if busy */$submitting: Store<boolean>;/** $$STORE - Form touched - true if touched */$touched: Store<boolean>;/** $$STORE - Form touches - all fields touches - flat */$touches: Store<Record<string,boolean>>;/** $$STORE - Form dirty - true if diff from initial value */$dirty: Store<boolean>;/** $$STORE - Form dirties - all fields dirty state - flat */$dirties: Store<Record<string,boolean>>;/** PROP - Form config */config: IFormConfig;/** PROP - Form config */configs: Record<string,IFieldConfig>;/** EVENT - Form erase - reset form and delete all assigned form data */erase: EventCallable<void>;/** EVENT - Form onChange event */onChange: EventCallable<{name: string;value: any;}>;/** EVENT - Form onBlur event */onBlur: EventCallable<{name: string;value: any;}>;/** EVENT - Form reset - resets form to initial values */reset: EventCallable<void>;/** EVENT - Field reset - resets field to initial value */resetField: EventCallable<string>;/** EVENT - Reset untouched fields to initial values */resetUntouched: EventCallable<string[]>;/** EVENT - Set form config */setActive: EventCallable<{name: string;value: boolean;}>;/** EVENT - Set form config */setConfig: EventCallable<IFormConfig>;/** EVENT - Set field config */setFieldConfig: EventCallable<IFieldConfig>;/** EVENT - Form update field values */setValues: EventCallable<IRValues>;/** EVENT - Form merge errors - merges provided errors into existing $errors store */setErrors: EventCallable<IRErrors>;/** EVENT - Form replace errors - replaces all $errors with provided errors (useful for server validation) */replaceErrors: EventCallable<IRErrors>;/** * EFFECT - Form submit - callback will be called with form values if form is valid * or if callback returns promise reject with errors, will highlight them in the form */submit: Effect<ISubmitArgs,ISubmitResponseSuccess,ISubmitResponseError>;/** EVENT - Form validate trigger */validate: EventCallable<IValidationParams>;}/** * setErrors vs replaceErrors: * - setErrors: Merges errors into existing $errors (preserves unrelated field errors) * - replaceErrors: Completely replaces $errors with new errors (clears all existing errors) * * **Important**: Submit validation uses `replaceErrors` - server errors from `onSubmit` reject * will **replace** all client validation errors, not merge with them. * * Example: * // Current errors: { name: ['Required'] } * setErrors({ email: ['Invalid'] }) * // Result: { name: ['Required'], email: ['Invalid'] } * * replaceErrors({ email: ['Invalid'] }) * // Result: { email: ['Invalid'] } * * // Submit reject (uses replaceErrors): * onSubmit: async (values) => { * throw { email: 'Already exists' }; // Clears name error, only shows email * } * * Use cases: * - setErrors: Add field errors without clearing others (e.g., adding server errors to existing client errors) * - replaceErrors: Server validation response (clear all client errors, show only server errors) - **used by submit** */

Methods / Hooks

import{getForm,useFormInstance}from'efx-forms';import{useForm}from'efx-forms/useForm';import{useFormData}from'efx-forms/useFormData';import{useFormValues}from'efx-forms/useFormValues';import{useFormStore}from'efx-forms/useFormStore';import{useFormStores}from'efx-forms/useFormStores';import{useFormMethods}from'efx-forms/useFormMethods';import{useField}from'efx-forms/useField';import{useFieldData}from'efx-forms/useFieldData';import{useFieldStore}from'efx-forms/useFieldStore';import{useFieldMethods}from'efx-forms/useFieldMethods';import{useStoreProp}from'efx-forms/useStoreProp';/** * Return form by name * @type (config: IFormConfig) => IForm */constformOne=getForm({name: 'form-one'});/** * Hook - return form (from context) data/methods or provided form by name. * Form name is needed when hook is used outside of the form context * or refers to another form. * Result includes all form data in plain objects and units in scope * @type (formName?: string) => ReturnType<typeof useForm> */constformTwo=useForm();/** * Hook - return form (from context) data or provided form by name. * Form name is needed when hook is used outside of the form context * or refers to another form. * Result includes all form data in plain objects and units in scope * @type (formName?: string) => ReturnType<typeof useFormData> */constformThree=useFormData();/** * Hook - return form (from context) instance or provided form by name. * Form name is needed when hook is used outside of the form context * or refers to another form. * Result contains all form stores and units, use useUnit to get values * @type (formName?: string) => ReturnType<typeof useFormInstance> */constformInst=useFormInstance();/** * Hook - return form (from context) store values or from provided form. * Form name is needed when hook is used outside of the form context * or refers to another form. * @type (store: string, formName?: string) => IFormErrors */constformErrors=useFormStore('$errors');/** * Hook - return form (from context) stores values array or from * provided form. Form name is needed when hook is used outside of the * form context or refers to another form. * @type (store: string[], formName?: string) => IFormErrors */const[errors,values]=useFormStores(['$errors','$values']);/** * Hook - return form (from context) values or from provided form. * Form name is needed when hook is used outside of the form context * or refers to another form. * @type (formName?: string) => IFormValues */constformValues=useFormValues();/** * Hook - return form (from context) methods or from provided form. * Form name is needed when hook is used outside of the form context * or refers to another form. * @type (formName?: string) => ReturnType<typeof useFormMethods> */constformMethods=useFormMethods();/** * Hook - return field data and methods combined * Form name is needed when hook is used outside of the form context * or refers to another form. * @type (name: string, formName?: string) => { * value: any, * active: boolean, * dirty: boolean, * error: string | null, * errors: string[] | null, * reset: () => void, * validate: () => void, * setActive: (value: boolean) => void, * setValue: (value: any) => void, * change: (value: any) => void, * setConfig: (cfg: IFieldConfig) => void * } */constfield=useField('user.name');// Returns: { value, active, dirty, error, errors, reset, validate, setActive, setValue, change, setConfig }/** * Hook - return field value by name * Form name is needed when hook is used outside of form context * or refers to another form. * @type (name: string, formName?: string) => ReturnType<typeof useFieldData> */constfieldData=useFieldData('field-one');/** * Hook - return field store value * Form name is needed when hook is used outside of form context * or refers to another form. * @type (data: { * store: string; * name: string; * formName?: string; * defaultValue?: any; * }) => ReturnType<typeof useFieldStore> */constfieldActive=useFieldStore({store: '$active',name: 'user.name',formName: 'login',defaultValue: '',});/** * Hook - return field methods only * Form name is needed when hook is used outside of form context * or refers to another form. * @type (name: string, formName?: string) => ReturnType<typeof useFieldMethods> */constfieldMethods=useFieldMethods('user.name');// Returns: { reset, validate, setActive, setValue, change, setConfig }/** * Hook - return store value * @type ( * store: Store, * prop: string, * defaultValue?: any, * ) => ReturnType<typeof useStoreProp> */conststorePropValue=useStoreProp(form.$values,'user.name','');

Utils

import{// effector forms domain, usefull for logging / debuggingdomain,truthyFy,shapeFy,truthyFyStore,shapeFyStore,flattenObjectKeys,}from'efx-forms/utils';/** * Return only truthy values from object * @type (values: IFormValues) => IFormValues */consttruthyValues=truthyFy(values);/** * Return flat to shaped values * @type (values: IFormValues) => {} * @example * { 'user.name': 'John' } => { user: { name: 'John } } */constshapedValues=shapeFy(values);/** * Return effector store with truthy values * @type ($values: Store): Store => $truthyValues */const$truthyStore=truthyFyStore($values);/** * Return effector store with shaped values * @type ($values: Store): Store => $shapedValues */const$shapedStore=shapeFyStore($values);/** * Return flatten one level object keys/values * helper for the nested initial values * @type (values: Record<string, any>): Record<string, any> => ({}) */constinitialValues=flattenObjectKeys(values);

Validators

Check validators.d.ts file to see all built-in validators and their arguments

import{required,email}from'efx-forms/validators';constformValidations={'user.name': [required()],'user.email': [required({msg: 'Email is required'}),// custom messageemail(),],}

Releases

Used by

Contributors

Languages