در این راهنما ساخت فرم با Formisch را بررسی میکنیم؛ یک کتابخانهٔ سبک، Schema-first و کاملاً Type-safe برای ساخت فرم در React. فرمها را با کامپوننت <Field /> میسازیم، با Schemaهای Valibot اعتبارسنجی میکنیم، خطاها را مدیریت میکنیم و دسترسپذیری فرم را بهبود میدهیم.
در این Demo یک فرم ساده با یک ورودی متنی و یک Textarea میسازیم. هنگام ارسال فرم، دادههای فرم اعتبارسنجی میشوند و خطاهای احتمالی نمایش داده خواهند شد.
نکته: برای این Demo، اعتبارسنجی پیشفرض مرورگر را عمداً غیرفعال کردهایم تا نحوهٔ کار Schema Validation و مدیریت خطاها در Formisch مشخص باشد. در پروژهٔ واقعی بهتر است اعتبارسنجی پایهٔ مرورگر را فعال نگه دارید.
این فرم از Formisch برای مدیریت فرم بهصورت Headless و Schema-first استفاده میکند. فرم را با کامپوننت <Field /> میسازیم که آزادی کاملی برای کنترل Markup و Styling در اختیار شما قرار میدهد.
useForm در Formisch برای مدیریت وضعیت فرم.<Form /> برای قرار دادن منطق ارسال روی عنصر native <form>.<Field /> مبتنی بر Render Prop برای ورودیهای Controlled.Formisch عملیات فرم را بهصورت توابع سطح بالا ارائه میکند، نه متدهای یک Object مربوط به فرم. فقط توابع موردنیاز خود را Import کنید:
import { getInput, insert, reset, submit } from "@formisch/react"همهٔ این توابع Signature یکسانی دارند: پارامتر اول همیشه Store فرم و پارامتر دوم، در صورت نیاز، یک Object تنظیمات است.
// خواندن مقدار یک فیلد
const email = getInput(form, { path: ["email"] })
// بازنشانی فرم با مقادیر اولیهٔ جدید
reset(form, { initialInput: { email: "", password: "" } })
// جابهجایی یک آیتم در آرایهٔ فیلد
move(form, { path: ["items"], from: 0, to: 3 })این ساختار باعث میشود API در تمام توابع انعطافپذیر و یکدست باقی بماند. در ادامه همین الگوی (form, config) را در بخشهای مختلف میبینید؛ از خواندن وضعیت فرم (getInput، getErrors) و تغییر وضعیت (setInput، setErrors) گرفته تا کنترل فرم (submit، validate، focus) و عملیات آرایه (insert، remove، move، swap، replace).
برای جزئیات بیشتر، مرجع کامل متدها را ببینید.
در اینجا یک نمونهٔ ساده از فرمی را میبینید که از <Field /> در Formisch و <Field /> در FarsiUI استفاده میکند.
<Form of={form} onSubmit={handleSubmit}>
<FieldGroup>
<FormischField of={form} path={["title"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-title">عنوان باگ</FieldLabel>
<Input
{...field.props}
id="form-title"
value={field.input}
aria-invalid={field.errors !== null}
placeholder="دکمهٔ ورود در موبایل کار نمیکند"
autoComplete="off"
/>
<FieldDescription>
یک عنوان کوتاه برای گزارش باگ بنویسید.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>
</FieldGroup>
</Form>نکته: Formisch کامپوننت Field مخصوص خودش را دارد. برای جلوگیری از
تداخل نام با Field در FarsiUI، در مثالهای این راهنما آن را با نام
FormischField Import میکنیم و Field مربوط به FarsiUI را با نام
اصلی نگه میداریم. در پروژهٔ خودتان میتوانید هرکدام را Alias کنید؛ فقط در تمام
کد یکدست عمل کنید.
ابتدا ساختار فرم را با یک Schema از Valibot تعریف میکنیم. Formisch تمام Typeهای ورودی و خروجی را مستقیماً از همین Schema استنتاج میکند.
import * as v from "valibot"
const FormSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "عنوان باگ باید حداقل ۵ کاراکتر باشد."),
v.maxLength(32, "عنوان باگ باید حداکثر ۳۲ کاراکتر باشد.")
),
description: v.pipe(
v.string(),
v.minLength(20, "توضیحات باید حداقل ۲۰ کاراکتر باشد."),
v.maxLength(100, "توضیحات باید حداکثر ۱۰۰ کاراکتر باشد.")
),
})در مرحلهٔ بعد از Hook useForm در Formisch برای ساخت Instance فرم استفاده میکنیم. Schema مستقیماً به useForm داده میشود و نیازی به Resolver جداگانه نیست.
import { Form, Field as FormischField, useForm } from "@formisch/react"
import type { SubmitHandler } from "@formisch/react"
import * as v from "valibot"
const FormSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "عنوان باگ باید حداقل ۵ کاراکتر باشد."),
v.maxLength(32, "عنوان باگ باید حداکثر ۳۲ کاراکتر باشد.")
),
description: v.pipe(
v.string(),
v.minLength(20, "توضیحات باید حداقل ۲۰ کاراکتر باشد."),
v.maxLength(100, "توضیحات باید حداکثر ۱۰۰ کاراکتر باشد.")
),
})
export function BugReportForm() {
const form = useForm({
schema: FormSchema,
initialInput: {
title: "",
description: "",
},
})
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
// با مقادیر اعتبارسنجیشدهٔ فرم کاری انجام دهید.
console.log(output)
}
return (
<Form of={form} onSubmit={handleSubmit}>
{/* ... */}
{/* فرم را اینجا بسازید */}
{/* ... */}
</Form>
)
}کامپوننت <Form /> یک عنصر native <form> را Wrap میکند، event.preventDefault() را اجرا میکند، اعتبارسنجی را انجام میدهد و فقط در صورت معتبر بودن دادهها، onSubmit را فراخوانی میکند. مقدار output نیز Type خود را مستقیماً از Schema دریافت میکند.
حالا میتوانیم فرم را با استفاده از <Field /> در Formisch و <Field /> در FarsiUI بسازیم.
همین! حالا یک فرم کاملاً دسترسپذیر با اعتبارسنجی سمت Client دارید.
هنگام ارسال فرم، تابع handleSubmit با دادههای اعتبارسنجیشدهٔ فرم اجرا میشود. اگر دادههای فرم معتبر نباشند، Formisch مقدار field.errors را برای هر فیلد نامعتبر پر میکند و خطاها در رابط کاربری نمایش داده میشوند.
Formisch دادههای فرم را با استفاده از Schema مربوط به Valibot که به useForm دادهاید اعتبارسنجی میکند. در اینجا Resolver وجود ندارد؛ Schema تنها منبع حقیقت برای اعتبارسنجی در زمان اجرا و Typeهای استاتیک است.
import { useForm } from "@formisch/react"
const FormSchema = v.object({
title: v.string(),
description: v.optional(v.string()),
})
export function ExampleForm() {
const form = useForm({
schema: FormSchema,
initialInput: {
title: "",
description: "",
},
})
}Formisch اعتبارسنجی اولیه را از اعتبارسنجیهای بعدی جدا میکند. این رفتار با Options validate و revalidate در useForm تنظیم میشود.
const form = useForm({
schema: FormSchema,
validate: "blur",
revalidate: "input",
})برای نمایش خطا در کنار فیلد از <FieldError /> استفاده کنید. Formisch خطاها را بهصورت آرایهای از Stringها برمیگرداند؛ بنابراین باید آنها را به ساختاری که <FieldError /> انتظار دارد تبدیل کنید.
برای Styling و دسترسپذیری:
data-invalid را به <Field /> اضافه کنید.aria-invalid را به کنترل فرم، مانند <Input />، <SelectTrigger />، <Checkbox /> و موارد مشابه اضافه کنید.<FormischField of={form} path={["email"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-email">ایمیل</FieldLabel>
<Input
{...field.props}
id="form-email"
value={field.input}
type="email"
aria-invalid={field.errors !== null}
/>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>Formisch دو روش برای اتصال یک فیلد به یک عنصر ارائه میدهد:
<Input /> و <Textarea />: مقدار field.props را Spread کنید و value={field.input} را قرار دهید. Formisch مواردی مانند name، ref، onChange، onBlur و onFocus را بهصورت خودکار مدیریت میکند.<Select />، <Checkbox />، <RadioGroup /> و <Switch /> مبتنی بر Radix: مقدار را از field.input بخوانید و برای بهروزرسانی آن، field.onChange(value) را فراخوانی کنید.field.props را Spread کنید و value={field.input} را قرار دهید.aria-invalid را به <Input /> و Prop data-invalid را به <Field /> اضافه کنید.<FormischField of={form} path={["username"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-username">نام کاربری</FieldLabel>
<Input
{...field.props}
id="form-username"
value={field.input}
aria-invalid={field.errors !== null}
/>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>field.props را Spread کنید و value={field.input} را قرار دهید.aria-invalid را به <Textarea /> و Prop data-invalid را به <Field /> اضافه کنید.<FormischField of={form} path={["about"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-about">دربارهٔ شما</FieldLabel>
<Textarea
{...field.props}
id="form-about"
value={field.input}
aria-invalid={field.errors !== null}
placeholder="من مهندس نرمافزار هستم..."
className="min-h-16"
/>
<FieldDescription>
اطلاعات بیشتری دربارهٔ خودتان بنویسید. از این اطلاعات برای شخصیسازی
تجربهٔ شما استفاده خواهد شد.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>field.input را بخوانید و در onValueChange مربوط به <Select />، field.onChange را فراخوانی کنید.aria-invalid را به <SelectTrigger /> و Prop data-invalid را به <Field /> اضافه کنید.<FormischField of={form} path={["language"]}>
{(field) => (
<Field orientation="responsive" data-invalid={field.errors !== null}>
<FieldContent>
<FieldLabel htmlFor="form-language">زبان گفتاری</FieldLabel>
<FieldDescription>
برای دریافت نتیجهٔ بهتر، زبانی را انتخاب کنید که به آن صحبت میکنید.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldContent>
<Select value={field.input} onValueChange={field.onChange}>
<SelectTrigger
id="form-language"
aria-invalid={field.errors !== null}
className="min-w-[120px]"
dir="rtl"
>
<SelectValue placeholder="انتخاب کنید" />
</SelectTrigger>
<SelectContent position="item-aligned" dir="rtl">
<SelectItem value="auto">خودکار</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)}
</FormischField>field.input را بخوانید و از طریق onCheckedChange و field.onChange آن را بهروزرسانی کنید.aria-invalid را به <Checkbox /> و Prop data-invalid را به <Field /> اضافه کنید.data-slot="checkbox-group" را به <FieldGroup /> اضافه کنید.<FormischField of={form} path={["tasks"]}>
{(field) => (
<FieldSet>
<FieldLegend variant="label">وظایف</FieldLegend>
<FieldDescription>
برای بهروزرسانی وظایفی که ایجاد کردهاید، اعلان دریافت کنید.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={field.errors !== null}
>
<Checkbox
id={`form-checkbox-${task.id}`}
aria-invalid={field.errors !== null}
checked={field.input?.includes(task.id) ?? false}
onCheckedChange={(checked) => {
const current = field.input ?? []
field.onChange(
checked === true
? [...current, task.id]
: current.filter((value) => value !== task.id)
)
}}
/>
<FieldLabel
htmlFor={`form-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldSet>
)}
</FormischField>field.input را بخوانید و در onValueChange، متد field.onChange را فراخوانی کنید.aria-invalid را به کامپوننت <RadioGroupItem /> و Prop data-invalid را به <Field /> اضافه کنید.<FormischField of={form} path={["plan"]}>
{(field) => (
<FieldSet>
<FieldLegend>پلن</FieldLegend>
<FieldDescription>
هر زمان بخواهید میتوانید پلن خود را ارتقا یا کاهش دهید.
</FieldDescription>
<RadioGroup value={field.input} onValueChange={field.onChange}>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`form-radiogroup-${plan.id}`}>
<Field
orientation="horizontal"
data-invalid={field.errors !== null}
>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-radiogroup-${plan.id}`}
aria-invalid={field.errors !== null}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldSet>
)}
</FormischField>field.input را بخوانید و در onCheckedChange، متد field.onChange را فراخوانی کنید.aria-invalid را به کامپوننت <Switch /> و Prop data-invalid را به <Field /> اضافه کنید.<FormischField of={form} path={["twoFactor"]}>
{(field) => (
<Field orientation="horizontal" data-invalid={field.errors !== null}>
<FieldContent>
<FieldLabel htmlFor="form-twoFactor">
احراز هویت چندمرحلهای
</FieldLabel>
<FieldDescription>
احراز هویت چندمرحلهای را برای افزایش امنیت حساب فعال کنید.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldContent>
<Switch
id="form-twoFactor"
checked={field.input ?? false}
onCheckedChange={field.onChange}
aria-invalid={field.errors !== null}
dir="ltr"
/>
</Field>
)}
</FormischField>در این مثال یک فرم پیچیدهتر با چندین فیلد و اعتبارسنجی میسازیم.
Formisch یک تابع سطحبالای reset در اختیار شما قرار میدهد. با ارسال Store فرم به این تابع، فرم به مقادیر اولیه بازنشانی میشود.
<Button type="button" variant="outline" onClick={() => reset(form)}>
بازنشانی
</Button>همچنین میتوانید مقادیر اولیهٔ جدیدی تعیین کنید یا فرم را طوری بازنشانی کنید که ورودی فعلی کاربر حفظ شود:
// بازنشانی با مجموعهای جدید از مقادیر اولیه
reset(form, {
initialInput: { title: "", description: "" },
})
// همگامسازی مقدار پایه با دادههای جدید سرور و حفظ تغییرات کاربر
reset(form, {
initialInput: serverData,
keepInput: true,
})Formisch یک کامپوننت <FieldArray /> و مجموعهای از توابع کمکی برای مدیریت فیلدهای آرایهای پویا ارائه میدهد. هر زمان نیاز دارید آیتمهایی را اضافه، حذف یا جابهجا کنید، از آن استفاده کنید.
<FieldArray /> مانند <Field /> از الگوی Render Prop استفاده میکند. آرایهٔ items برای هر آیتم یک Key پایدار دارد که باید از آن بهعنوان key در React استفاده کنید.
import {
Field as FormischField,
FieldArray,
insert,
remove,
} from "@formisch/react"
export function ExampleForm() {
// ... تنظیمات فرم
return (
<FieldArray of={form} path={["emails"]}>
{(fieldArray) => (
<FieldGroup className="gap-4">
{fieldArray.items.map((item, index) => (
<FormischField
key={item}
of={form}
path={["emails", index, "address"]}
>
{(field) => /* ... */}
</FormischField>
))}
</FieldGroup>
)}
</FieldArray>
)
}فیلدهای آرایهای را داخل یک <FieldSet /> همراه با <FieldLegend /> و <FieldDescription /> قرار دهید.
<FieldSet className="gap-4">
<FieldLegend variant="label">آدرسهای ایمیل</FieldLegend>
<FieldDescription>
حداکثر ۵ آدرس ایمیل اضافه کنید تا بتوانیم با شما در تماس باشیم.
</FieldDescription>
<FieldGroup className="gap-4">{/* آیتمهای آرایه اینجا قرار میگیرند */}</FieldGroup>
</FieldSet>برای اضافه کردن آیتم جدید به آرایه از تابع insert استفاده کنید. بهصورت پیشفرض آیتم جدید به انتهای آرایه اضافه میشود. همچنین میتوانید با تعیین at، محل قرارگیری آیتم را مشخص کنید.
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
insert(form, { path: ["emails"], initialInput: { address: "" } })
}
disabled={fieldArray.items.length >= 5}
>
افزودن آدرس ایمیل
</Button>برای حذف یک آیتم از تابع remove همراه با at استفاده کنید.
import { remove } from "@formisch/react"
{
fieldArray.items.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(form, { path: ["emails"], at: index })}
aria-label={`حذف ایمیل ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)
}Formisch همچنین توابع move، swap و replace را برای جابهجایی و جایگزینی آیتمها ارائه میدهد. این توابع نیز از همان الگوی (form, config) استفاده میکنند.
برای محدود کردن فیلدهای آرایهای، از Validatorهای array و Pipelineهای اعتبارسنجی Valibot استفاده کنید.
const FormSchema = v.object({
emails: v.pipe(
v.array(
v.object({
address: v.pipe(
v.string(),
v.nonEmpty("یک آدرس ایمیل وارد کنید."),
v.email("یک آدرس ایمیل معتبر وارد کنید.")
),
})
),
v.minLength(1, "حداقل یک آدرس ایمیل اضافه کنید."),
v.maxLength(5, "حداکثر ۵ آدرس ایمیل میتوانید اضافه کنید.")
),
})