Form
react-hook-form bilan formalar: FormField label, xato va qiymatni har qanday kutubxona komponentiga avtomatik ulaydi.
'use client';
import { zUz } from '@akhatmirzo/core/zod';
import {
Checkbox,
DatePicker,
Input,
PhoneInput,
PinflInput,
Radio,
RadioGroup,
RegionSelect,
} from '@akhatmirzo/ui';
import { Form, FormField, FormRootError, FormSubmit } from '@akhatmirzo/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const schema = z.object({
fullName: z.string().trim().min(3, "To'liq ismingizni kiriting"),
phone: zUz.phone(),
pinfl: zUz.pinfl(),
birthDate: z.date({ error: "Tug'ilgan sanani kiriting" }),
gender: z.enum(['male', 'female'], { error: 'Jinsni tanlang' }),
region: zUz.region(),
password: zUz.password({ minScore: 2 }),
terms: z.literal(true, { error: 'Shartlarga rozilik bering' }),
});
type Registration = z.output<typeof schema>;
export default function FormRegistration() {
const [saved, setSaved] = useState<Registration | null>(null);
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { fullName: '', phone: '', pinfl: '', region: '', password: '' },
});
const save = async (data: Registration) => {
await new Promise((r) => setTimeout(r, 600));
if (data.pinfl.startsWith('6')) {
form.setError('root', {
message: "Bu JShShIR bilan foydalanuvchi allaqachon ro'yxatdan o'tgan",
});
return;
}
setSaved(data);
};
return (
<div className="flex w-full max-w-md flex-col gap-4">
<Form form={form} onSubmit={save} className="flex flex-col gap-4">
<FormField name="fullName" label="To'liq ism" required>
<Input placeholder="Sarvar Abduxoliqov" autoComplete="name" />
</FormField>
<FormField name="phone" label="Telefon" description="SMS kod yuboriladi" required>
<PhoneInput />
</FormField>
<FormField name="pinfl" label="JShShIR" description="Pasportdagi 14 xonali raqam" required>
<PinflInput />
</FormField>
<FormField name="birthDate" label="Tug'ilgan sana" required>
<DatePicker />
</FormField>
<FormField name="gender" label="Jinsi" required>
<RadioGroup orientation="horizontal">
<Radio value="male" label="Erkak" />
<Radio value="female" label="Ayol" />
</RadioGroup>
</FormField>
<FormField name="region" label="Hudud" required>
<RegionSelect />
</FormField>
<FormField name="password" label="Parol" required>
<Input type="password" autoComplete="new-password" />
</FormField>
<FormField name="terms">
<Checkbox label="Foydalanish shartlariga roziman" />
</FormField>
<FormRootError />
<FormSubmit fullWidth>Ro'yxatdan o'tish</FormSubmit>
</Form>
{saved && (
<pre className="overflow-x-auto rounded-lg bg-surface-muted p-3 text-xs text-text-muted">
{JSON.stringify(saved, null, 2)}
</pre>
)}
</div>
);
}
O'rnatish
@akhatmirzo/ui/form — alohida kirish nuqtasi. Asosiy @akhatmirzo/ui importi
react-hook-form ni tortmaydi, shuning uchun formasiz loyihalarga ortiqcha kod tushmaydi.
react-hook-form — ixtiyoriy peer-bog'liqlik, uni o'zingiz o'rnatasiz (zod va resolver —
validatsiya uchun, tavsiya etiladi):
pnpm add react-hook-form @hookform/resolvers zodImport
import { Form, FormField, FormSubmit, FormRootError, useFormField } from '@akhatmirzo/ui/form';Qachon ishlatish
- Validatsiyali har qanday forma — ro'yxatdan o'tish, profil, buyurtma. Label, izoh, xato
matni,
aria-*va fokus qo'lda yozilmaydi. - O'zbekcha maydonlar uchun
zUzsxemalari bilan birga ishlating: telefon, JShShIR, pasport, summa — tayyor tekshiruv va o'zbekcha xato matnlari. - Bitta-ikkita maydonli oddiy forma uchun
react-hook-formshart emas —FieldvauseStateyetarli. - Har bir
FormFieldichida bitta boshqaruv elementi bo'lsin. Bir nechta element kerak bo'lsa —renderyoki alohidaFormFieldlar.
To'liq misol
zodResolver sxemani tekshiradi, zUz transformlari esa onSubmit ga tozalangan qiymat
beradi: telefon "998901234567", summa "1 250 000" emas 1250000. Formadagi qiymat
(foydalanuvchi ko'radigani) o'zgarmaydi.
'use client';
import { zUz } from '@akhatmirzo/core/zod';
import { Checkbox, Input, PhoneInput, PinflInput, RegionSelect } from '@akhatmirzo/ui';
import { Form, FormField, FormRootError, FormSubmit } from '@akhatmirzo/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const schema = z.object({
fullName: z.string().trim().min(3, "To'liq ismingizni kiriting"),
phone: zUz.phone(),
pinfl: zUz.pinfl(),
region: zUz.region(),
terms: z.literal(true, { error: 'Shartlarga rozilik bering' }),
});
export function RegisterForm() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { fullName: '', phone: '', pinfl: '', region: '' },
});
async function save(data: z.output<typeof schema>) {
// data.phone === '998901234567'
const res = await fetch('/api/register', { method: 'POST', body: JSON.stringify(data) });
if (!res.ok) form.setError('root', { message: "Server javob bermadi, qayta urinib ko'ring" });
}
return (
<Form form={form} onSubmit={save} className="flex flex-col gap-4">
<FormField name="fullName" label="To'liq ism" required>
<Input />
</FormField>
<FormField name="phone" label="Telefon" description="SMS kod yuboriladi">
<PhoneInput />
</FormField>
<FormField name="pinfl" label="JShShIR">
<PinflInput />
</FormField>
<FormField name="region" label="Hudud">
<RegionSelect />
</FormField>
<FormField name="terms">
<Checkbox label="Foydalanish shartlariga roziman" />
</FormField>
<FormRootError />
<FormSubmit>Ro'yxatdan o'tish</FormSubmit>
</Form>
);
}Form — <form noValidate> + FormProvider; onSubmit faqat validatsiyadan o'tganda
chaqiriladi. FormSubmit yuborish davomida loading holatiga o'tadi, FormRootError esa
setError('root', ...) (yoki 'root.server') xatosini Alert bilan ko'rsatadi.
Moslik jadvali
FormField bolaning turini aniqlaydi va unga mos prop'larni uzatadi. Kutubxona komponentlari
uchun tayyor jadval bor; label, description, id, aria-describedby, aria-invalid,
required va disabled esa Field konteksti orqali hammasiga avtomatik yetadi.
| Komponent | Uzatiladigan prop'lar | Forma qiymati |
|---|---|---|
Input, Textarea | value, onChange(event), name | string |
native <input>, <textarea>, <select> | value, onChange(event), name, id, aria-*, disabled | string |
native <input type="checkbox"> | checked, onChange(event), name | boolean |
Checkbox, Switch | checked, onCheckedChange | boolean |
RadioGroup, Select, RegionSelect | value, onValueChange, name | string (UzRegionId) |
SegmentedControl, ColorPicker, OtpInput | value, onValueChange | string |
MaskedInput, PhoneInput, CardInput | value, onValueChange | string (toza raqamlar) |
PinflInput, PassportInput, InnInput, BankAccountInput, CarPlateInput | value, onValueChange | string (bo'shliqsiz) |
MoneyInput | value, onValueChange | number | null |
Slider | value, onValueChange | number[] |
Rating | value, onValueChange | number |
Combobox | value, onValueChange | string | null · multiple — string[] |
TagInput | value, onValueChange | string[] |
DatePicker | value, onValueChange | Date | null |
DateRangePicker | value, onValueChange | { from, to } | null |
FileUpload | onFilesChange (boshqarilmaydi) | FileUploadItem[] |
| boshqa komponentlar | value, onValueChange (konvensiya) | — |
Qoidalar:
- Bo'sh qiymat. Forma qiymati
undefined/nullbo'lsa, komponent boshqariladigan holatda qolishi uchun bo'sh qiymat beriladi: matnlarga'',Checkboxgafalse,TagInputga[],Ratingga0,Sliderga[min]. Shu tufaylireset()har bir komponentni tozalaydi. - O'z handler'ingiz ham ishlaydi.
<Input onChange={log} />— avval sizniki, keyin formaniki chaqiriladi. FileUploadichki holatini o'zi boshqaradi: forma qiymati tashqaridan o'zgarsa (reset,setValue) komponent qayta yaratiladi va ro'yxat tozalanadi.- Fokus. Submit'dagi xato yoki
form.setFocus(name)maydonning asosiy elementini fokuslaydi (input, Select tugmasi, slider dastagi ...). Bolagarefuzatish shart emas.
Barcha qatorlar integratsiya testlari bilan tekshirilgan: qiymat kiradi va chiqadi, xato
Field da chiqadi, fokus xatoli maydonga o'tadi, reset() ishlaydi.
Ko'p komponentli forma
value / onValueChange komponentlari (SegmentedControl, MoneyInput, TagInput) va
boshqarilmaydigan FileUpload bitta formada. Narx zUz.money({ min, max }) bilan tekshiriladi.
'use client';
import { zUz } from '@akhatmirzo/core/zod';
import { FileUpload, MoneyInput, SegmentedControl, TagInput, toast } from '@akhatmirzo/ui';
import { Form, FormField, FormSubmit } from '@akhatmirzo/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const schema = z.object({
condition: z.enum(['new', 'used'], { error: 'Holatini tanlang' }),
price: zUz.money({ min: 10_000, max: 500_000_000 }),
tags: z.array(z.string()).min(1, 'Kamida bitta teg').max(5, "Ko'pi bilan 5 ta teg"),
photos: z.array(z.unknown()).min(1, 'Kamida bitta rasm yuklang'),
});
export default function FormListing() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { price: null, tags: [], photos: [] },
});
return (
<Form
form={form}
onSubmit={(data) => {
toast.success("E'lon saqlandi", {
description: `${data.price} so'm · ${data.tags.join(', ')}`,
});
form.reset();
}}
className="flex w-full max-w-md flex-col gap-4"
>
<FormField name="condition" label="Holati">
<SegmentedControl fullWidth>
<SegmentedControl.Item value="new">Yangi</SegmentedControl.Item>
<SegmentedControl.Item value="used">Ishlatilgan</SegmentedControl.Item>
</SegmentedControl>
</FormField>
<FormField name="price" label="Narxi" description="10 000 so'mdan 500 mln so'mgacha">
<MoneyInput showWords />
</FormField>
<FormField name="tags" label="Teglar" description="Enter yoki vergul bilan qo'shing">
<TagInput placeholder="telefon, samsung" />
</FormField>
<FormField name="photos" label="Rasmlar">
<FileUpload accept={['image/*']} multiple maxFiles={4} />
</FormField>
<FormSubmit>E'lonni joylash</FormSubmit>
</Form>
);
}
Zaxira yo'li: render
Jadvalda yo'q yoki boshqa API'li element uchun render ni ishlating — value,
onValueChange va controlProps (id, aria-*) qo'lingizda:
'use client';
import { cn } from '@akhatmirzo/ui';
import { Form, FormField, FormSubmit } from '@akhatmirzo/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const slots = ['09:00–12:00', '12:00–15:00', '15:00–18:00', '18:00–21:00'];
const schema = z.object({
slot: z.string().min(1, 'Yetkazib berish vaqtini tanlang'),
});
export default function FormRenderFallback() {
const form = useForm({ resolver: zodResolver(schema), defaultValues: { slot: '' } });
return (
<Form
form={form}
onSubmit={(data) => alert(`Tanlandi: ${data.slot}`)}
className="flex w-full max-w-sm flex-col gap-4"
>
<FormField
name="slot"
label="Yetkazib berish vaqti"
render={({ value, onValueChange, controlProps, invalid }) => (
<div {...controlProps} role="radiogroup" className="grid grid-cols-2 gap-2">
{slots.map((slot) => (
<button
key={slot}
type="button"
role="radio"
aria-checked={value === slot}
onClick={() => onValueChange(slot)}
className={cn(
'rounded-lg border px-3 py-2 text-sm tabular-nums',
value === slot
? 'border-primary bg-primary-soft text-primary-soft-foreground'
: 'border-border bg-surface text-text',
invalid && value !== slot && 'border-danger',
)}
>
{slot}
</button>
))}
</div>
)}
/>
<FormSubmit>Tasdiqlash</FormSubmit>
</Form>
);
}
<FormField
name="city"
label="Shahar"
render={({ value, onValueChange, controlProps }) => (
<select {...controlProps} value={value} onChange={(e) => onValueChange(e.target.value)}>
<option value="tsh">Toshkent</option>
</select>
)}
/>O'z komponentingiz
useFormField() — FormField ichidagi komponent maydonga o'zi ulanadi. Bunday bolaga
adapter="none" bering, shunda unga prop'lar uzatilmaydi:
function Stepper() {
const { value, onValueChange, id, invalid } = useFormField<number>();
return (
<button type="button" id={id} aria-invalid={invalid} onClick={() => onValueChange(value + 1)}>
{value}
</button>
);
}
<FormField name="guests" label="Mehmonlar" adapter="none">
<Stepper />
</FormField>;createFormAdapter() — prop nomlari boshqacha bo'lgan komponent uchun:
import { createFormAdapter } from '@akhatmirzo/ui/form';
const pickerAdapter = createFormAdapter({
valueProp: 'selected',
changeProp: 'onSelect',
empty: [],
});
<FormField name="ids" label="Mahsulotlar" adapter={pickerAdapter}>
<ProductPicker />
</FormField>;Tayyor nomlar: adapter="native" (value + onChange(event)), "value", "checked",
"none".
Props
Form
Qolgan barcha <form> atributlari ham qabul qilinadi.
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
formmajburiy | UseFormReturn<TFieldValues, TContext, TTransformedValues> | — | useForm(...) natijasi |
onSubmitmajburiy | SubmitHandler<TTransformedValues> | — | Validatsiyadan o'tgan (resolver o'zgartirgan) qiymatlar bilan |
onInvalid | SubmitErrorHandler<TFieldValues> | — | Validatsiya xatolari bilan |
FormField
label, description, required, disabled, orientation, className va boshqa
Field prop'lari ham qabul qilinadi (error dan tashqari — u formadan olinadi).
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
namemajburiy | FieldPath<TFieldValues> | — | Maydon nomi |
children | ReactElement | — | Bitta boshqaruv elementi — prop'lar avtomatik uzatiladi |
render | (field: FormFieldRenderProps) => ReactNode | — | Zaxira yo'l: elementni o'zingiz chizasiz |
adapter | FormFieldAdapter | 'native' | 'nativeChecked' | 'value' | 'checked' | 'none' | — | Qiymat qanday uzatilishi; standart — avtomatik (moslik jadvali) |
control | Control<TFieldValues> | — | Berilmasa — eng yaqin <Form> dan |
rules | RegisterOptions | — | react-hook-form qoidalari |
defaultValue | FieldPathValue | — | Maydonning boshlang'ich qiymati |
shouldUnregister | boolean | — | O'chirilganda qiymat ham olib tashlanadi |
FormSubmit
Button prop'lari; type="submit", yuborilayotganda loading.
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
variant | ButtonVariant | 'primary' |
FormRootError
Alert prop'lari. Xato bo'lmasa hech narsa chizilmaydi.
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
color | 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'danger' | |
children | (message: string) => ReactNode | — | Matn o'rniga o'z ko'rinishingiz |
useFormField / render argumenti
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
name | string | — | |
value | TValue | — | Formadagi joriy qiymat |
onValueChange | (value: TValue) => void | — | |
onBlur | () => void | — | Maydon "tegilgan" deb belgilanadi |
invalid | boolean | — | |
error | string | undefined | — | Xato matni |
id | string | — | Label ulangan id |
disabled | boolean | — | |
controlProps | { id; 'aria-describedby'?; 'aria-invalid'?; 'aria-required'? } | — | Faqat render argumentida — native elementga yoyiladi |
Accessibility
- Label va xato bog'langan. Har bir maydon
Fieldorqali<label htmlFor>,aria-describedby(izoh + xato) vaaria-invalidoladi; native elementlar varenderham shu atributlarni oladi. - Xato e'lon qilinadi. Xato matni
aria-live="polite"hududida — submit'dan keyin ekran o'quvchi o'qiydi.FormRootError—role="alert". - Fokus xatoga o'tadi. Submit'da birinchi xatoli maydonning asosiy elementi fokuslanadi
(
shouldFocusError), klaviatura foydalanuvchisi darhol tuzatishni boshlaydi. - Brauzer validatsiyasi o'chirilgan (
noValidate) — xabarlar bir xil ko'rinishda, o'zbekcha. - Portal'li komponentlar (Select, Combobox, DateRangePicker) ochilganda maydon "tark etilgan" hisoblanmaydi
—
mode: 'onBlur'da xato muddatidan oldin chiqmaydi.