Pola
Validasi & pesan error
Kapan memeriksa isian, cara menulis pesan error, dan ringkasan kesalahan di atas form.
Contoh
Tekan Simpan: ringkasan muncul di atas dan menautkan ke tiap isian. Perbaiki satu per satu — pesan hilang begitu isiannya benar.
import { useEffect, useRef, useState } from 'react';
import { AmountInput, Banner, Button, FormField, Input, Select, ToastProvider, useToast } from '@divistant/circca-react';
// Pola validasi: periksa saat Simpan, lalu periksa ulang setiap isian berubah.
// Ringkasan di atas form menautkan ke tiap isian yang salah; tombol Simpan tetap aktif.
// Berhasil → Toast singkat (di aplikasi, ToastProvider dipasang sekali di root).
type Field = 'npwp' | 'email' | 'nilai' | 'termin';
interface Data {
npwp: string;
email: string;
nilai: number | null;
termin?: string;
}
const LABEL: Record<Field, string> = { npwp: 'NPWP', email: 'E-mail PIC', nilai: 'Plafon kredit', termin: 'Termin pembayaran' };
function periksa(d: Data): Partial<Record<Field, string>> {
const e: Partial<Record<Field, string>> = {};
if (d.npwp.replace(/\D/g, '').length !== 16) e.npwp = 'NPWP harus 16 digit. Periksa kembali angka di kartu NPWP.';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(d.email)) e.email = 'Tulis e-mail lengkap, mis. nama@perusahaan.co.id.';
if (!d.nilai) e.nilai = 'Isi plafon kredit lebih dari Rp 0.';
else if (d.nilai > 5_000_000_000) e.nilai = 'Plafon di atas Rp 5 M perlu persetujuan Direksi — ajukan lewat Change request.';
if (!d.termin) e.termin = 'Pilih termin pembayaran.';
return e;
}
function FormCustomer() {
const toast = useToast();
const [data, setData] = useState<Data>({ npwp: '01.234.567.8-901', email: 'maya@cakra', nilai: null });
const [dicoba, setDicoba] = useState(false); // sudah pernah menekan Simpan?
const [percobaan, setPercobaan] = useState(0); // naik setiap Simpan ditekan
const refs = useRef<Partial<Record<Field, HTMLElement | null>>>({});
const ringkasan = useRef<HTMLElement>(null);
const error = dicoba ? periksa(data) : {};
const salah = Object.keys(error) as Field[];
// Setelah Simpan gagal, pindahkan fokus ke ringkasan agar pembaca layar langsung membacanya.
useEffect(() => {
if (percobaan > 0) ringkasan.current?.focus();
}, [percobaan]);
const ubah = (patch: Partial<Data>) => {
setData(d => ({ ...d, ...patch }));
};
return (
<form
noValidate
className="grid max-w-lg gap-4"
onSubmit={e => {
e.preventDefault();
setDicoba(true);
if (Object.keys(periksa(data)).length) setPercobaan(n => n + 1);
else toast({ tone: 'success', message: 'Data PT Cakra Media Digital disimpan' });
}}
>
{salah.length > 0 && (
<Banner
ref={ringkasan}
tabIndex={-1}
announce
tone="danger"
title={`${salah.length} isian perlu diperbaiki`}
description={
<ul className="grid gap-1 pt-1">
{salah.map(f => (
<li key={f}>
<button type="button" className="text-danger-fg underline underline-offset-2" onClick={() => refs.current[f]?.focus()}>
{LABEL[f]}
</button>
</li>
))}
</ul>
}
/>
)}
<FormField label={LABEL.npwp} required helperText="16 digit, sesuai kartu NPWP." error={error.npwp}>
<Input ref={el => { refs.current.npwp = el; }} value={data.npwp} onChange={e => ubah({ npwp: e.target.value })} inputMode="numeric" />
</FormField>
<FormField label={LABEL.email} required error={error.email}>
<Input ref={el => { refs.current.email = el; }} type="email" value={data.email} onChange={e => ubah({ email: e.target.value })} />
</FormField>
<FormField label={LABEL.nilai} required error={error.nilai}>
<AmountInput ref={el => { refs.current.nilai = el; }} value={data.nilai} onValueChange={nilai => ubah({ nilai })} />
</FormField>
<FormField label={LABEL.termin} required error={error.termin}>
<Select
ref={el => { refs.current.termin = el; }}
value={data.termin}
onValueChange={termin => ubah({ termin })}
placeholder="Pilih termin…"
options={[
{ value: 'net14', label: 'Net 14 hari' },
{ value: 'net30', label: 'Net 30 hari' },
]}
/>
</FormField>
<div className="flex justify-end gap-2">
<Button variant="ghost">Batal</Button>
<Button type="submit" variant="primary">Simpan</Button>
</div>
</form>
);
}
export default function Example() {
return (
<ToastProvider>
<FormCustomer />
</ToastProvider>
);
}Kapan memeriksa
- 1Saat menyimpan, bukan saat mengetikJangan memarahi pengguna yang belum selesai mengetik. Periksa pertama kali saat tombol Simpan atau Ajukan ditekan.
- 2Setelah itu, periksa ulang setiap perubahanBegitu pesan error tampil, hilangkan segera setelah isiannya benar agar pengguna tahu perbaikannya berhasil.
- 3Pemeriksaan server tetap ditampilkan di isiannyaMis. "Nomor PO customer sudah dipakai di PO-2026-0131" — taruh di isian No. PO, bukan di notifikasi umum.
Ringkasan kesalahan
- 1Tampil di atas form bila ada lebih dari satu kesalahanBanner Danger berisi jumlah kesalahan dan tautan ke tiap isian. Fokus pindah ke Banner agar pembaca layar langsung membacanya.
- 2Tombol simpan tetap aktifMenonaktifkan tombol tanpa penjelasan membuat pengguna menebak. Biarkan aktif dan tunjukkan apa yang kurang. Pengecualian: syarat kelengkapan dokumen yang dijelaskan di daftar kelengkapan (lihat Form dokumen).
Menulis pesan error
NPWP harus 16 digit. Periksa kembali angka di kartu NPWP.
Input tidak valid!