MATIN MOLKARA
SERIES: آموزش Next js — PART 14 OF 28

قسمت چهاردهم - فرم‌ها در Next.js

فرم‌ها می‌توانند برای ارسال پیام تماس، ساخت پروژه جدید، نوشتن مقاله، ویرایش محتوا یا حتی فیلتر کردن داده‌ها استفاده شوند.

فرم‌ها در Next.js

فرم‌ها می‌توانند برای ارسال پیام تماس، ساخت پروژه جدید، نوشتن مقاله، ویرایش محتوا یا حتی فیلتر کردن داده‌ها استفاده شوند.

تا اینجا در پروژه با بخش‌هایی مثل دریافت داده، نمایش لیست پروژه‌ها، صفحه جزئیات و مدیریت حالت‌های مختلف route کار کردیم. اما یک پروژه واقعی فقط داده را نمایش نمی‌دهد؛ در بسیاری از بخش‌ها باید از کاربر داده بگیرد.

فرم‌ها دقیقاً همین نقش را دارند.

در یک پروژه پرتفولیو یا CMS، فرم‌ها می‌توانند برای ارسال پیام تماس، ساخت پروژه جدید، نوشتن مقاله، ویرایش محتوا یا حتی فیلتر کردن داده‌ها استفاده شوند.

در این پست تمرکز اصلی روی ساختار فرم‌ها، مدیریت inputها و تجربه کاربری فرم است.


فرم‌ها در این پروژه کجا استفاده می‌شوند؟

در این پروژه چند نوع فرم مهم داریم.

اولین فرم، فرم تماس است. این فرم معمولاً در صفحه اصلی یا صفحه contact قرار می‌گیرد و کاربر می‌تواند نام، ایمیل و پیام خود را وارد کند.

نوع دوم، فرم ایجاد پروژه است. این فرم در dashboard استفاده می‌شود و برای اضافه کردن پروژه جدید به پرتفولیو کاربرد دارد. این فرم معمولاً فیلدهای بیشتری دارد، مثل عنوان، slug، توضیحات، محتوای کامل، تکنولوژی‌ها، لینک دمو، لینک گیت‌هاب و تصویر پروژه.

نوع سوم، فرم ایجاد مقاله است. این فرم برای بخش بلاگ یا CMS استفاده می‌شود و فیلدهایی مثل عنوان، slug، خلاصه، محتوا، وضعیت انتشار و تصویر شاخص دارد.

پس فرم‌ها فقط یک بخش کوچک از UI نیستند. در عمل، فرم‌ها نقطه ورود داده به سیستم هستند.

به همین دلیل باید از همان ابتدا با ساختار تمیز، state مناسب و UX قابل قبول ساخته شوند.


ساخت فرم تماس

فرم تماس معمولاً ساده‌ترین فرم پروژه است. هدف آن گرفتن چند مقدار از کاربر است:

  • نام
  • ایمیل
  • پیام

برای شروع می‌توانیم یک فرم ساده بسازیم:

export function ContactForm() {
  return (
    <form className="space-y-4">
      <div className="space-y-2">
        <label htmlFor="name" className="text-sm text-white">
          نام
        </label>
        <input
          id="name"
          name="name"
          type="text"
          className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
          placeholder="نام شما"
        />
      </div>

      <div className="space-y-2">
        <label htmlFor="email" className="text-sm text-white">
          ایمیل
        </label>
        <input
          id="email"
          name="email"
          type="email"
          className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
          placeholder="you@example.com"
        />
      </div>

      <div className="space-y-2">
        <label htmlFor="message" className="text-sm text-white">
          پیام
        </label>
        <textarea
          id="message"
          name="message"
          rows={5}
          className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
          placeholder="پیام خود را بنویسید..."
        />
      </div>

      <button
        type="submit"
        className="rounded-lg bg-white px-4 py-2 text-sm font-medium text-black"
      >
        ارسال پیام
      </button>
    </form>
  )
}

