ردیف فرم (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}
فهرستی از واژه‌های آزاد (کلیدواژه‌ها)TagInputvalue={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 و نه Inputvalue={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}
روشن/خاموش (هر فیلد روشن/خاموش فرم)Switchchecked={field.value} onCheckedChange={field.onChange}
پذیرش یک شرط («قبول شرایط»)Checkboxchecked={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

Prop

Type

FormSection

Prop

Type

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 قالب)، نه فیلد فرم.