Form Component
Koleksi komponen form untuk membangun form dengan validasi, error handling, dan berbagai tipe input. Komponen-komponen ini menggunakan Formik untuk state management dan Mantine untuk styling.
Dependencies
Pastikan dependencies berikut sudah terinstal:
npm install formik yup @mantine/core @mantine/hooks @mantine/dates @mantine/dropzone @tabler/icons-react react-select react-select-async-paginate axios react-query
Struktur Komponen
components/
├── Field/
│ ├── InputField.tsx # Text/number input dengan validasi
│ ├── DropZoneField.tsx # File upload dengan drag-drop
│ ├── GenericAsyncSelect.tsx # Async single select dengan pagination
│ ├── GenericAsyncMultiSelect.tsx # Async multi-select dengan pagination
│ ├── CreateableSelect.tsx # Select dengan opsi custom
│ └── [FieldSpecific].tsx # Field-specific (30+ variants)
├── StaticSelect/
│ └── StaticSelect.tsx # Static dropdown select
├── MultiSelectField/
│ └── MultiSelectAnggota.tsx # Multi-select anggota dari API
├── HorizontalInputWrapper/
│ └── HorizontalInputWrapper.tsx # Horizontal layout wrapper
└── FormLaporanKegiatan/
└── FormLaporanKegiatan.tsx # Complete form example
InputField
Komponen input text/number yang terintegrasi dengan Formik dan mendukung validasi inline.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
formikName | string | ✅* | - | Nama field di Formik (required jika disabledFormik=false) |
disabledFormik | boolean | ❌ | false | Disable integrasi Formik (standalone mode) |
label | string | ❌ | - | Label untuk input field |
textOnly | boolean | ❌ | false | Hanya terima input text (a-z, A-Z, spasi) |
numberOnly | boolean | ❌ | false | Hanya terima input angka (0-9) |
onChangeValue | (value: string) => void | ❌ | - | Callback saat value berubah |
required | boolean | ❌ | false | Mark field sebagai required |
placeholder | string | ❌ | - | Placeholder text |
disabled | boolean | ❌ | false | Disable input field |
defaultValue | string | ❌ | - | Default value (standalone mode) |
value | string | ❌ | - | Current value (standalone mode) |
Penggunaan dengan Formik
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { InputField } from '@/components/Field/InputField';
import { Button } from '@mantine/core';
// Schema validasi
const validationSchema = Yup.object({
nama: Yup.string()
.required('Nama harus diisi')
.min(3, 'Minimal 3 karakter'),
email: Yup.string()
.email('Email tidak valid')
.required('Email harus diisi'),
noTelepon: Yup.string()
.matches(/^[0-9]{10,12}$/, 'Nomor telepon harus 10-12 digit')
.required('Nomor telepon harus diisi'),
});
function UserFormComponent() {
return (
<Formik
initialValues={{
nama: '',
email: '',
noTelepon: '',
}}
validationSchema={validationSchema}
onSubmit={(values) => {
console.log(values);
}}
>
{({ values, errors, touched, setFieldValue }) => (
<Form>
<InputField
formikName="nama"
label="Nama Lengkap"
placeholder="Masukkan nama"
required
textOnly
/>
<InputField
formikName="email"
label="Email"
placeholder="contoh@email.com"
required
type="email"
/>
<InputField
formikName="noTelepon"
label="Nomor Telepon"
placeholder="62812345678"
required
numberOnly
/>
<Button type="submit">Simpan</Button>
</Form>
)}
</Formik>
);
}
Penggunaan Standalone (Tanpa Formik)
import { useState } from 'react';
import { InputField } from '@/components/Field/InputField';
function StandaloneInputComponent() {
const [name, setName] = useState('');
return (
<InputField
disabledFormik={true}
label="Nama"
placeholder="Masukkan nama"
value={name}
onChangeValue={(value) => setName(value)}
textOnly
required
/>
);
}
Validasi Khusus
// Hanya text
<InputField
formikName="nama"
label="Nama"
textOnly
required
/>
// Hanya angka
<InputField
formikName="umur"
label="Umur"
numberOnly
required
/>
// Text dan angka (normal)
<InputField
formikName="alamat"
label="Alamat"
required
/>
StaticSelect
Komponen select dengan opsi yang sudah tetap (tidak dari API).
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | ✅ | - | Label untuk select field |
placeholder | string | ✅ | - | Placeholder text |
data | Array<{value, label}> | ✅ | - | Array opsi select |
value | string | ❌ | - | Nilai yang dipilih |
onChange | (value: string) => void | ✅ | - | Callback saat value berubah |
required | boolean | ❌ | false | Mark field sebagai required |
clearable | boolean | ❌ | true | Bisa dikosongkan |
error | string | ❌ | - | Error message |
disabled | boolean | ❌ | false | Disable select field |
Contoh Dasar
import { useState } from 'react';
import StaticSelect from '@/components/StaticSelect/StaticSelect';
function StatusSelectComponent() {
const [status, setStatus] = useState('');
const statusOptions = [
{ value: 'active', label: 'Aktif' },
{ value: 'inactive', label: 'Tidak Aktif' },
{ value: 'pending', label: 'Tertunda' },
];
const handleStatusChange = (value: string) => {
setStatus(value);
console.log('Status dipilih:', value);
};
return (
<StaticSelect
label="Status Pengguna"
placeholder="Pilih status"
data={statusOptions}
value={status}
onChange={handleStatusChange}
required
/>
);
}
Dengan Error Handling
import { useState } from 'react';
import StaticSelect from '@/components/StaticSelect/StaticSelect';
function UserRoleSelectComponent() {
const [role, setRole] = useState('');
const [error, setError] = useState('');
const roleOptions = [
{ value: 'admin', label: 'Administrator' },
{ value: 'user', label: 'User' },
{ value: 'guest', label: 'Guest' },
];
const handleRoleChange = (value: string) => {
setRole(value);
if (!value) {
setError('Role harus dipilih');
} else {
setError('');
}
};
return (
<StaticSelect
label="Role Pengguna"
placeholder="Pilih role"
data={roleOptions}
value={role}
onChange={handleRoleChange}
error={error}
required
/>
);
}
HorizontalInputWrapper
Wrapper untuk layout horizontal form yang responsive.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | ✅ | - | Label untuk field |
withAsterisk | boolean | ❌ | false | Tampilkan asterisk untuk required field |
children | ReactNode | ✅ | - | Input field atau komponen apapun |
error | string | ❌ | - | Error message |
Contoh
import { TextInput } from '@mantine/core';
import HorizontalInputWrapper from '@/components/HorizontalInputWrapper/HorizontalInputWrapper';
function HorizontalFormComponent() {
const [nama, setNama] = useState('');
const [email, setEmail] = useState('');
const [error, setError] = useState('');
return (
<div>
<HorizontalInputWrapper
label="Nama Lengkap"
withAsterisk
>
<TextInput
placeholder="Masukkan nama"
value={nama}
onChange={(e) => setNama(e.target.value)}
/>
</HorizontalInputWrapper>
<HorizontalInputWrapper
label="Email"
withAsterisk
error={error}
>
<TextInput
placeholder="contoh@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => {
if (!email.includes('@')) {
setError('Email tidak valid');
} else {
setError('');
}
}}
/>
</HorizontalInputWrapper>
</div>
);
}
Komponen ini otomatis mengubah layout dari horizontal ke vertikal di mobile.
GenericAsyncSelect
Single select dengan opsi dari API, mendukung pagination dan search.
Props & Configuration
type LoaderConfig<TItem> = {
endpoint: string; // Path API endpoint
token?: string; // Auth token
perPage?: number; // Default 10
staticParams?: CustomParam[];
staticFilters?: FilterRule[];
sort?: {
columnAccessor: string;
direction: 'asc' | 'desc';
};
searchToFilters?: (search: string) => FilterRule[];
extractItems?: (json: unknown) => TItem[];
mapItemToOption: (item: TItem) => Option<TItem>;
};
type GenericAsyncSelectProps<TItem> = {
name: string;
value: Option<TItem> | null;
onChange: (v: Option<TItem> | null) => void;
onBlur?: () => void;
error?: string;
loadOptions: ReturnType<typeof createAsyncPaginateLoader<TItem>>;
initialAdditional?: AdditionalGeneric;
placeholder?: string;
debounceTimeout?: number;
isDisabled?: boolean;
isClearable?: boolean;
};
Contoh
import { useSelector } from 'react-redux';
import {
createAsyncPaginateLoader,
GenericAsyncSelect,
Option,
} from '@/components/Field/GenericAsyncSelect';
interface Pegawai {
id: string;
nama: string;
nopeg: string;
}
function PegawaiSelectComponent() {
const { token } = useSelector((state: RootState) => state.auth);
const [selectedPegawai, setSelectedPegawai] = useState<Option<Pegawai> | null>(null);
const loadOptions = createAsyncPaginateLoader<Pegawai>({
endpoint: '/kepegawaian/informasi-data-pegawai/',
token,
perPage: 20,
staticFilters: [
{
field: 'status',
operation: 'equals',
value: 'aktif',
},
],
searchToFilters: (search) => [
{
field: 'nama',
operation: 'contains',
value: search,
},
],
extractItems: (json: any) => json.data,
mapItemToOption: (pegawai) => ({
value: pegawai.id,
label: `${pegawai.nama} (${pegawai.nopeg})`,
detail: pegawai,
}),
});
return (
<GenericAsyncSelect<Pegawai>
name="pegawai_id"
value={selectedPegawai}
onChange={setSelectedPegawai}
loadOptions={loadOptions}
placeholder="Cari pegawai..."
isClearable
/>
);
}
Multiple Filters & Sorting
const loadOptions = createAsyncPaginateLoader<Pegawai>({
endpoint: '/kepegawaian/informasi-data-pegawai/',
token,
perPage: 20,
staticFilters: [
{ field: 'status', operation: 'equals', value: 'aktif' },
{ field: 'dinas_id', operation: 'equals', value: dinasId },
],
sort: {
columnAccessor: 'nama',
direction: 'asc',
},
searchToFilters: (search) => [
{ field: 'nama', operation: 'ilike', value: search },
{ field: 'nopeg', operation: 'prefix', value: search },
],
extractItems: (json: any) => json.data,
mapItemToOption: (pegawai) => ({
value: pegawai.id,
label: pegawai.nama,
detail: pegawai,
}),
});
GenericAsyncMultiSelect
Komponen multi-select dengan opsi dari API, mendukung pagination dan search. Props hampir sama dengan GenericAsyncSelect, tapi value bertipe array.
Contoh
import {
createAsyncPaginateLoader,
GenericAsyncMultiSelect,
Option,
} from '@/components/Field/GenericAsyncMultiSelect';
interface Kegiatan {
id: string;
nama: string;
}
function KegiatanMultiSelectComponent() {
const { token } = useSelector((state: RootState) => state.auth);
const [selectedKegiatan, setSelectedKegiatan] = useState<Option<Kegiatan>[]>([]);
const loadOptions = createAsyncPaginateLoader<Kegiatan>({
endpoint: '/master-data/jenis-kegiatan/',
token,
perPage: 15,
searchToFilters: (search) => [
{ field: 'nama', operation: 'contains', value: search },
],
extractItems: (json: any) => json.data,
mapItemToOption: (kegiatan) => ({
value: kegiatan.id,
label: kegiatan.nama,
detail: kegiatan,
}),
});
return (
<GenericAsyncMultiSelect<Kegiatan>
name="kegiatan_ids"
value={selectedKegiatan}
onChange={setSelectedKegiatan}
loadOptions={loadOptions}
placeholder="Pilih kegiatan..."
isClearable
/>
);
}
MultiSelectAnggota
Komponen multi-select khusus untuk memilih anggota/pegawai dari API.
Contoh
import { MultiSelectAnggota } from '@/components/MultiSelectField/MultiSelectAnggota';
import { Button } from '@mantine/core';
function AnggotaSelectionComponent() {
return (
<div>
<h2>Pilih Anggota Tim</h2>
<MultiSelectAnggota />
<Button>Simpan Anggota</Button>
</div>
);
}
DropZoneField
Komponen file upload dengan drag-and-drop interface dan preview.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
onDrop | (files: File[]) => void | ❌ | - | Callback saat file di-drop |
initialFiles | string | ❌ | - | Path file yang sudah upload (comma-separated) |
label | string | ❌ | - | Label untuk field |
disabled | boolean | ❌ | false | Disable upload |
| Plus semua Mantine Dropzone props |
Contoh
import { useState } from 'react';
import DropZoneField from '@/components/Field/DropZoneField';
import { Button } from '@mantine/core';
function FileUploadComponent() {
const [files, setFiles] = useState<File[]>([]);
const handleUpload = async () => {
const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});
try {
await uploadFiles(formData);
} catch (error) {
console.error(error);
}
};
return (
<div>
<DropZoneField
label="Upload Dokumen"
onDrop={(droppedFiles) => setFiles(droppedFiles)}
accept={{ 'application/pdf': ['.pdf'], 'image/*': ['.png', '.jpg'] }}
multiple
/>
<Button onClick={handleUpload}>Simpan Dokumen</Button>
</div>
);
}
FormLaporanKegiatan
Contoh implementasi form kompleks yang menggunakan beberapa komponen dengan Formik.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | ❌ | 'Default Title' | Judul form |
id | string | ❌ | '' | ID untuk edit mode |
disable | boolean | ❌ | - | Disable semua fields |
disableInterface | boolean | ❌ | - | Disable UI elements |
Contoh
import FormLaporanKegiatan from '@/components/FormLaporanKegiatan/FormLaporanKegiatan';
function LaporanKegiatanPage() {
return (
<div>
<h1>Form Laporan Kegiatan</h1>
<FormLaporanKegiatan title="Kirim Laporan Kegiatan" />
</div>
);
}
// Edit mode
function EditLaporanKegiatanPage({ id }) {
return (
<FormLaporanKegiatan title="Edit Laporan Kegiatan" id={id} />
);
}
Struktur Form Laporan Kegiatan
{
id: string;
jenis_kegiatan_id: string;
uraian_kegiatan: string;
tanggal: string;
waktu_start: string;
waktu_end: string;
lokasi: string;
}
Best Practices
Form Validation dengan Yup
import * as Yup from 'yup';
const validationSchema = Yup.object({
nama: Yup.string()
.required('Nama harus diisi')
.min(3, 'Minimal 3 karakter')
.max(100, 'Maksimal 100 karakter'),
email: Yup.string()
.email('Email tidak valid')
.required('Email harus diisi'),
noTelepon: Yup.string()
.matches(/^[0-9]{10,12}$/, 'Nomor telepon harus 10-12 digit')
.required('Nomor telepon harus diisi'),
tanggal: Yup.date()
.required('Tanggal harus dipilih')
.typeError('Format tanggal tidak valid'),
});
Error Handling
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={async (values) => {
try {
const response = await submitForm(values);
showNotification({
title: 'Sukses',
message: 'Data berhasil disimpan',
color: 'green',
});
navigate('/success');
} catch (error) {
showNotification({
title: 'Error',
message: error.message,
color: 'red',
});
}
}}
>
{({ errors, touched, isSubmitting }) => (
<Form>
{/* Fields */}
<Button type="submit" loading={isSubmitting}>
Simpan
</Button>
</Form>
)}
</Formik>
Conditional Fields
{({ values }) => (
<>
<InputField
formikName="jenis"
label="Jenis"
/>
{values.jenis === 'khusus' && (
<InputField
formikName="keterangan_khusus"
label="Keterangan Khusus"
required
/>
)}
</>
)}
Dynamic Fields Array
import { FieldArray } from 'formik';
<FieldArray name="anggota">
{(arrayHelpers) => (
<div>
{values.anggota.map((anggota, index) => (
<div key={index}>
<InputField
formikName={`anggota.${index}.nama`}
label="Nama Anggota"
/>
<Button
onClick={() => arrayHelpers.remove(index)}
color="red"
>
Hapus
</Button>
</div>
))}
<Button onClick={() => arrayHelpers.push({ nama: '' })}>
+ Tambah Anggota
</Button>
</div>
)}
</FieldArray>
Contoh Form Lengkap
import { Formik, Form, FieldArray } from 'formik';
import * as Yup from 'yup';
import { Button, SimpleGrid, Group } from '@mantine/core';
import { InputField } from '@/components/Field/InputField';
import StaticSelect from '@/components/StaticSelect/StaticSelect';
import HorizontalInputWrapper from '@/components/HorizontalInputWrapper/HorizontalInputWrapper';
import { showNotification } from '@mantine/notifications';
const validationSchema = Yup.object({
nama: Yup.string().required('Nama harus diisi'),
email: Yup.string().email().required('Email harus diisi'),
status: Yup.string().required('Status harus dipilih'),
anggota: Yup.array().of(
Yup.object({
nama: Yup.string().required('Nama anggota harus diisi'),
})
),
});
function CompleteFormComponent() {
const statusOptions = [
{ value: 'aktif', label: 'Aktif' },
{ value: 'non-aktif', label: 'Non-Aktif' },
];
return (
<Formik
initialValues={{
nama: '',
email: '',
status: '',
anggota: [{ nama: '' }],
}}
validationSchema={validationSchema}
onSubmit={async (values) => {
try {
await submitForm(values);
showNotification({
title: 'Sukses',
message: 'Data berhasil disimpan',
color: 'green',
});
} catch (error) {
showNotification({
title: 'Error',
message: 'Gagal menyimpan data',
color: 'red',
});
}
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<HorizontalInputWrapper label="Nama" withAsterisk>
<InputField
formikName="nama"
placeholder="Masukkan nama"
required
/>
</HorizontalInputWrapper>
<HorizontalInputWrapper label="Email" withAsterisk>
<InputField
formikName="email"
placeholder="contoh@email.com"
type="email"
required
/>
</HorizontalInputWrapper>
<HorizontalInputWrapper label="Status" withAsterisk>
<StaticSelect
label=""
placeholder="Pilih status"
data={statusOptions}
value={values.status}
onChange={(value) => setFieldValue('status', value)}
required
/>
</HorizontalInputWrapper>
<FieldArray name="anggota">
{(arrayHelpers) => (
<div>
{values.anggota.map((_, index) => (
<HorizontalInputWrapper
key={index}
label={`Anggota ${index + 1}`}
withAsterisk
>
<Group>
<InputField
formikName={`anggota.${index}.nama`}
placeholder="Nama anggota"
required
/>
<Button
onClick={() => arrayHelpers.remove(index)}
color="red"
variant="light"
>
Hapus
</Button>
</Group>
</HorizontalInputWrapper>
))}
<Button
onClick={() => arrayHelpers.push({ nama: '' })}
variant="default"
>
+ Tambah Anggota
</Button>
</div>
)}
</FieldArray>
<Group mt="xl" justify="flex-end">
<Button variant="default" type="reset">
Reset
</Button>
<Button type="submit" loading={isSubmitting}>
Simpan
</Button>
</Group>
</Form>
)}
</Formik>
);
}
export default CompleteFormComponent;
🔧 Field-Specific Components
Ada 30+ field-specific components yang tersedia untuk berbagai kebutuhan:
Location Fields
FieldProvinsi- Select provinsiFieldKota- Select kotaFieldKecamatan- Select kecamatanFieldKelurahan- Select kelurahanFieldWilayah- Select wilayahFieldWilayahMultiple- Multi-select wilayah
Employee Fields
FieldDataPegawai- Select pegawai dengan searchFieldPangkat- Select pangkatFieldGolongan- Select golonganFieldJabatan- Select jabatanFieldEselon- Select eselonFieldSKPD- Select SKPD
Master Data Fields
FieldJenisKegiatan- Select jenis kegiatanFieldJenisPasal- Select jenis pasalFieldJenisKondisi- Select kondisi barangFieldJenisSatuan- Select satuanFieldJenisUsaha- Select jenis usaha
Advanced Fields
FieldPhoneNumber- Phone number dengan formatFieldSelectAgama- Select agamaFieldPilihRegu- Select reguFieldInformasiPenggunaPaginate- Select pengguna dengan pagination
🧪 Testing Examples
Unit Test InputField
import { render, screen, fireEvent } from '@testing-library/react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { InputField } from '@/components/Field/InputField';
describe('InputField', () => {
it('should render input field with label', () => {
render(
<Formik
initialValues={{ name: '' }}
validationSchema={Yup.object({ name: Yup.string() })}
onSubmit={() => {}}
>
<Form>
<InputField formikName="name" label="Nama" />
</Form>
</Formik>
);
expect(screen.getByText('Nama')).toBeInTheDocument();
});
it('should update value on input change', () => {
const { container } = render(
<Formik
initialValues={{ name: '' }}
validationSchema={Yup.object()}
onSubmit={() => {}}
>
<Form>
<InputField formikName="name" label="Nama" />
</Form>
</Formik>
);
const input = container.querySelector('input');
fireEvent.change(input!, { target: { value: 'John Doe' } });
expect((input as HTMLInputElement).value).toBe('John Doe');
});
it('should accept only text when textOnly is true', () => {
const { container } = render(
<Formik
initialValues={{ name: '' }}
validationSchema={Yup.object()}
onSubmit={() => {}}
>
<Form>
<InputField formikName="name" label="Nama" textOnly />
</Form>
</Formik>
);
const input = container.querySelector('input') as HTMLInputElement;
fireEvent.change(input, { target: { value: '123' } });
// Should not have numbers
expect(input.value).not.toContain('123');
});
});
🚀 Migration ke Project Lain
Langkah 1: Copy Komponen
# Copy field components
cp -r components/Field /path/to/new-project/components/
# Copy other form components
cp -r components/StaticSelect /path/to/new-project/components/
cp -r components/MultiSelectField /path/to/new-project/components/
cp -r components/HorizontalInputWrapper /path/to/new-project/components/
Langkah 2: Update Imports
Sesuaikan path imports sesuai struktur project:
// Dari
import { InputField } from '@/components/Field/InputField';
import StaticSelect from '@/components/StaticSelect/StaticSelect';
// Ke (sesuaikan dengan project Anda)
import { InputField } from '../components/Field/InputField';
import StaticSelect from '../components/StaticSelect/StaticSelect';
Langkah 3: Setup Axios & API
Pastikan axios instance dan httpHeader sudah di-setup:
// lib/axios/index.ts
import axios from 'axios';
export const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});
export const httpHeader = (token: string) => ({
headers: {
Authorization: `Bearer ${token}`,
},
});
Langkah 4: Environment Variables
Tambahkan ke .env:
NEXT_PUBLIC_API_URL=https://your-api.com/api/
API Reference
InputField
- Input text/number terintegrasi dengan Formik
- Support validasi custom
- Format khusus untuk text-only dan number-only
StaticSelect
- React-Select wrapper dengan Mantine styling
- Untuk opsi yang sudah tetap
- Support error handling
HorizontalInputWrapper
- Layout helper untuk form horizontal
- Auto-responsive di mobile
GenericAsyncSelect / GenericAsyncMultiSelect
- Async select dengan pagination
- Customizable filter & mapping
- Built-in search
DropZoneField
- File upload dengan drag-drop
- Preview gambar
- Support multiple files
FormLaporanKegiatan
- Contoh form kompleks
- Mode create & update
Troubleshooting
Formik Value Tidak Update
Pastikan formikName sesuai dengan initialValues:
// ✅ Benar
<Formik
initialValues={{ nama: '' }}
onSubmit={() => {}}
>
<InputField formikName="nama" />
</Formik>
// ❌ Salah
<InputField formikName="nama_lengkap" />
Async Select Tidak Fetch Data
Pastikan token valid dan endpoint benar:
const { token } = useSelector((state: RootState) => state.auth);
if (!token) return <div>Loading...</div>;
const loadOptions = createAsyncPaginateLoader({
endpoint: '/correct-endpoint/',
token,
// ...
});
Validasi Tidak Berfungsi
Pastikan validationSchema sesuai dengan field names:
const validationSchema = Yup.object({
nama: Yup.string().required('Nama harus diisi'),
email: Yup.string().email().required('Email harus diisi'),
});
// Render errors
{errors.nama && touched.nama && <Text color="red">{errors.nama}</Text>}
Referensi
- Formik Documentation
- Yup Schema Validation
- Mantine Form Components
- React-Select
- React-Select-Async-Paginate
Terakhir diupdate: January 2026
Versi: 1.0.0