قالب صفحهٔ فرم (FormPage)

صفحه‌ای که یک فرم است — ساخت یک هشدار، ویرایش یک منبع — با ردیف‌های فرم در یک کارت و دکمه‌های انصراف و ثبت در انتهای خط

معرفی

FormPage قالب صفحه‌ای است که یک فرم است و یک بار ثبت می‌شود. قالب فرم را می‌سازد و هر تصمیم آن را می‌گیرد:

  • عرض باریک 768 پیکسل؛
  • ردیف‌ها (FormRow) در یک کارت، برچسب بالای هر کنترل، با جداکننده بین ردیف‌ها (تا 5 فیلد بی‌گروه، 6 فیلد یا بیشتر در گروه‌های عنوان‌دار FormSection)؛
  • پایین کارت: «انصراف» و دکمهٔ ثبت در انتهای خط، دکمهٔ ثبت آخر (تنها اقدام اصلی صفحه)؛ دکمه‌ها به‌اندازهٔ برچسبشان‌اند و فقط روی موبایل تمام‌عرض می‌شوند (دکمهٔ ثبت بالا)؛
  • خطای ثبت به‌صورت خلاصه بالای فرم، که اعلام و فوکوس می‌شود؛
  • حالت بارگذاری دادهٔ در حال ویرایش به‌جای کارت.

چه زمانی استفاده کنیم:

  • ساختن یا ویرایش یک چیز با یک دکمهٔ ثبت: «هشدار تازه»، «ویرایش منبع»، «دعوت عضو».
  • صفحه‌ای به نام «تنظیمات …» که در واقع یک فرم با یک «ذخیره» و یک «انصراف» است.

چه زمانی استفاده نکنیم:

  • تنظیمات در چند گروه مستقل که هر گروه جدا ذخیره می‌شود: SettingsPage.
  • فرم دو سه فیلدی یا تأیید یک کار: Dialog؛ فرم بلند بدون ترک صفحهٔ جاری: Sheet. (انتخاب قالب صفحه)

استفاده

FormRowها را مستقیم فرزند FormPage کنید (فرم 6 فیلدی یا بلندتر: در FormSectionها، پایین‌تر). با react-hook-form، Form (از @partodata/ui/form) فرم را فراهم می‌کند و handleSubmit به onSubmit می‌رود:

'use client'
import { useForm } from 'react-hook-form'
import { useRouter } from 'next/navigation'
import { Input, Switch } from '@partodata/ui'
import { Form, FormField } from '@partodata/ui/form'
import { FormPage, FormRow } from '@partodata/ui/templates'

type Values = { name: string; email: boolean }

export default function NewAlertPage() {
  const router = useRouter()
  const form = useForm<Values>({ defaultValues: { name: '', email: true } })
  const save = form.handleSubmit(async () => {
    router.push('/mentions')
  })
  return (
    <Form {...form}>
      <FormPage
        title="هشدار تازه"
        back={{ href: '/mentions', label: 'منشن‌ها' }}
        onSubmit={save}
        onCancel={() => router.push('/mentions')}
        submitLabel="ساخت هشدار"
        submitting={form.formState.isSubmitting}
      >
        <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="ارسال ایمیل">
              <Switch checked={field.value} onCheckedChange={field.onChange} />
            </FormRow>
          )}
        />
      </FormPage>
    </Form>
  )
}

دکمهٔ ثبت را قالب می‌سازد (submitLabel): دکمهٔ type="submit" دیگری داخل فرم ننویسید (در محیط توسعه هشدار می‌دهد).

حالت‌ها و انواع

خطای ثبت

error پیام خطای آخرین ثبت است (پیام سرور به زبان کاربر). قالب آن را با عنوان «ذخیره انجام نشد» بالای کارت نشان می‌دهد، با role="alert" اعلام می‌کند و فوکوس را به آن می‌برد. خطای هر فیلد در ردیف خودش است (FormRow error).

هر ذخیرهٔ ناموفق — حتی با همان پیام قبلی — خلاصه را دوباره نشان می‌دهد، اعلام و فوکوس می‌کند. ثبتی که اعتبارسنجی فیلدها جلویش را می‌گیرد ذخیرهٔ ناموفق نیست: ذخیره اجرا نشده، پس خلاصه دوباره اعلام نمی‌شود و فوکوس روی اولین فیلد نامعتبر می‌ماند تا کاربر ببیند چه چیزی را باید درست کند.

با react-hook-form، خطای ثبت خطای ریشهٔ فرم است: در catch ذخیره form.setError('root', { message: 'ذخیره انجام نشد؛ لحظه‌ای بعد دوباره تلاش کنید.' }) و روی قالب error={form.formState.errors.root?.message}. ثبت بعدی خودش آن را پاک می‌کند و useState جداگانه‌ای لازم نیست.

در حال ثبت

submitting دکمهٔ ثبت را به حالت بارگذاری می‌برد و هر دو دکمه را غیرفعال می‌کند (با react-hook-form: form.formState.isSubmitting). اگر دکمهٔ ثبت با صفحه‌کلید زده شده باشد، فوکوس در این مدت روی خود فرم می‌ماند (نه <body>) و وقتی دکمه دوباره فعال شد به آن برمی‌گردد.

بازگشت و «انصراف»

«انصراف» (onCancel) در هر صفحهٔ فرم هست و الزامی است. کارش به جای صفحه بستگی دارد:

