Effector JS forms
📚 Documentation: https://darianstlex.github.io/efx-forms/
$ npm install efx-formsPeer dependencies - library depends on:
react effector effector-react lodash
import { Form, Field } from 'efx-forms';
import { FormDataProvider } from 'efx-forms/FormDataProvider';
import { required, email } from 'efx-forms/validators';
const Input = ({ id, label, error, errors, value, ...props }) => (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} value={value || ''} type="text" {...props} />
<span>{error}</span>
</div>
)
const TextField = (props) => <Field Field={Input} {...props} />
const validators = {
name: [required()],
}
const Page = () => {
const submit = (values) => {
console.log(values);
}
return (
<Form name="user-form" onSubmit={submit} validators={validators}>
<TextField name="name" label="Name" />
<TextField
name="email"
label="Email"
type="email"
validators={[
required({ msg: `Hey, email is required` }),
email(),
]}
/>
{[0, 1, 2].map((idx) => (
<TextField
key={idx}
name={`address[${idx}]`}
label={`Address ${idx + 1}`}
/>
))}
<FormDataProvider>
{({ values }) => (
<div>
<pre>JSON.stringify(values)</pre>
<pre>JSON.stringify(shapeFy(values))</pre>
</div>
)}
</FormDataProvider>
<button type="submit">Submit</button>
</Form>
)
}// Form values
values = {
'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',
]
}interface Form {
// Form name - required, used to get form instance outside of context
name: 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: false
skipClientValidation?: boolean;
// Form initial values - field initialValue is in priority
initialValues?: { fieldName: 'value' }
// Keep form data on unmount
// Default: false
keepOnUnmount: boolean;
// PROPERTY - serialize - serialize stores
// not reactive, initialized only once on form creation
// Default: false
serialize?: boolean;
// Set fields validation behavior onBlur
// Default: true
validateOnBlur?: boolean;
// Set fields validation behavior onChange
// Default: false
validateOnChange?: boolean;
// Disable reinit on initialValue change
disableFieldsReinit?: boolean;
// Validators config per field - field validators are in priority
validators?: {
fieldName: [
(value: any, values: Record<string, any>) => string | false,
]
};
}interface Field {
// Field name - required, used to register/get field in the form
name: string,
// Field initial value - used on initial load and reset
// default = ''
initialValue?: any;
// Transform value before set to store
parse?: (value: any) => any;
// Format value before displaying
format?: (value: any) => any;
// Passive field does not update its active state and config
passive?: boolean;
// Validators array - applied on validation
validators?: TFieldValidator[];
// Set validation behaviour onBlur, overrides form value
// Default: true
validateOnBlur?: boolean;
// Set validation behaviour onChange, overrides form value
// Default: false
validateOnChange?: boolean;
// Disable reinit on initialValue change
disableFieldReinit?: boolean;
// Field component - component to be used as form field
Field: ReactComponent<any>;
// Form name - if field belongs to a different form or used outside
// of the form context
formName?: string;
}Conditional rendering based on form values
interface IfFormValues {
children?: ReactNode;
// Form name - used to get form values,
// if not provided will be taken from context
form?: string;
// Condition check - accepts form values and return boolean,
// if true render children
check: (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: 0
updateDebounce?: number;
// Render prop - accepts form values and return react element
// if defined will be used instead of children
render?: (values: IRValues) => ReactElement;
}import { IfFormValues } from 'efx-forms/IfFormValues';
const ConditionalRender = () => (
<IfFormValues check={({ age }) => age > 21 }>
<div>Hey, I am here</div>
</IfFormValues>
);
const ConditionalRenderProp = () => (
<IfFormValues
check={({ age }) => age > 21 }
render={({ age, name }) => <div>Hi, I am {name} - {age}</div>}
/>
);Subscribe for form values changes
interface FormDataProvider {
// Render function - provides all subscribed data
children: (values: ReturnType<typeof useFormData>) => ReactNode;
// Form name if used outside of context or refers to another form
name?: string;
}import { FormDataProvider } from 'efx-forms/FormDataProvider';
const FormData = () => (
<FormDataProvider>
{({ values, errors }) => <div>{values} - {errors}</div>}
</FormDataProvider>
);Conditional rendering based on field value
interface IfFieldValue {
children?: ReactNode;
// Field name
field: string;
// Form name - used to get form values,
// if not provided will be taken from context
formName?: string;
// Condition check - accepts form values and return boolean,
// if true render children
check: (value: any) => boolean;
// Render prop - accepts form values and return react element
// if defined will be used instead of children
render?: (values: any) => ReactElement;
}import { IfFieldValue } from 'efx-forms/IfFieldValue';
const ConditionalRender = () => (
<IfFieldValue check={(age) => age > 21 }>
<div>Hey, I am here</div>
</IfFieldValue>
);
const ConditionalRenderProp = () => (
<IfFieldValue
check={(age) => age > 21 }
render={(age) => <div>Hi, I am {age}</div>}
/>
);Subscribe for field value changes
interface FieldDataProvider {
// Render function - provides all subscribed data
children: (values: ReturnType<typeof useFieldData>) => ReactNode;
// Field name to get stores values from
name: string;
// Form name if used outside of context or refers to another form
formName?: string;
}import { FieldDataProvider } from 'efx-forms/FieldDataProvider';
const FieldData = () => (
<FieldDataProvider name="user.name">
{({ value, active }) => <div>{value} - {active}</div>}
</FieldDataProvider>
);Form Instance
interface FormInstance {
/** 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**
*/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
*/
const formOne = 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>
*/
const formTwo = 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>
*/
const formThree = 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>
*/
const formInst = 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
*/
const formErrors = 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
*/
const formValues = 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>
*/
const formMethods = 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
* }
*/
const field = 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>
*/
const fieldData = 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>
*/
const fieldActive = 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>
*/
const fieldMethods = useFieldMethods('user.name');
// Returns: { reset, validate, setActive, setValue, change, setConfig }
/**
* Hook - return store value
* @type (
* store: Store,
* prop: string,
* defaultValue?: any,
* ) => ReturnType<typeof useStoreProp>
*/
const storePropValue = useStoreProp(form.$values, 'user.name', '');import {
// effector forms domain, usefull for logging / debugging
domain,
truthyFy,
shapeFy,
truthyFyStore,
shapeFyStore,
flattenObjectKeys,
} from 'efx-forms/utils';
/**
* Return only truthy values from object
* @type (values: IFormValues) => IFormValues
*/
const truthyValues = truthyFy(values);
/**
* Return flat to shaped values
* @type (values: IFormValues) => {}
* @example
* { 'user.name': 'John' } => { user: { name: 'John } }
*/
const shapedValues = 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> => ({})
*/
const initialValues = flattenObjectKeys(values);Check validators.d.ts file to see all built-in validators and their arguments
import { required, email } from 'efx-forms/validators';
const formValidations = {
'user.name': [required()],
'user.email': [
required({ msg: 'Email is required' }), // custom message
email(),
],
}