در این مرحله هنوز فرم را به بک‌اند وصل نکرده‌ایم. اما همین ساختار چند نکته مهم دارد:

  • هر input یک name مشخص دارد
  • هر input با label مرتبط شده است
  • فرم از نظر ظاهری با theme پروژه هماهنگ است
  • دکمه submit از نوع submit است
  • ساختار فرم ساده و قابل توسعه باقی مانده است

وجود name برای inputها مهم است، چون وقتی بعداً بخواهیم داده فرم را پردازش کنیم، داده‌ها از همین nameها خوانده می‌شوند.


ساخت فرم ایجاد پروژه

فرم ایجاد پروژه از فرم تماس پیچیده‌تر است، چون داده‌ای که وارد می‌شود مستقیماً روی محتوای سایت اثر می‌گذارد.

در dashboard، این فرم می‌تواند چنین فیلدهایی داشته باشد:

  • عنوان پروژه
  • slug
  • توضیح کوتاه
  • محتوای کامل
  • تکنولوژی‌ها
  • لینک دمو
  • لینک گیت‌هاب
  • تصویر یا تصاویر پروژه
  • وضعیت انتشار

نمونه ساده فرم:

export function CreateProjectForm() {
  return (
    <form className="space-y-5">
      <div className="grid gap-4 md:grid-cols-2">
        <div className="space-y-2">
          <label htmlFor="title" className="text-sm text-white">
            عنوان پروژه
          </label>
          <input
            id="title"
            name="title"
            type="text"
            className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
            placeholder="Portfolio Dashboard"
          />
        </div>

        <div className="space-y-2">
          <label htmlFor="slug" className="text-sm text-white">
            slug
          </label>
          <input
            id="slug"
            name="slug"
            type="text"
            className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
            placeholder="portfolio-dashboard"
          />
        </div>
      </div>

      <div className="space-y-2">
        <label htmlFor="description" className="text-sm text-white">
          توضیح کوتاه
        </label>
        <textarea
          id="description"
          name="description"
          rows={4}
          className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
          placeholder="توضیح کوتاهی درباره پروژه..."
        />
      </div>

      <div className="space-y-2">
        <label htmlFor="technologies" className="text-sm text-white">
          تکنولوژی‌ها
        </label>
        <input
          id="technologies"
          name="technologies"
          type="text"
          className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
          placeholder="Next.js, Prisma, PostgreSQL, Tailwind CSS"
        />
      </div>

      <div className="grid gap-4 md:grid-cols-2">
        <div className="space-y-2">
          <label htmlFor="demoUrl" className="text-sm text-white">
            لینک دمو
          </label>
          <input
            id="demoUrl"
            name="demoUrl"
            type="url"
            className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
            placeholder="https://example.com"
          />
        </div>

        <div className="space-y-2">
          <label htmlFor="githubUrl" className="text-sm text-white">
            لینک گیت‌هاب
          </label>
          <input
            id="githubUrl"
            name="githubUrl"
            type="url"
            className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white outline-none focus:border-white/30"
            placeholder="https://github.com/username/project"
          />
        </div>
      </div>

      <div className="space-y-2">
        <label htmlFor="images" className="text-sm text-white">
          تصاویر پروژه
        </label>
        <input
          id="images"
          name="images"
          type="file"
          multiple
          className="w-full rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-sm text-white file:mr-4 file:rounded-md file:border-0 file:bg-white file:px-3 file:py-2 file:text-sm file:text-black"
        />
      </div>

      <button
        type="submit"
        className="rounded-lg bg-white px-4 py-2 text-sm font-medium text-black"
      >
        ایجاد پروژه
      </button>
    </form>
  )
}

نکته مهم این است که فرم ایجاد پروژه معمولاً فقط یک فرم ساده نیست. این فرم در ادامه به چند بخش دیگر وصل می‌شود:

  • اعتبارسنجی داده‌ها
  • پردازش داده در بک‌اند
  • ذخیره داده در دیتابیس
  • آپلود تصویر در سرویس‌هایی مثل Cloudinary
  • redirect یا به‌روزرسانی UI بعد از ثبت موفق

