If you working with forms in an Effector-application, there is often a lot of boilerplate code, such as:
export const emailChanged = login.event()
export const passwordChanged = login.event()
export const submitForm = login.event()
export const $email = login.store('')
export const $password = login.store('')
export const $form = combine({
password: $password,
email: $email,
})
$email.on(emailChanged, (_, email) => email)
$password.on(passwordChanged, (_, password) => password)
If you also need validation, it really hurts. This library was created to improve the "form experience" in an effector application by generating form state from declarative configuration.
The library comes with hooks for react/react-native, however you can use it with VueJS or Forest as well (in the case of VueJS, you have to connect it to the view layer yourself).
Good typescript support! :heart: :v: :+1:
If you are using SSR, add effector-forms to the babel plugin config
{
"plugins": [
[
"effector/babel-plugin",
{
"factories": [
"effector-forms"
]
}
]
],
}
Model:
import { restore, sample, createEffect } from "effector"
import { createForm } from 'effector-forms'
export const loginForm = createForm({
fields: {
email: {
init: "", // field's store initial value
rules: [
{
name: "email",
validator: (value: string) => /\S+@\S+\.\S+/.test(value)
},
],
},
password: {
init: "", // field's store initial value
rules: [
{
name: "required",
validator: (value: string) => Boolean(value),
}
],
},
},
validateOn: ["submit"],
})
export const loginFx = createEffect()
sample({
clock: loginForm.formValidated,
target: loginFx,
})
The createForm factory creates stores and events of the form and binds them by declarative configuration. formValidated effector event will be triggered when the form is submitted (if all its fields are valid). Option validateOn sets an array of triggers by which the form values will be validated. Possible conditions: submit, blur, change. The value of each field and validation errors are stored in effector stores.
After we have created the form, we can connect it to the view using the useForm hook:
import { useForm } from 'effector-forms'
import { useUnit } from 'effector-react'
import { loginForm, loginFx } from '../model'
export const LoginForm = () => {
const { fields, submit, eachValid } = useForm(loginForm)
const pending = useUnit(loginFx.pending)
const onSubmit = (e) => {
e.preventDefault()
submit()
}
return (
<form onSubmit={onSubmit)}>
<input
type="text"
value={fields.email.value}
disabled={pending}
onChange={(e) => fields.email.onChange(e.target.value)}
/>
{fields.email.errorText({
"email": "you must enter a valid email address",
})}
<input
type="password"
value={fields.password.value}
disabled={pending}
onChange={(e) => fields.password.onChange(e.target.value)}
/>
{fields.password.errorText({
"required": "password required"
})}
<button
disabled={!eachValid || pending}
type="submit"
>
Login
</button>
</form>
)
}
The effector-forms entities implement the @@unitShape protocol! This means that instead of useForm and useField hooks, you can connect the form to the view via "useUnit".
React's example:
import { useUnit } from "effector-react"
import { createForm } from 'effector-forms'
const loginForm = createForm({
fields: {
email: {
init: "",
rules: [],
},
password: {
init: "",
rules: [],
},
},
validateOn: ["submit"],
})
const LoginForm = () => {
const email = useUnit(loginForm.fields.email)
const password = useUnit(loginForm.fields.password)
const form = useUnit(loginForm)
const submitHandler = (e) => {
e.preventDefault()
form.submit()
}
return (
<form onSubmit={submitHandler}>
<input
type="text"
value={email.value}
onChange={(e) => email.onChange(e.target.value)}
/>
<input
type="password"
value={password.value}
onChange={(e) => password.onChange(e.target.value)}
/>
</form>
)
}
import { createForm, useField } from 'effector-forms'
export const form = createForm({
fields: {
email: {
init: "",
rules: emailRules,
},
password: {
init: "",
rules: passwordRules,
},
},
})
const Email = () => {
const { value, onChange } = useField(form.fields.email)
return (
<input
type="text"
placeholder="email"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)
}
const Password = () => {
const { value, onChange } = useField(form.fields.password)
return (
<input
type="password"
placeholder="password"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)
}
const Form = () => {
const onSubmit = (e) => {
e.preventDefault()
form.submit()
}
<form onSubmit={onSubmit}>
<Email />
<Password />
<button type="submit">Login</button>
</form>
}
You can access the state of fields, form and its events directly in the model:
const form = createForm(formConfig)
form.fields.email.$value.watch(() => {
console.log("username field changed")
})
form.fields.email.$errors.watch((errors) => {
console.log(errors)
})
form.$values.watch(() => {
console.log("form changed")
})
form.$eachValid.watch((eachValid) => {
if (eachValid) {
console.log("form valid")
}
})
form.fields.email.onChange("value")
sample({
clock: form.fields.email.onChange,
target: someEvent,
})
Sometimes we need to prevent form submission conditionally, for example if we have an error from the server. To do this, you can use the filter option by passing a boolean Store:
export const loginFx = createEffect()
const $serverError = restore(loginFx.failData, null)
export const loginForm = createForm({
filter: $serverError.map((error) => error === null),
fields,
validateOn: ["submit"],
})
$serverError.reset(form.$values.updates)
sample({
clock: form.formValidated,
target: loginFx,
})
In this example the formValidated event will be triggered after submit only if the $serverError is null & form is valid.
It is often necessary to set the value of the form by an event. This is useful for initializing initial field values. In this case you can use setForm event:
type User = {
username: string
about?: string
birhDate?: string
}
const getUserProfileFx = createEffect<void, User, Error>()
const form = createForm({
fields: {
username: {
init: "" as string,
},
about: {
init: "" as string,
},
birthDate: {
init: null as string | null,
},
},
})
sample({
clock: getUserProfileFx.doneData,
target: form.setForm,
})
In case you get values from the backend, use the form.setInitialForm event instead of form.setForm. This event changes both the value and the initial values. The $isDirty flag will be false.
You can validate different form fields against different triggers:
const form = createForm({
validateOn: ["submit"],
fields: {
email: {
init: "",
rules: emailRulesArr,
validateOn: ["blur"],
},
username: {
init: "",
rules: usernameRulesArr,
},
password: {
init: "",
rules: passwordRulesArr,
validateOn: ["change"],
},
},
})
const RegisterForm = () => {
const { submit, fields } = useForm(form)
const onSubmit = (e) => {
e.preventDefault()
submit()
}
return (
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="email"
value={fields.email.value}
onBlur={() => fields.email.onBlur()}
onChange={(e) => fields.email.onChange(e.target.value)}
/>
<input
type="text"
placeholder="username"
value={fields.username.value}
onChange={(e) => fields.username.onChange(e.target.value)}
/>
<input
type="password"
placeholder="password"
value={fields.password.value}
onChange={(e) => fields.password.onChange(e.target.value)}
/>
<button type="submit">Register</button>
</form>
)
}
In this example: * the email field will be validated on blur and submit * the password field will be validated on change and submit * the username field will be validated on submit only
Note that you must directly trigger the onBlur event on the email field. This is due to the fact that the form state is separated from the view, and the form state "knows nothing" about input field event.
You can also pass multiple triggers:
const form = createForm({
validateOn: ["submit", "blur"],
fields: formFields,
})
It so happens that to validate a field, you need to know the value of another field. In this case you can use the second argument of the validator function:
const form = createForm({
fields: {
email: {
init: "",
rules: emailRules,
},
password: {
init: "",
rules: passwordRules,
},
confirmation: {
init: "",
rules: [
{
name: "confirmation",
validator: (confirmation, { password }) => confirmation === password
},
],
},
}
})
If you need to have all units of a form (fields stores and events) in a domain, you can pass the domain option. This can be useful for SSR.
const myDomain = createDomain()
const form = createForm({
domain: myDomain,
fields: formFields,
})
Validation rule is an object with name and validator properties:
{
name: "required",
validator: (value) => Boolean(value),
}
To reuse validation rules, put them in a separate module. Very often the validator needs a parameter. For this reason, we recommend using "factory-style" when creating your validators library:
export const rules = {
required: (): Rule<string> => ({
name: "required",
validator: (value) => Boolean(value),
}),
email: (): Rule<string> => ({
name: "email",
validator: (value) => /\S+@\S+\.\S+/.test(value)
}),
minLength: (min: number): Rule<string> => ({
name: "minLength",
validator: (value) => value.length >= min
}),
maxLength: (max: number): Rule<string> => ({
name: "maxLength",
validator: (value) => value.length <= max
}),
}
import { rules } from 'my-validation-rules'
const form = createForm({
fields: {
email: {
init: "",
rules: [
rules.email(),
],
},
password: {
init: "",
rules: [
rules.required(),
rules.minLength(3),
],
},
},
})
Sometimes you need to calculate validation rules based on the current state of the form. In this case, you can pass a factory function to rules:
const form = createForm({
fields: {
needNotification: {
init: false,
rules: [],
},
email: {
init: "" as string,
rules: (value: string, form) => form.needNotification ? [email()] : [],
},
},
validateOn: ["submit"],
})
Each field has two boolean stores: $isTouched and $isDirty.
$isTouched true if this field has ever changed (the onChange event has been called at least once). false otherwise.
isDirty true if the current value is different from the initial (===). false otherwise.
Both fields are reset on form.reset event.
```ts const form = createForm({ fields: { email: { init: "email@example.com", rules: [ rules.required(), ], }, } })
form.fields.email.onChange("") console.log(form.fields.email.$isTouched.getState()) // true console.
$ claude mcp add effector-forms \
-- python -m otcore.mcp_server <graph>