feat: add useConditionalField hook and implement reactive form showcase examples

This commit is contained in:
Firman Ramdhani
2026-06-15 22:03:11 +07:00
parent f7c7bc6907
commit 50cce32ada
14 changed files with 862 additions and 172 deletions
@@ -0,0 +1,187 @@
import { useForm } from 'react-hook-form';
import { Button, Paper, Title, Group, Stack, Code, Divider, Text, Chip, Radio } from '@repo/ui/components';
import {
FieldTextInput, FieldPasswordInput, FieldTextarea, FieldNumberInput,
FieldJsonInput, FieldPinInput, FieldAutocomplete, FieldSelect,
FieldMultiSelect, FieldNativeSelect, FieldTagsInput, FieldCheckbox,
FieldRadioGroup, FieldSwitch, FieldChipGroup, FieldSegmentedControl,
FieldSlider, FieldRangeSlider, FieldRating, FieldColorInput,
FieldColorPicker, FieldFileInput
} from '@repo/ui/form';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
export default function AllFieldsDemo() {
const t = useFormDemoTranslation();
const { control, handleSubmit, watch } = useForm<any>({
defaultValues: {
customerName: '',
email: '',
password: '',
description: '',
age: undefined,
jsonConfig: '',
pin: '',
country: '',
orderType: '',
categories: [],
nativeOrderType: '',
tags: [],
terms: false,
priority: '',
receiveEmails: false,
chipSelection: '',
segmentedPriority: 'normal',
satisfaction: 5,
priceRange: [0, 100],
rating: 0,
themeColor: '',
colorPicker: '#1c7ed6',
avatar: null
}
});
const onSubmit = (data: any) => console.log('All Fields Submitted:', data);
const data = watch();
return (
<Stack gap="xl">
<Paper p="xl" withBorder radius="md">
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="xl">
{/* --- Text & Numbers --- */}
<div>
<Title order={5} mb="sm" c="brand">Text & Numbers</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldTextInput name="customerName" control={control} label={t.fields.customerName} />
<FieldTextInput name="email" control={control} label={t.fields.email} />
</Group>
<Group grow align="flex-start" mb="md">
<FieldPasswordInput name="password" control={control} label={t.fields.password} />
<FieldNumberInput name="age" control={control} label={t.fields.age} />
</Group>
<Group grow align="flex-start" mb="md">
<FieldTextarea name="description" control={control} label={t.fields.description} minRows={3} />
<FieldJsonInput name="jsonConfig" control={control} label={t.fields.jsonConfig} formatOnBlur />
</Group>
<div>
<Text size="sm" fw={500} mb={3}>{t.fields.pin}</Text>
<FieldPinInput name="pin" control={control} length={6} />
</div>
</div>
{/* --- Selections --- */}
<div>
<Title order={5} mb="sm" c="brand">Selections</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldSelect
name="orderType"
control={control}
label={t.fields.orderType}
data={['BULK', 'RETAIL']}
/>
<FieldNativeSelect
name="nativeOrderType"
control={control}
label={`Native ${t.fields.orderType}`}
data={['BULK', 'RETAIL']}
/>
</Group>
<Group grow align="flex-start" mb="md">
<FieldAutocomplete
name="country"
control={control}
label="Country"
data={['Indonesia', 'Singapore', 'Malaysia']}
/>
<FieldMultiSelect
name="categories"
control={control}
label="Categories"
data={['Electronics', 'Fashion', 'Food']}
/>
</Group>
<FieldTagsInput name="tags" control={control} label={t.fields.tags} />
</div>
{/* --- Toggles & Choices --- */}
<div>
<Title order={5} mb="sm" c="brand">Toggles & Choices</Title>
<Divider mb="md" />
<Group mb="md">
<FieldCheckbox name="terms" control={control} label={t.fields.terms} />
<FieldSwitch name="receiveEmails" control={control} label={t.fields.receiveEmails} />
</Group>
<FieldRadioGroup
name="priority"
control={control}
label={t.fields.priority}
mb="md"
>
<Group mt="xs">
<Radio value="low" label="Low" />
<Radio value="high" label="High" />
</Group>
</FieldRadioGroup>
<FieldSegmentedControl
name="segmentedPriority"
control={control}
label={t.fields.priority}
data={[
{ label: 'Normal', value: 'normal' },
{ label: 'Urgent', value: 'urgent' }
]}
mb="md"
/>
<div>
<Text size="sm" fw={500} mb={3}>Chip Selection</Text>
<FieldChipGroup
name="chipSelection"
control={control}
>
<Group>
<Chip value="1">Option 1</Chip>
<Chip value="2">Option 2</Chip>
</Group>
</FieldChipGroup>
</div>
</div>
{/* --- Ranges & Specialized --- */}
<div>
<Title order={5} mb="sm" c="brand">Ranges & Specialized</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldSlider name="satisfaction" control={control} label={t.fields.rating} />
<FieldRangeSlider name="priceRange" control={control} label="Price Range" />
</Group>
<Group grow align="flex-start" mb="md">
<FieldColorInput name="themeColor" control={control} label={t.fields.themeColor} />
<FieldFileInput name="avatar" control={control} label={t.fields.avatar} />
</Group>
<Group grow align="flex-start" mb="md">
<div>
<Text size="sm" fw={500} mb={3}>{t.fields.themeColor} Picker</Text>
<FieldColorPicker name="colorPicker" control={control} />
</div>
<div>
<Text size="sm" fw={500} mb={3}>{t.fields.rating}</Text>
<FieldRating name="rating" control={control} />
</div>
</Group>
</div>
<Button type="submit" mt="md">{t.common.submit}</Button>
</Stack>
</form>
</Paper>
<Paper p="md" withBorder radius="md" bg="var(--mantine-color-gray-0)">
<Title order={6} mb="xs">{t.common.submittedData}</Title>
<Code block>{JSON.stringify(data, null, 2)}</Code>
</Paper>
</Stack>
);
}
@@ -0,0 +1,235 @@
import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button, Paper, Title, Divider, Stack, Code, Alert } from '@repo/ui/components';
import { FieldTextInput, FieldSelect, FieldSwitch } from '@repo/ui/form';
import { useConditionalField } from '@repo/ui/hooks';
import { compose, required, emailValidator } from '@repo/ui/validators';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
import { Info } from 'lucide-react';
export default function ReactiveWatchDemo() {
const t = useFormDemoTranslation();
// Define atomic validators for conditional fields
const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId));
const spouseNameValidator = compose(z.string(), required(t.watch.spouseName));
const newsletterEmailValidator = compose(z.string(), required('Newsletter Email'), emailValidator());
const roleValidator = compose(z.string(), required('Role'));
const reactiveSchema = z
.object({
userType: z.enum(['PERSONAL', 'CORPORATE']),
corporateTaxId: z.string().optional(),
hasSpouse: z.boolean(),
spouseName: z.string().optional(),
newsletter: z.boolean(),
newsletterEmail: z.string().optional(),
department: z.string().optional(),
role: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.userType === 'CORPORATE') {
const res = taxIdValidator.safeParse(data.corporateTaxId || '');
if (!res.success) res.error.issues.forEach((i: any) => ctx.addIssue({ ...i, path: ['corporateTaxId'] }));
}
if (data.hasSpouse) {
const res = spouseNameValidator.safeParse(data.spouseName || '');
if (!res.success) res.error.issues.forEach((i: any) => ctx.addIssue({ ...i, path: ['spouseName'] }));
}
if (data.newsletter) {
const res = newsletterEmailValidator.safeParse(data.newsletterEmail || '');
if (!res.success) res.error.issues.forEach((i: any) => ctx.addIssue({ ...i, path: ['newsletterEmail'] }));
}
if (data.department) {
const res = roleValidator.safeParse(data.role || '');
if (!res.success) res.error.issues.forEach((i: any) => ctx.addIssue({ ...i, path: ['role'] }));
}
});
const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({
resolver: zodResolver(reactiveSchema as any),
defaultValues: {
userType: 'PERSONAL',
corporateTaxId: '',
hasSpouse: false,
spouseName: '',
newsletter: false,
newsletterEmail: '',
department: '',
role: '',
},
});
// Micro-subscriptions via useWatch
const userType = useWatch({ control, name: 'userType' });
const hasSpouse = useWatch({ control, name: 'hasSpouse' });
const newsletter = useWatch({ control, name: 'newsletter' });
const department = useWatch({ control, name: 'department' });
const role = useWatch({ control, name: 'role' });
// Use the custom hook to cleanly unregister and reset fields when hidden
useConditionalField({
condition: userType === 'CORPORATE',
name: 'corporateTaxId',
setValue,
unregister,
mode: 'unregister',
defaultValue: '',
});
useConditionalField({
condition: hasSpouse === true,
name: 'spouseName',
setValue,
unregister,
mode: 'unregister',
defaultValue: '',
});
// Use the reset mode to clear values and errors without unregistering the field
useConditionalField({
condition: newsletter === true,
name: 'newsletterEmail',
setValue,
clearErrors,
mode: 'reset',
defaultValue: '',
});
// Cascading Dropdown Logic: Department -> Role
const roleOptions: Record<string, { value: string; label: string }[]> = {
IT: [
{ value: 'FRONTEND', label: 'Frontend Engineer' },
{ value: 'BACKEND', label: 'Backend Engineer' },
],
HR: [
{ value: 'RECRUITER', label: 'Technical Recruiter' },
{ value: 'MANAGER', label: 'HR Manager' },
],
FINANCE: [
{ value: 'ACCOUNTANT', label: 'Accountant' },
{ value: 'ANALYST', label: 'Financial Analyst' },
],
};
const currentRoleOptions = department ? roleOptions[department] : [];
const isRoleValid = !role || (!!department && currentRoleOptions.some((opt) => opt.value === role));
// Reset Mode: Automatically clears the 'role' field value and errors if the department changes
// and the currently selected role is no longer valid for the new department.
useConditionalField({
condition: isRoleValid,
name: 'role',
setValue,
clearErrors,
mode: 'reset',
defaultValue: '',
});
// Use watch only to display the JSON output at the bottom
const allValues = useWatch({ control });
const onSubmit = (data: any) => console.log('Reactive Passed:', data);
return (
<Stack gap="xl">
<Paper p="xl" withBorder radius="md">
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<Title order={5} mb="sm" c="brand">
Dynamic Fields & Validation
</Title>
<Divider mb="sm" />
<Alert icon={<Info size={16} />} title="Micro-subscription Pattern" color="blue" variant="light">
This form demonstrates isolated re-rendering using <code>useWatch</code>. Instead of re-rendering the
entire form root when typing, only the specific conditional fields update their display states.
</Alert>
<Divider label="Hidden/Unmounted Pattern" labelPosition="center" my="sm" />
<FieldSelect
name="userType"
control={control}
label={t.watch.userType}
data={[
{ value: 'PERSONAL', label: t.watch.typePersonal || 'Personal' },
{ value: 'CORPORATE', label: t.watch.typeCorporate || 'Corporate' },
]}
withAsterisk
/>
{userType === 'CORPORATE' && (
<FieldTextInput name="corporateTaxId" control={control} label={t.watch.corporateTaxId} withAsterisk />
)}
<FieldSwitch name="hasSpouse" control={control} label={t.watch.hasSpouse} mt="md" />
{hasSpouse && (
<FieldTextInput name="spouseName" control={control} label={t.watch.spouseName} withAsterisk />
)}
<Divider label="Visible but Disabled Pattern" labelPosition="center" my="md" />
<FieldSwitch name="newsletter" control={control} label="Subscribe to Newsletter" />
<FieldTextInput
name="newsletterEmail"
control={control}
label="Newsletter Email"
disabled={!newsletter}
placeholder="Enter your email to subscribe"
withAsterisk={newsletter}
/>
<Divider label="Reset Mode (Cascading Dependencies)" labelPosition="center" my="md" />
<FieldSelect
name="department"
control={control}
label="Department"
placeholder="Select a department"
data={[
{ value: 'IT', label: 'Information Technology' },
{ value: 'HR', label: 'Human Resources' },
{ value: 'FINANCE', label: 'Finance' },
]}
/>
{/* * ⚠️ CRITICAL UI FIX: DYNAMIC KEY
* Why bind the 'key' to the parent dependency (department)?
* * Mantine's Select component caches its internal visual state. When the parent
* 'department' changes, our useConditionalField hook successfully clears the RHF
* payload state, but Mantine might visually retain the old text on the screen.
* * By changing the 'key' whenever the department changes, we force React to
* completely unmount and remount this component. This destroys Mantine's old
* internal cache and guarantees a perfectly clean UI sync.
*/}
<FieldSelect
key={`role-select-${department}`}
name="role"
control={control}
label="Role"
placeholder="Select a role"
disabled={!department}
data={currentRoleOptions}
withAsterisk={!!department}
/>
<Button type="submit" mt="md">
{t.common?.submit || 'Submit Reactive Form'}
</Button>
</Stack>
</form>
</Paper>
<Paper p="md" withBorder radius="md" bg="var(--mantine-color-gray-0)">
<Title order={6} mb="xs">
{t.common?.submittedData || 'Submitted Data'}
</Title>
<Code block>{JSON.stringify(allValues, null, 2)}</Code>
</Paper>
</Stack>
);
}
@@ -0,0 +1,114 @@
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button, Paper, Title, Group, Stack, Code, Divider } from '@repo/ui/components';
import {
FieldTextInput, FieldPasswordInput, FieldNumberInput
} from '@repo/ui/form';
import {
compose, required, rangeLength,
positiveNumber, simplePassword,
complexPassword, phoneValidator, rangeValue
} from '@repo/ui/validators';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
export default function ValidationBankDemo() {
const t = useFormDemoTranslation();
// Compose the Zod schema using the atomic validators
const validationSchema = z.object({
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
phone: compose(z.string(), required(t.validation.phone), phoneValidator())
});
type ValidationFormValues = z.infer<typeof validationSchema>;
const { control, handleSubmit, watch } = useForm<ValidationFormValues>({
resolver: zodResolver(validationSchema),
defaultValues: {
username: '',
simplePass: '',
complexPass: '',
age: undefined as any,
score: undefined as any,
phone: ''
}
});
const onSubmit = (data: ValidationFormValues) => console.log('Validation Passed:', data);
const data = watch();
return (
<Stack gap="xl">
<Paper p="xl" withBorder radius="md">
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<Title order={5} c="brand">Validation Bank (Atomic Registry)</Title>
<Divider mb="sm" />
<FieldTextInput
name="username"
control={control}
label={t.fields.customerName}
description={t.validation.usernameRange}
withAsterisk
/>
<Group grow align="flex-start">
<FieldPasswordInput
name="simplePass"
control={control}
label={t.validation.simplePassword}
description="Min 6 chars"
withAsterisk
/>
<FieldPasswordInput
name="complexPass"
control={control}
label={t.validation.complexPassword}
description="Min 8, 1 uppercase, 1 number, 1 special"
withAsterisk
/>
</Group>
<Group grow align="flex-start">
<FieldNumberInput
name="age"
control={control}
label={t.fields.age}
description={t.validation.ageRange}
withAsterisk
/>
<FieldNumberInput
name="score"
control={control}
label={t.validation.score}
description="Must be > 0"
withAsterisk
/>
</Group>
<FieldTextInput
name="phone"
control={control}
label={t.validation.phone}
description="Format: +62..."
withAsterisk
/>
<Button type="submit" mt="md">{t.common.submit}</Button>
</Stack>
</form>
</Paper>
<Paper p="md" withBorder radius="md" bg="var(--mantine-color-gray-0)">
<Title order={6} mb="xs">{t.common.submittedData}</Title>
<Code block>{JSON.stringify(data, null, 2)}</Code>
</Paper>
</Stack>
);
}
@@ -0,0 +1,31 @@
import { Tabs } from '@repo/ui/components';
import AllFieldsDemo from './components/all-fields-demo';
import ValidationBankDemo from './components/validation-bank-demo';
import ReactiveWatchDemo from './components/reactive-watch-demo';
import { useFormDemoTranslation } from './i18n/useFormDemoTranslation';
export default function FormDemoView() {
const t = useFormDemoTranslation();
return (
<Tabs defaultValue="all-fields" variant="outline" radius="md">
<Tabs.List mb="md">
<Tabs.Tab value="all-fields">{t.tabs.allFields}</Tabs.Tab>
<Tabs.Tab value="validation-bank">{t.tabs.validationBank}</Tabs.Tab>
<Tabs.Tab value="reactive-watch">{t.tabs.reactiveWatch}</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="all-fields">
<AllFieldsDemo />
</Tabs.Panel>
<Tabs.Panel value="validation-bank">
<ValidationBankDemo />
</Tabs.Panel>
<Tabs.Panel value="reactive-watch">
<ReactiveWatchDemo />
</Tabs.Panel>
</Tabs>
);
}
@@ -0,0 +1,47 @@
{
"tabs": {
"allFields": "All 22 Fields Demo",
"validationBank": "Validation Bank",
"reactiveWatch": "Reactive Watch (useWatch)"
},
"common": {
"submit": "Submit Data",
"reset": "Reset Form",
"submittedData": "Submitted Data"
},
"fields": {
"customerName": "Customer Name",
"email": "Email Address",
"priority": "Production Priority",
"password": "Password",
"description": "Description",
"age": "Age",
"jsonConfig": "JSON Config",
"tags": "Tags",
"terms": "I agree to the terms and conditions",
"receiveEmails": "Receive marketing emails",
"rating": "Satisfaction Rating",
"themeColor": "Theme Color",
"avatar": "Avatar Upload",
"orderType": "Order Type",
"quantity": "Quantity",
"fabricColor": "Fabric Color",
"pin": "Security PIN"
},
"validation": {
"simplePassword": "Simple Password",
"complexPassword": "Complex Password",
"score": "Score (Positive)",
"ageRange": "Age Range (18-65)",
"usernameRange": "Username Length (3-15)",
"phone": "Phone Number (+62)"
},
"watch": {
"userType": "User Type",
"typePersonal": "Personal",
"typeCorporate": "Corporate",
"corporateTaxId": "Corporate Tax ID",
"hasSpouse": "Do you have a spouse?",
"spouseName": "Spouse Name"
}
}
@@ -0,0 +1,47 @@
{
"tabs": {
"allFields": "Demo 22 Field",
"validationBank": "Bank Validasi",
"reactiveWatch": "Reactive Watch (useWatch)"
},
"common": {
"submit": "Kirim Data",
"reset": "Reset Form",
"submittedData": "Data Terkirim"
},
"fields": {
"customerName": "Nama Pelanggan",
"email": "Alamat Email",
"priority": "Prioritas Produksi",
"password": "Kata Sandi",
"description": "Deskripsi",
"age": "Usia",
"jsonConfig": "Konfigurasi JSON",
"tags": "Label (Tags)",
"terms": "Saya setuju dengan syarat dan ketentuan",
"receiveEmails": "Terima email pemasaran",
"rating": "Peringkat Kepuasan",
"themeColor": "Warna Tema",
"avatar": "Unggah Avatar",
"orderType": "Tipe Pesanan",
"quantity": "Jumlah",
"fabricColor": "Warna Kain",
"pin": "PIN Keamanan"
},
"validation": {
"simplePassword": "Sandi Sederhana",
"complexPassword": "Sandi Kompleks",
"score": "Skor (Positif)",
"ageRange": "Rentang Usia (18-65)",
"usernameRange": "Panjang Username (3-15)",
"phone": "Nomor Telepon (+62)"
},
"watch": {
"userType": "Tipe Pengguna",
"typePersonal": "Personal",
"typeCorporate": "Perusahaan",
"corporateTaxId": "NPWP Perusahaan",
"hasSpouse": "Apakah Anda memiliki pasangan?",
"spouseName": "Nama Pasangan"
}
}
@@ -0,0 +1,10 @@
import { useTranslation } from 'react-i18next';
import en from './en.json';
import id from './id.json';
export type FormDemoI18n = typeof en;
export function useFormDemoTranslation(): FormDemoI18n {
const { i18n } = useTranslation();
return (i18n.language === 'id' ? id : en) as FormDemoI18n;
}
@@ -0,0 +1 @@
export { default } from './form-demo-view';
@@ -1,169 +0,0 @@
import { useState } from 'react';
import { z } from 'zod';
import { useForm, FormProvider, type SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Paper, Title, Group, Stack, Code, Text } from '@repo/ui/components';
// Import our RHF-connected Field components from packages/ui
import {
FieldTextInput,
FieldNumberInput,
FieldSelect,
FieldColorInput,
FieldCheckbox,
FieldSegmentedControl
} from '@repo/ui/form';
// ─── 1. VALIDATOR SCHEMA WITH JSON I18N PAYLOAD ───────────────────
const erpOrderSchema = z.object({
customerName: z.string().min(3, {
message: JSON.stringify({ key: 'validation:min_length', values: { field: 'Nama Pelanggan', min: 3 } })
}),
email: z.string().email({
message: JSON.stringify({ key: 'validation:invalid_email' })
}),
orderType: z.enum(['BULK', 'RETAIL'], {
required_error: JSON.stringify({ key: 'validation:required', values: { field: 'Tipe Pesanan' } })
}),
quantity: z.number({
required_error: JSON.stringify({ key: 'validation:required', values: { field: 'Jumlah Roll' } })
}).min(5, {
message: JSON.stringify({ key: 'validation:min_length', values: { field: 'Jumlah Roll', min: 5 } })
}),
fabricColor: z.string().min(1, {
message: JSON.stringify({ key: 'validation:required', values: { field: 'Kode Warna Kain' } })
}),
priority: z.string(),
termsAccepted: z.literal(true, {
errorMap: () => ({ message: JSON.stringify({ key: 'validation:required', values: { field: 'Persetujuan Syarat & Ketentuan' } }) })
})
});
type ErpOrderPayload = z.infer<typeof erpOrderSchema>;
export default function FormShowcase() {
const [submittedData, setSubmittedData] = useState<ErpOrderPayload | null>(null);
// ─── 2. INITIALIZE REACT HOOK FORM WITH ZOD RESOLVER ─────────────
const methods = useForm<ErpOrderPayload>({
resolver: zodResolver(erpOrderSchema),
defaultValues: {
customerName: '',
email: '',
orderType: 'BULK',
quantity: 5,
fabricColor: '#228be6',
priority: 'normal',
// @ts-ignore - literal true is required but we start with false
termsAccepted: false,
},
});
const onSubmit: SubmitHandler<ErpOrderPayload> = (data) => {
setSubmittedData(data);
};
return (
<Stack gap="md" style={{ maxWidth: 800, margin: '0 auto', width: '100%' }}>
<Paper withBorder p="xl" radius="md" bg="var(--mantine-color-body)">
<Title order={3} mb="xs">📦 RHF + Zod + i18n Enterprise Demo</Title>
<Text c="dimmed" size="sm" mb="xl">
This form demonstrates the integration of our generated Mantine UI wrappers, React Hook Form micro-subscriptions, and Zod validation using JSON i18n payloads.
</Text>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit as any)}>
<Stack gap="md">
<Group grow align="flex-start">
<FieldTextInput
name="customerName"
label="Nama Pelanggan"
placeholder="Masukkan nama perusahaan atau perorangan"
withAsterisk
/>
<FieldTextInput
name="email"
label="Email Korespondensi"
placeholder="billing@company.com"
withAsterisk
/>
</Group>
<FieldSegmentedControl
name="priority"
label="Prioritas Produksi"
data={[
{ label: 'Normal', value: 'normal' },
{ label: 'Urgent', value: 'urgent' },
{ label: 'Critical', value: 'critical' }
]}
/>
<Group grow align="flex-start">
<FieldSelect
name="orderType"
label="Tipe Pesanan"
data={[
{ value: 'BULK', label: 'Grosir (Bulk)' },
{ value: 'RETAIL', label: 'Eceran (Retail)' }
]}
withAsterisk
/>
<FieldNumberInput
name="quantity"
label="Jumlah Produksi (Roll)"
placeholder="Minimal 5 roll"
withAsterisk
/>
</Group>
<FieldColorInput
name="fabricColor"
label="Spesifikasi Warna Bahan"
placeholder="Pilih atau masukkan kode HEX warna"
withAsterisk
/>
<FieldCheckbox
name="termsAccepted"
label="Saya menyetujui syarat & ketentuan produksi"
mt="md"
/>
<Group justify="flex-end" mt="xl">
<Button
type="button"
variant="subtle"
color="gray"
onClick={() => {
methods.reset();
setSubmittedData(null);
}}
>
Reset Form
</Button>
<Button type="submit" color="brand">
Submit Payload
</Button>
</Group>
</Stack>
</form>
</FormProvider>
</Paper>
{/* Output Panel untuk pembuktian Payload akhir */}
{submittedData && (
<Paper withBorder p="md" radius="md" bg="dark.8">
<Title order={5} c="green.4" mb="xs"> Validated Payload Output (PATCH/POST Ready):</Title>
<Code block color="dark" style={{ fontSize: '13px' }}>
{JSON.stringify(submittedData, null, 2)}
</Code>
</Paper>
)}
</Stack>
);
}
+2 -2
View File
@@ -27,7 +27,7 @@ import PrinterList from './printer-list';
import ExamplePage from './example/example.page';
import EventsDemoPage from './events-demo';
import PouchSample from './pouch-sample';
import FormShowcase from './example/features/form-showcase';
import FormDemoView from './example/features/form-demo';
interface ShowcaseViewProps {
colorScheme: ColorSchemeType;
@@ -286,7 +286,7 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
{/* --- FORMS TAB --- */}
{activeTab === 'forms' && (
<Stack gap="xl">
<FormShowcase />
<FormDemoView />
</Stack>
)}