ردیف فرم (FormRow)
یک فیلد فرم — برچسب، کنترل، پیام خطا و توضیح — که چیدمانش را ظرف تعیین میکند، نه خود ردیف
معرفی
FormRow یک فیلد فرم است: برچسب، کنترل، پیام خطا و توضیح. خود ردیف چیدمانش را انتخاب نمیکند؛ ظرفی که
ردیف در آن است تعیین میکند:
| ظرف | برچسب | ردیف |
|---|---|---|
FormPage (صفحهٔ یک فرم) | بالای کنترل | ردیفی از کارت فرم، با جداکننده |
SettingsSection (بخش صفحهٔ تنظیمات) | کنار کنترل (از عرض md) | ردیفی از کارت بخش، با جداکننده |
دیالوگ یا پنل کناری (ردیفها در FormSection) | بالای کنترل | ستون ردیفها با فاصلهٔ 16 پیکسل بین ردیفها |
Switch در هر ظرف و هر عرضی روی خط برچسب و در انتهای آن مینشیند، و Checkbox پیش از برچسبش. کنترلهای
نردبان اندازه (Input، SelectTrigger، MultiSelect، DatePicker، NumberInputLocale) اندازهٔ کنترل فرم را
میگیرند (امروز sm، 34 پیکسل) و عرض ستون خودشان را پر میکنند؛ TagInput و Textarea ارتفاع خودشان را دارند.
FormRow روی FormItemLayout ساخته شده است؛ FormItemLayout جزء سطح
پایینتری است که چیدمانش را خودتان انتخاب میکنید.
چه زمانی استفاده کنیم:
- هر فیلد در صفحهٔ فرم، صفحهٔ تنظیمات یا فرم یک دیالوگ.
- هر فیلدی که با react-hook-form اعتبارسنجی میشود:
FormRowرا ازrenderدرFormFieldبرگردانید.
چه زمانی استفاده نکنیم:
- کنترلهای نوارابزار صفحه (جستوجو، فیلترها): آنها
searchوfiltersقالبListPageهستند (PageToolbarفقط درون یکCustomPage) و برچسب دیداری ندارند. - سطری که فیلد نیست (یک یادداشت، یک جدول کوچک) در کارت
FormPageیاSettingsSection: آن را در یکCardContent(از@partodata/ui/card) بگذارید تا فاصلهٔ داخلی کارت را بگیرد.
فرم بیرون از قالبهای صفحه، فرم یک دیالوگ یا پنل کناری است. دکمهٔ «هشدار تازه» را بزنید:
ردیفها در صفحهٔ فرم (FormPage) — هفت فیلد، پس در سه گروه:
استفاده
FormRow و FormSection از @partodata/ui/templates وارد میشوند، همراه قالبهای صفحه. با react-hook-form،
FormField از @partodata/ui/form فیلد را به فرم وصل میکند و FormRow آن را نمایش میدهد. فرم یک دیالوگ:
DialogHeader، بعد یک <form id> با ردیفها در یک FormSection (بی عنوان وقتی فرم یک گروه است)، و بعد
DialogFooter با «انصراف» و دکمهٔ ثبت، که با form به id فرم وصل است:
'use client'
import * as React from 'react'
import { useForm } from 'react-hook-form'
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Switch,
} from '@partodata/ui'
import { Form, FormField } from '@partodata/ui/form'
import { FormRow, FormSection } from '@partodata/ui/templates'
type Values = { name: string; email: boolean }
export function NewAlertDialog({
open,
onOpenChange,
onSave,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onSave: (values: Values) => void
}) {
const form = useForm<Values>({ defaultValues: { name: '', email: true } })
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>هشدار تازه</DialogTitle>
<DialogDescription>هشدار وقتی ساخته میشود که منشنی با این قاعده پیدا شود.</DialogDescription>
</DialogHeader>
<Form {...form}>
<form id="new-alert" onSubmit={form.handleSubmit(onSave)} noValidate>
<FormSection>
<FormField
control={form.control}
name="name"
rules={{ required: 'نام هشدار را وارد کنید' }}
render={({ field, fieldState }) => (
<FormRow label="نام هشدار" required error={fieldState.error?.message}>
<Input {...field} />
</FormRow>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormRow label="ارسال ایمیل" description="خلاصهٔ هشدارها به ایمیل شما فرستاده میشود.">
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormRow>
)}
/>
</FormSection>
</form>
</Form>
<DialogFooter>
<Button variant="default" onClick={() => onOpenChange(false)}>
انصراف
</Button>
<Button variant="primary" type="submit" form="new-alert">
ساخت هشدار
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}error={fieldState.error?.message}پیام اعتبارسنجی را زیر کنترل نشان میدهد، آن را باrole="alert"اعلام میکند و به کنترلaria-invalidوaria-describedbyمیدهد.FormItem،FormLabel،FormControl،FormMessage،FormItemLayoutو خانوادهٔFieldرا کنارFormRowبه کار نبرید؛ همین کارها راFormRowانجام میدهد (قانونparto/form-rowافزونهٔ ESLint آنها را علامت میزند).- قاعدهٔ اعتبارسنجی در فرم است (
rulesدرFormField، یا یک schema)؛requiredرویFormRowفقط نشانهٔ*وaria-requiredاست. پس این دو همیشه با هم میآیند: ردیفrequiredیعنیrules={{ required: '…' }}(یا یکvalidateکه مقدار خالی را رد کند) رویFormFieldآن، و برعکس. یکی بدون دیگری در حالت توسعه هشدار میدهد. - هر قاعده پیام خودش را دارد:
required: '…'،pattern: { value, message: '…' }،validate: (value) => … || '…'. داخلFormField، ردیف خطای خود فیلد را حتی بدونerrorنشان میدهد، پس قاعدهای که شکست بخورد هرگز بیصدا جلوی ذخیره را نمیگیرد؛ ولی قاعدهٔ بیپیام فقط «این فیلد را پر کنید» یا «مقدار این فیلد معتبر نیست» را نشان میدهد و در حالت توسعه هشدار میدهد. - در صفحهٔ فرم یا تنظیمات،
FormRowها را مستقیم فرزندFormPageیاSettingsSectionکنید؛ قالب، فرم، کارت و دکمهها را خودش میسازد.
کنترل هر نوع فیلد
کنترل را نوع فیلد تعیین میکند، نه سلیقه؛ و هر کنترل field در react-hook-form را (از render در FormField) به
شکل ستون آخر میگیرد:
| فیلد | کنترل | با react-hook-form |
|---|---|---|
| متن، ایمیل، نشانی وب | Input با kind (email، url، tel، password)؛ kind صفحهکلید، تکمیل خودکار و جهت لاتین را میگذارد | {...field} |
| متن بلند | Textarea | {...field} |
| فهرستی از واژههای آزاد (کلیدواژهها) | TagInput | value={field.value} onChange={field.onChange} |
| یکی از چند گزینه | Select با SelectTrigger و SelectContent؛ RadioGroup فقط برای 2 یا 3 گزینه که هر کدام توضیح خودش را دارد | value={field.value} onValueChange={field.onChange} (روی Select یا RadioGroup) |
| چند گزینه از چند گزینه | MultiSelect — هرگز گروهی از Checkboxها | value={field.value} onValueChange={field.onChange} |
| عدد یا درصد | NumberInputLocale (برای درصد unit="%") — نه Slider و نه Input | value={field.value} onValueChange={field.onChange} onBlur={field.onBlur} ref={field.ref} |
| یک تاریخ | DatePicker mode="single" (مقدار فیلد یک Date است) | value={field.value ? { from: field.value } : undefined} onChange={(range) => field.onChange(range?.from)} |
| یک بازه (از … تا) | DatePicker (مقدار فیلد { from, to } است) | value={field.value} onChange={field.onChange} |
| روشن/خاموش (هر فیلد روشن/خاموش فرم) | Switch | checked={field.value} onCheckedChange={field.onChange} |
| پذیرش یک شرط («قبول شرایط») | Checkbox | checked={field.value} onCheckedChange={field.onChange} |
{...field} فقط برای Input و Textarea است. روی NumberInputLocale، MultiSelect، Select، RadioGroup،
Switch یا Checkbox کامپایل میشود ولی مقدار هرگز به فرم نمیرسد، چون این کنترلها مقدارشان را با onValueChange
یا onCheckedChange خبر میدهند، نه onChange؛ onChange={field.onChange} روی Select، RadioGroup، Switch یا
Checkbox هم همین است. قانون parto/form-row و یک هشدار حالت توسعه هر دو را نشان میدهند. Input با
type="number" برای عدد، رشته به فرم میدهد و آن هم هشدار میدهد.
FormRow به کنترل id، aria-labelledby، aria-invalid، aria-describedby و aria-required میدهد — در
Select به SelectTrigger آن. DatePicker aria-required نمیگیرد، چون ماشهٔ آن دکمه است. Slider،
ToggleGroup یا یک div از چند Checkbox کنترل یک فیلد نیستند و در حالت توسعه هشدار میدهند.
حالتها و انواع
چیدمان از ظرف
FormRow ویژگی چیدمان ندارد. در صفحهٔ فرم برچسب بالای کنترل است، در بخش تنظیمات کنار آن (ستون برچسب یکسوم و
ستون کنترل دوسوم، از عرض md)، و در دیالوگ دوباره بالای کنترل — حتی وقتی دیالوگ از داخل یک بخش تنظیمات باز
شده باشد: هر لایهٔ روی صفحه (دیالوگ، پنل کناری، پاپاور) از چیدمان پیشفرض شروع میکند.
ردیف کلید
Switch روی خط برچسب مینشیند: برچسب و توضیح در ابتدای خط، کلید در انتهای آن. Checkbox پیش از برچسبش میآید،
مثل هر فهرست گزینه. هر دو در همهٔ عرضها، از گوشی تا دسکتاپ، یک خطاند. همین را FormRow تشخیص میدهد؛ ویژگیای
برایش لازم نیست.
گروه فیلدها: FormSection
یک قاعده برای گروهبندی FormPage:
- تا 5 فیلد: ردیفها مستقیم در
FormPage، بدونFormSection. - 6 فیلد یا بیشتر: هر فیلد در یک
FormSectionعنواندار، 2 تا 4 فیلد در هر گروه (پس دستکم دو گروه).
هر فیلدی را که فرم ممکن است نشان دهد بشمارید: فیلدی که فقط با یک شرط دیده میشود (نشانی ایمیل وقتی «ارسال ایمیل» روشن است) هم شمرده میشود و وقتی پنهان است، گروهش میتواند یک ردیف داشته باشد.
گروه داخل همان کارت فرم است (ردیف عنوان، بعد ردیفهای گروه، و یک جداکننده بعد از گروه). در دیالوگ یا پنل کناری
ردیفها در یک FormSection میآیند، با عنوان یا بیعنوان، و گروه بعدی 24 پیکسل پایینتر شروع میشود. گروههای صفحهٔ
تنظیمات SettingsSectionاند، نه FormSection؛ FormSection داخل SettingsSection در حالت توسعه هشدار میدهد
و عنوانش h3 میشود.
هشدارهای حالت توسعه
در حالت توسعه (نه در نسخهٔ تولید)، هر اشتباهی که ردیف یا قالب خودش نمیتواند درست کند یکبار در کنسول گزارش میشود:
Selectی کهSelectTriggerفرزند مستقیمش نیست؛Slider،ToggleGroupیا یکdivبهجای کنترل؛sizeیا کلاس عرض روی کنترل؛{...field}یاonChangeروی کنترلی که مقدارش را باonValueChangeیاonCheckedChangeخبر میدهد (پیام، سیمکشی درست را میگوید)، وInputباtype="number"یاinputMode="decimal"بهجایNumberInputLocale؛- قاعدهای بیپیام که شکست خورده است (
required: true، یاvalidateکه فقطfalseبرمیگرداند)؛ - ردیف
requiredکهFormFieldآن نهrules.requiredدارد نهvalidate، یا فیلدی باrules.requiredکه ردیفشrequiredنیست؛ CardیاCardHeaderداخلFormPageیاSettingsSection، وFormRowداخل یکCardContent؛- گروهبندی
FormPageبیرون از قاعدهٔ بالا (6 فیلد یا بیشتر بیگروه، تنها یک گروه، فیلد بیرون از گروهها، گروه بیعنوان، گروه بیش از 4 فیلد)؛ FormSectionداخلSettingsSection، یا در صفحه ولی بیرون ازFormPage؛FormSectionباdescriptionو بدونtitle(توضیح زیر عنوان میآید، پس بدون عنوان نشان داده نمیشود).
راهنمای استفاده
بکنید
- برای هر فیلد یک
FormRowباlabelبنویسید؛ فیلد اجباری راrequiredکنید. - پیام خطا را از
fieldState.error?.messageبهerrorبدهید. - کنترل را از جدول «کنترل هر نوع فیلد» بردارید، بدون
sizeو بدون کلاس عرض؛ ردیف اندازه و عرض را تعیین میکند. آن را همانطور که ستون آخر جدول میگوید بهfieldوصل کنید. - در دیالوگ یا پنل کناری، ردیفها را در یک
FormSectionبگذارید و دکمهها را درDialogFooter.
نکنید
- برچسب را با
Labelوdivکنار کنترل نسازید، وgridیاspace-y-*برای فاصلهٔ فیلدها ننویسید. - یک
FormRowرا دور چند کنترل نپیچید؛ هر ردیف یک کنترل است. چند گزینه از چند گزینهMultiSelectاست. {...field}را جز رویInputوTextareaپخش نکنید، وonChangeرا بهSelect،RadioGroup،SwitchیاCheckboxندهید؛ کامپایل میشوند ولی مقدارشان به فرم نمیرسد.- قاعدهٔ بیپیام ننویسید (
required: true،validate: (v) => v.length > 0)؛ پیام را در خود قاعده بگذارید. CardداخلFormPageیاSettingsSectionنگذارید؛ فیلدهای صفحهٔ فرم را باFormSectionگروه کنید.- برای چیدمان افقی سراغ
FormItemLayout layout="horizontal"نروید؛ در صفحهٔ تنظیماتSettingsSectionآن را انجام میدهد.
Props
FormRow
FormSection
FormRow و FormSection className و id نمیپذیرند: چیدمان مال ظرف است و id مال خود کنترل (اگر داده
نشود، ساخته میشود).
دسترسیپذیری
- برچسب نام کنترل است: با
htmlForوaria-labelledbyبه کنترل وصل است، پسSelect(رویSelectTrigger)،MultiSelectوRadioGroupهم با برچسب ردیف خوانده میشوند، نه با متن جاینگهدار. - کلیک روی برچسب کنترل را فوکوس میکند؛
Selectبا آن باز نمیشود (مثلselectبومی) وRadioGroupفوکوس را به گزینهٔ انتخابشده میدهد. - توضیح و پیام خطا با
aria-describedbyبه کنترل وصلاند؛ خطاrole="alert"دارد و کنترلaria-invalidمیگیرد. - نشانهٔ
*از فناوری کمکی پنهان است و اجباری بودن باaria-requiredاعلام میشود. FormSectionبا عنوان یک گروه نامدار است (role="group"باaria-labelledby)، نه یک ناحیهٔ صفحه؛ عنوانش در صفحهٔ فرمh2و در دیالوگh3است.- ورودی پنهان فرمِ
SwitchوCheckboxدر صفحهٔ راستبهچپ عرضی نمیگیرد، پس کلیدِ انتهای خط صفحه را افقی پیمایشپذیر نمیکند.
نامهای ادغامشده
FormItemLayout داربست داخلی FormRow است (مرجع) و چیدمانهایش همان گزینههای layout در FormRow است. خانوادهٔ Field و FormHeader منسوخاند؛ به جایشان FormRow و عنوانِ FormSection یا SettingsSection را به کار ببرید.
کامپوننتهای مرتبط
FormPage— صفحهٔ یک فرم؛ ردیفها و گروههایش در کارت آن.FormItemLayout— جزء سطح پایینی کهFormRowروی آن ساخته شده؛ فقط وقتی چیدمان ردیف را خودتان باید انتخاب کنید.Form— اتصال react-hook-form؛FormFieldاز آن باFormRowبه کار میرود.ListPage— کنترلهای جستوجو و فیلتر صفحهٔ فهرست (searchوfiltersقالب)، نه فیلد فرم.