قسمت چهاردهم - فرمها در 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 inputcontrolled 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وuncontrolledinputها - اهمیت
pending state، پیام خطا، پیام موفقیت وUXمناسب - کاربرد
<Form>ازnext/formبرای فرمهایsearchوnavigationمحور
در این مرحله هنوز فرمها را به بکاند یا دیتابیس وصل نکردهایم.
در ادامه، بهصورت مرحلهبهمرحله سراغ اعتبارسنجی، پردازش داده و اتصال فرمها به لایه سرور میرویم.