پس بهتر است از همین ابتدا نام فیلدها، ساختار فرم و UX آن را تمیز نگه داریم.


فرم‌های جستجو و کامپوننت Form در Next.js

در داک رسمی Next.js یک کامپوننت به نام <Form> از مسیر next/form معرفی شده است.

این کامپوننت نسخه توسعه‌یافته فرم HTML است و برای فرم‌هایی که با navigation و search params کار می‌کنند، مفید است.

مثلاً برای فرم جستجو:

import Form from 'next/form'

export function SearchForm() {
  return (
    <Form action="/projects">
      <input
        name="query"
        placeholder="جستجوی پروژه..."
        className="rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white"
      />
      <button type="submit">جستجو</button>
    </Form>
  )
}

وقتی کاربر مقدار جستجو را وارد کند، فرم به این مسیر می‌رود:

/projects?query=dashboard

مزیت <Form> در این حالت این است که Next.js می‌تواند navigation را به‌صورت client-side انجام دهد و حتی loading UI مسیر مقصد را prefetch کند.

این مدل بیشتر برای فرم‌هایی مناسب است که قرار نیست داده جدیدی در دیتابیس بسازند، بلکه فقط وضعیت صفحه یا URL را تغییر می‌دهند؛ مثل:

  • جستجو
  • فیلتر
  • مرتب‌سازی
  • pagination

state و کنترل inputها

در React معمولاً دو مدل برای کار با inputها داریم:

  • uncontrolled input
  • controlled input

در حالت uncontrolled، مقدار input را با state کنترل نمی‌کنیم. خود مرورگر مقدار را نگه می‌دارد و هنگام submit می‌توانیم داده را از فرم بخوانیم.

مثلاً:

<form>
  <input name="title" />
  <button type="submit">Submit</button>
</form>

این روش برای خیلی از فرم‌ها ساده و مناسب است.

اما در حالت controlled، مقدار input را داخل state نگه می‌داریم:

'use client'

import { useState } from 'react'

export function ProjectTitleInput() {
  const [title, setTitle] = useState('')

  return (
    <input
      value={title}
      onChange={(event) => setTitle(event.target.value)}
      className="rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white"
      placeholder="عنوان پروژه"
    />
  )
}

این روش زمانی مفید است که بخواهیم هم‌زمان با تایپ کاربر، UI را تغییر دهیم. مثلاً:

  • نمایش preview
  • ساخت خودکار slug
  • فعال یا غیرفعال کردن بخش‌هایی از فرم
  • نمایش validation لحظه‌ای
  • مدیریت inputهای پیچیده

برای مثال، می‌توانیم از عنوان پروژه، slug پیشنهادی بسازیم:

'use client'

import { useState } from 'react'

export function ProjectSlugFields() {
  const [title, setTitle] = useState('')
  const slug = title
    .toLowerCase()
    .trim()
    .replaceAll(' ', '-')

  return (
    <div className="grid gap-4 md:grid-cols-2">
      <input
        value={title}
        onChange={(event) => setTitle(event.target.value)}
        className="rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white"
        placeholder="عنوان پروژه"
      />

      <input
        value={slug}
        readOnly
        className="rounded-lg border border-white/10 bg-zinc-950 px-4 py-2 text-white/70"
        placeholder="slug"
      />
    </div>
  )
}

البته لازم نیست همه inputها controlled باشند. در فرم‌های بزرگ، controlled کردن همه فیلدها می‌تواند کد را شلوغ‌تر کند.

یک رویکرد بهتر این است:

  • inputهای ساده را uncontrolled نگه داریم
  • inputهایی که به preview یا منطق لحظه‌ای نیاز دارند controlled کنیم
  • پردازش نهایی فرم را در لایه مناسب انجام دهیم

UX مناسب فرم