صفحهٔ فرمbackonCancel
مورد منوی خودش را دارد («تنظیمات هشدار»)نداردتغییرات را کنار می‌گذارد: () => form.reset()
از صفحهٔ دیگری باز شده (ساخت یا ویرایش، مثلاً از فهرست هشدارها)پیوند به همان صفحهبه همان صفحه برمی‌گردد: () => router.push(back.href)

back و breadcrumbs با هم پذیرفته نمی‌شوند (خطای نوع)؛ breadcrumbs فقط برای صفحه‌ای دو سطح یا بیشتر عمیق.

اقدام‌های ثانوی فرم

اقدامی که مال خود فرم است ولی آن را ثبت نمی‌کند — «تست اتصال» (کلیدی را پیش از ذخیره بیازمایید)، «ارسال ایمیل آزمایشی»، «بازگردانی پیش‌فرض‌ها» — در secondaryActions است: در ابتدای خط پاورقی کارت (ثبت و «انصراف» در انتهای آن)، هر کدام Button variant="default" با onClick خودش که فرم را ثبت نمی‌کند (نوع پیش‌فرض Button همان button است)، پس دکمهٔ ثبت تنها اقدام اصلی می‌ماند. نتیجه‌اش: toast («اتصال برقرار است») یا setError روی همان فیلد. روی گوشی زیر دکمه‌ها و تمام‌عرض.

فرم ویرایش

در ویرایش، حالت را از بارگذاری دادهٔ فعلی بسازید: state={pageState({ data: settings, isLoading, error, onRetry })}. تا داده برسد اسکلت ردیف‌ها به‌جای کارت می‌نشیند و سرِ صفحه می‌ماند؛ اگر بارگذاری شکست خورد، ErrorState با «تلاش مجدد». فرم حالت خالی ندارد (موجودیتی که وجود ندارد UtilityPage نوع 404 است) و فرم ساختن چیز تازه state ندارد.

گروه‌بندی: یک قاعده

  • تا 5 فیلد: ردیف‌ها مستقیم در FormPage، بدون FormSection.
  • 6 فیلد یا بیشتر: هر فیلد در یک FormSection عنوان‌دار، 2 تا 4 فیلد در هر گروه (پس دست‌کم دو گروه).

هر فیلدی را که فرم ممکن است نشان دهد بشمارید: فیلدی که فقط با یک شرط دیده می‌شود (نشانی ایمیل وقتی «ارسال ایمیل» روشن است) هم شمرده می‌شود و وقتی پنهان است، گروهش می‌تواند یک ردیف داشته باشد.

گروه داخل همان کارت است و فرم همچنان یک دکمهٔ ثبت دارد. کارت را قالب می‌سازد: Card یا CardHeader خودتان را داخل FormPage نگذارید. نمونهٔ بالا (همان فرم §5 راهنمای مصرف و «تنظیمات هشدار» اپ شروع) هفت فیلد دارد، پس سه گروه دارد. در حالت توسعه، Card داخل کارت فرم، FormRow داخل یک CardContent، و هر گروه‌بندی بیرون از این قاعده (بی‌گروه با 6 فیلد یا بیشتر، تنها یک گروه، فیلد بیرون از گروه‌ها، گروه بی‌عنوان، گروه بیش از 4 فیلد) یک‌بار در کنسول هشدار می‌دهد.

راهنمای استفاده

بکنید

  • برای هر صفحه‌ای که یک فرم با یک ثبت است FormPage را برگردانید.
  • برچسب دکمهٔ ثبت را فعل همان کار کنید («ساخت هشدار»، «ذخیره»).
  • onCancel را طبق جدول «بازگشت و انصراف» بدهید: در فرم منو form.reset()، در فرمی با back بازگشت به همان صفحه.

نکنید

  • فرم یا گروهی از فیلدها را در Card خودتان نپیچید (گروه FormSection است)، و دکمه‌ها را خودتان نسازید یا تمام‌عرض نکنید.
  • عرض فرم را با max-w-* تغییر ندهید؛ فرم صفحه همیشه 768 است.
  • برای تنظیماتی که گروه‌گروه ذخیره می‌شوند FormPage را به کار نبرید؛ آن SettingsPage است.

Props

FormPage

Prop

Type

دسترسی‌پذیری

  • فرم یک ناحیهٔ نام‌دار است (form با نام عنوان صفحه) و noValidate دارد تا پیام‌های فرم جای پیام‌های مرورگر را بگیرند. پس kind="email" چیزی را بررسی نمی‌کند (فقط صفحه‌کلید را انتخاب می‌کند): قالب ایمیل یا نشانی وب را در rules همان فیلد بررسی کنید (pattern، مثل «نشانی ایمیل» نمونهٔ بالا).
  • ترتیب Tab: فیلدها به ترتیب، بعد «انصراف»، بعد دکمهٔ ثبت؛ Enter در یک فیلد فرم را ثبت می‌کند.
  • خلاصهٔ خطای ثبت role="alert" دارد و فوکوس می‌گیرد، پس هم خوانده می‌شود و هم در دید است.

کامپوننت‌های مرتبط

  • FormRow — هر فیلد فرم؛ چیدمانش را از FormPage می‌گیرد.
  • SettingsPage — تنظیمات گروه‌گروه، هر گروه با ذخیرهٔ خودش.
  • Form — اتصال react-hook-form.
  • Dialog — فرم کوتاه دو سه فیلدی، بدون صفحهٔ جدا.