فرم خوب فقط مجموعه‌ای از inputها نیست. فرم باید به کاربر نشان دهد چه چیزی لازم است، چه چیزی اشتباه است و بعد از submit چه اتفاقی افتاده.

چند نکته مهم برای UX فرم‌ها:

label واضح داشته باشیم

هر input باید label مشخص داشته باشد. placeholder جایگزین label نیست، چون وقتی کاربر شروع به تایپ می‌کند، placeholder ناپدید می‌شود.

<label htmlFor="email">ایمیل</label>
<input id="email" name="email" type="email" />

این کار هم برای accessibility بهتر است، هم خوانایی فرم را بیشتر می‌کند.

دکمه submit باید وضعیت داشته باشد

وقتی فرم در حال ارسال است، بهتر است دکمه submit غیرفعال شود یا متن آن تغییر کند.

مثلاً:

<button
  type="submit"
  disabled
  className="rounded-lg bg-white px-4 py-2 text-sm font-medium text-black disabled:cursor-not-allowed disabled:opacity-60"
>
  در حال ارسال...
</button>

ایده اصلی این است که کاربر بفهمد عملیات در حال انجام است و دوباره روی دکمه کلیک نکند.

پیام خطا و موفقیت نمایش بدهیم

اگر کاربر فیلدی را اشتباه پر کند، فقط submit را متوقف نکنیم. باید پیام واضح نشان بدهیم.

مثلاً:

<p className="text-sm text-red-400">
  عنوان پروژه الزامی است.
</p>

همین‌طور بعد از موفقیت‌آمیز بودن عملیات، بهتر است پیام یا redirect مناسب داشته باشیم.

برای فرم تماس، شاید نمایش پیام موفقیت کافی باشد.
اما برای فرم ایجاد پروژه، معمولاً بهتر است بعد از ساخت پروژه، کاربر به صفحه جزئیات همان پروژه یا لیست پروژه‌ها منتقل شود.

فرم را بیش از حد شلوغ نکنیم

فرم ایجاد پروژه یا مقاله می‌تواند فیلدهای زیادی داشته باشد. اگر همه فیلدها بدون فاصله و دسته‌بندی نمایش داده شوند، کاربر سریع خسته می‌شود.

بهتر است فرم را به بخش‌های منطقی تقسیم کنیم:

  • اطلاعات اصلی
  • محتوا
  • لینک‌ها
  • تصاویر
  • وضعیت انتشار

این کار باعث می‌شود فرم حرفه‌ای‌تر و قابل استفاده‌تر باشد.

از validation فقط در یک نقطه استفاده نکنیم

اعتبارسنجی بهتر است چند لایه داشته باشد.

در سمت client می‌توان بعضی خطاهای ساده را سریع‌تر نمایش داد.
اما اعتبارسنجی اصلی باید در بک‌اند انجام شود، چون کاربر می‌تواند client-side validation را دور بزند.

در پست‌های بعدی این بخش را با جزئیات بیشتری بررسی می‌کنیم.


جمع‌بندی

فرم‌ها یکی از مهم‌ترین بخش‌های هر پروژه واقعی هستند، چون مسیر ورود داده به سیستم را می‌سازند.

در این پست فرم‌ها را از زاویه UI و UX بررسی کردیم:

  • فرم تماس برای دریافت پیام از کاربر
  • فرم ایجاد پروژه برای مدیریت محتوای پرتفولیو
  • نقش name، label و ساختار درست inputها
  • تفاوت controlled و uncontrolled inputها
  • اهمیت pending state، پیام خطا، پیام موفقیت و UX مناسب
  • کاربرد <Form> از next/form برای فرم‌های search و navigation محور

در این مرحله هنوز فرم‌ها را به بک‌اند یا دیتابیس وصل نکرده‌ایم.
در ادامه، به‌صورت مرحله‌به‌مرحله سراغ اعتبارسنجی، پردازش داده و اتصال فرم‌ها به لایه سرور می‌رویم.

// 0 comments
#Next.js#اموزش#forms

نظرات (0)

نظر خود را بنویسید