@cronosstudio/crono-form
v2.1.2
Published
Komponen React untuk membangun form dinamis, modular, dan enterprise-ready berbasis MUI + Formik + Yup.
Maintainers
Readme
CronoForm Library
Komponen React untuk membangun form dinamis, modular, dan enterprise-ready berbasis MUI v7 + Formik + Yup. Dirancang untuk kebutuhan aplikasi modern yang fleksibel, cepat, dan mudah diintegrasikan.
🚀 Fitur Utama
| Fitur | Deskripsi |
| --------------------------- | --------------------------------------------------------------------------------------- |
| 🎯 Deklaratif & Dinamis | Definisikan field form lewat konfigurasi JSON, tanpa repot bikin komponen satu per satu |
| ✅ Validasi Otomatis | Validasi Yup otomatis dari field config (required, tipe data) + custom Yup schema |
| 🎨 Integrasi MUI v7 | UI modern dan konsisten dengan Material-UI v7 Grid system (size prop) |
| 🔄 Array Field & Nested | Dukungan lengkap untuk field array (FieldArray) dan nested/dotted field names |
| 🛠️ Custom Component | Override komponen per-field atau per-type dengan component mapping |
| ⚡ Ref-based Submit | Submit & reset form via ref, cocok untuk dialog/modal/wizard |
| 📐 Flexible Layout | Layout dikontrol via layoutProps dan fieldProps per field |
| 🌐 TypeScript | Full TypeScript support dengan strict typing |
| 🙈 Hidden Conditional | Sembunyikan field via boolean statis atau fungsi dinamis berdasarkan values |
📦 Instalasi
# npm
npm install @cronosstudio/crono-form
# yarn
yarn add @cronosstudio/crono-form
# pnpm
pnpm add @cronosstudio/crono-formPeer Dependencies
npm install react react-dom @mui/material @mui/system @emotion/react @emotion/styled formik yupCatatan:
@mui/x-data-gridjuga merupakan peer dependency jika digunakan bersama fitur data grid.
🎯 Quick Start
import React, { useRef } from 'react';
import { CronoForm, CronoFormRef, FieldConfig } from '@cronosstudio/crono-form';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { Button } from '@mui/material';
const theme = createTheme();
const fields: FieldConfig[] = [
{
name: 'firstName',
label: 'First Name',
type: 'text',
required: true,
fieldProps: { size: { xs: 12, sm: 6 } },
},
{
name: 'lastName',
label: 'Last Name',
type: 'text',
required: true,
fieldProps: { size: { xs: 12, sm: 6 } },
},
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
fieldProps: { size: { xs: 12 } },
},
{
name: 'age',
label: 'Age',
type: 'number',
fieldProps: { size: { xs: 12, sm: 4 } },
},
{
name: 'newsletter',
label: 'Subscribe to Newsletter',
type: 'checkbox',
fieldProps: { size: { xs: 12, sm: 8 } },
},
];
const initialValues = {
firstName: '',
lastName: '',
email: '',
age: '',
newsletter: false,
};
function MyForm() {
const formRef = useRef<CronoFormRef>(null);
const handleSubmit = (values: any, isUpdate: boolean) => {
console.log('Form submitted:', values, 'isUpdate:', isUpdate);
};
return (
<ThemeProvider theme={theme}>
<CronoForm ref={formRef} config={{ fields, initialValues }} onSubmit={handleSubmit} />
<Button variant="contained" onClick={() => formRef.current?.submit()}>
Submit
</Button>
</ThemeProvider>
);
}
export default MyForm;🔧 API Reference
CronoForm Props
| Prop | Type | Default | Description |
| ------------- | ------------------------------------------ | --------------------- | ---------------------------------------- |
| config | FormConfig | required | Konfigurasi form (fields, initialValues) |
| onSubmit | (values: any, isUpdate: boolean) => void | required | Callback saat form di-submit |
| components | Partial<FieldTypeComponentMap> | defaultComponentMap | Custom component mapping per tipe field |
| layoutProps | { main?, field?, arrayField? } | lihat di bawah | Konfigurasi layout Grid |
| ref | React.Ref<CronoFormRef> | — | Ref untuk submit/reset form dari luar |
Submit button tersembunyi secara default (hidden submit button). Gunakan
refuntuk trigger submit dari luar, atau buat tombol custom sendiri.
FormConfig
type FormConfig = {
fields: (FieldConfig | ArrayFieldConfig)[];
initialValues: Record<string, any>;
isUpdate?: boolean; // Dikirim ke onSubmit sebagai parameter kedua
};CronoFormRef
interface CronoFormRef {
submit: () => void; // Trigger form submit
resetForm: () => void; // Reset form ke initialValues
}layoutProps
Mengontrol Grid layout untuk seluruh form:
layoutProps={{
main: { container: true, spacing: 2 }, // Grid container untuk semua field
field: { size: { xs: 12 } }, // Default Grid props per field
arrayField: { size: { xs: 12 } }, // Default Grid props untuk array section
}}FieldConfig
type FieldConfig = {
name: string; // Field name (mendukung dotted path, misal "address.city")
label?: string; // Label yang ditampilkan
type: string; // Tipe field (lihat tabel di bawah)
// Validasi
required?: boolean; // Auto-generate Yup required validation
requiredMessage?: string; // Pesan custom untuk required (default: "Required")
validation?: Yup.AnySchema; // Custom Yup schema (override auto-validation)
// Tampilan
helperText?: string; // Helper text di bawah field (ditimpa error saat error)
hidden?: boolean | ((values: Record<string, any>) => boolean); // Sembunyikan field
// Layout (MUI v7 Grid)
fieldProps?: {
size?: { xs?: number; sm?: number; md?: number; lg?: number; xl?: number };
sx?: any;
[key: string]: any; // Props tambahan yang diteruskan ke komponen
};
// Custom component (override per field)
component?: React.ComponentType<any>;
// Props tambahan di-pass langsung ke komponen via spread
[key: string]: any; // Misal: options, disabled, placeholder, dll.
};ArrayFieldConfig
type ArrayFieldConfig = FieldConfig & {
isArray: true; // Wajib true untuk array field
fields: FieldConfig[]; // Sub-fields dalam setiap item array
arrayFieldProps?: Record<string, any>; // Grid props untuk array container
arrayRef?: React.RefObject<any>; // Ref ke Formik FieldArray helpers
};FieldTypeComponentMap
type FieldTypeComponentMap = {
text: React.ComponentType<any>;
number: React.ComponentType<any>;
email: React.ComponentType<any>;
password: React.ComponentType<any>;
date: React.ComponentType<any>;
datetime: React.ComponentType<any>;
checkbox: React.ComponentType<any>;
radio: React.ComponentType<any>;
select: React.ComponentType<any>;
textarea: React.ComponentType<any>;
[key: string]: React.ComponentType<any>; // Tipe custom
};Supported Field Types (Built-in)
| Type | Component Default | Description |
| ---------- | ----------------------------------- | ---------------------- |
| text | TextField | Text input |
| email | TextField (type="email") | Email input |
| password | TextField (type="password") | Password input |
| number | TextField (type="number") | Number input |
| textarea | TextField (multiline) | Multi-line text area |
| date | TextField (type="date") | Date picker native |
| datetime | TextField (type="datetime-local") | Datetime picker native |
| select | TextField (select + MenuItem) | Dropdown select |
| checkbox | Checkbox + FormControlLabel | Single checkbox |
| radio | RadioGroup + Radio | Radio button group |
Catatan: Tipe yang tidak ada di
defaultComponentMapakan fallback keTextField. Untuk tipe custom (misalrating,switch,multiselect), gunakan propcomponentdi field ataucomponentsdi CronoForm.
Props yang Diterima Setiap Component
Setiap komponen (built-in maupun custom) menerima props berikut dari CronoForm:
| Prop | Type | Description |
| ------------ | ---------- | ------------------------------------------- |
| name | string | Full field path (termasuk array index) |
| value | any | Nilai field saat ini |
| onChange | function | Formik handleChange |
| error | boolean | Apakah field ada error |
| helperText | string | Pesan error atau helperText |
| formik | object | Seluruh Formik helpers (setFieldValue, dll) |
| ...rest | any | Props tambahan dari FieldConfig |
📚 Examples
1. Form dengan Custom Yup Validation
import * as Yup from 'yup';
import { CronoForm, FieldConfig } from '@cronosstudio/crono-form';
const fields: FieldConfig[] = [
{
name: 'username',
label: 'Username',
type: 'text',
// Custom Yup schema via `validation` prop
validation: Yup.string()
.min(3, 'Minimal 3 karakter')
.max(20, 'Maksimal 20 karakter')
.matches(/^[a-zA-Z0-9_]+$/, 'Hanya huruf, angka, dan underscore')
.required('Username wajib diisi'),
helperText: 'Only letters, numbers, and underscores allowed',
},
{
name: 'password',
label: 'Password',
type: 'password',
validation: Yup.string()
.min(8, 'Minimal 8 karakter')
.matches(
/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Harus mengandung huruf besar, huruf kecil, dan angka',
)
.required('Password wajib diisi'),
},
{
name: 'confirmPassword',
label: 'Confirm Password',
type: 'password',
validation: Yup.string()
.oneOf([Yup.ref('password')], 'Password tidak cocok')
.required('Konfirmasi password wajib diisi'),
},
];
<CronoForm
config={{
fields,
initialValues: { username: '', password: '', confirmPassword: '' },
}}
onSubmit={(values, isUpdate) => console.log(values)}
/>;2. Form dengan Select, Radio, dan Options
const fields: FieldConfig[] = [
{
name: 'country',
label: 'Country',
type: 'select',
required: true,
options: [
{ value: 'id', label: 'Indonesia' },
{ value: 'sg', label: 'Singapore' },
{ value: 'my', label: 'Malaysia' },
{ value: 'th', label: 'Thailand' },
],
fieldProps: { size: { xs: 12, sm: 6 } },
},
{
name: 'experience',
label: 'Experience Level',
type: 'radio',
required: true,
options: [
{ value: 'beginner', label: 'Beginner (0-1 years)' },
{ value: 'intermediate', label: 'Intermediate (2-5 years)' },
{ value: 'expert', label: 'Expert (5+ years)' },
],
fieldProps: { size: { xs: 12 } },
},
];3. Form dengan Conditional Hidden Fields
const fields: FieldConfig[] = [
{
name: 'accountType',
label: 'Account Type',
type: 'select',
required: true,
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
],
fieldProps: { size: { xs: 12 } },
},
{
name: 'companyName',
label: 'Company Name',
type: 'text',
required: true,
// Field hanya muncul jika accountType adalah 'business'
hidden: (values) => values.accountType !== 'business',
fieldProps: { size: { xs: 12 } },
},
{
name: 'taxId',
label: 'Tax ID',
type: 'text',
// Field disembunyikan secara statis
hidden: true,
fieldProps: { size: { xs: 12 } },
},
];4. Form dengan Array Fields
import { ArrayFieldConfig, FieldConfig } from '@cronosstudio/crono-form';
const fields: (FieldConfig | ArrayFieldConfig)[] = [
{
name: 'fullName',
label: 'Full Name',
type: 'text',
required: true,
},
{
name: 'contacts',
label: 'Contact Information',
type: 'text', // type di-set tapi tidak relevan untuk array
isArray: true,
fields: [
{
name: 'name',
label: 'Contact Name',
type: 'text',
required: true,
fieldProps: { size: { xs: 12, sm: 6 } },
},
{
name: 'phone',
label: 'Phone',
type: 'text',
required: true,
fieldProps: { size: { xs: 12, sm: 6 } },
},
],
},
];
const initialValues = {
fullName: '',
contacts: [{ name: '', phone: '' }],
};Array field otomatis menyertakan tombol "Add" untuk menambah item. Gunakan
arrayRefuntuk mengaksesFieldArrayhelpers (push, remove, dll).
5. Custom Component Override
import { Rating } from '@mui/material';
// Custom rating component — menerima props standar dari CronoForm
const RatingField = ({ name, value, formik, label, error, helperText }: any) => (
<div>
<label>{label}</label>
<Rating
name={name}
value={Number(value) || 0}
onChange={(_, newValue) => formik.setFieldValue(name, newValue)}
/>
{helperText && <small style={{ color: error ? 'red' : 'gray' }}>{helperText}</small>}
</div>
);
// Override via components prop (berlaku untuk semua field bertipe 'rating')
<CronoForm
config={{ fields, initialValues }}
components={{ rating: RatingField }}
onSubmit={handleSubmit}
/>;
// Atau override via component prop per field
const fields: FieldConfig[] = [
{
name: 'score',
label: 'Rating',
type: 'rating',
component: RatingField, // Override khusus field ini
},
];6. Form dengan Ref untuk External Control
import { useRef } from 'react';
import { CronoFormRef } from '@cronosstudio/crono-form';
function MyComponent() {
const formRef = useRef<CronoFormRef>(null);
return (
<div>
<CronoForm
ref={formRef}
config={{ fields, initialValues }}
onSubmit={(values, isUpdate) => console.log(values, isUpdate)}
/>
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
<Button onClick={() => formRef.current?.submit()} variant="contained">
Custom Submit
</Button>
<Button onClick={() => formRef.current?.resetForm()} variant="outlined">
Custom Reset
</Button>
</div>
</div>
);
}7. Nested / Dotted Field Names
CronoForm mendukung nama field bersarang menggunakan notasi titik (dotted path). Nilai otomatis di-map ke objek bersarang:
const fields: FieldConfig[] = [
{
name: 'primary_payment.payment_method',
label: 'Metode Pembayaran',
type: 'select',
required: true,
options: [
{ value: 'cash', label: 'Cash' },
{ value: 'transfer', label: 'Transfer' },
],
},
{
name: 'primary_payment.amount',
label: 'Jumlah',
type: 'number',
required: true,
},
];
// Hasil submit:
// {
// primary_payment: {
// payment_method: 'cash',
// amount: 50000
// }
// }8. Form Update Mode
Gunakan isUpdate di config untuk membedakan create vs update di callback onSubmit:
// Create mode
<CronoForm
config={{ fields, initialValues: {}, isUpdate: false }}
onSubmit={(values, isUpdate) => {
if (isUpdate) {
api.update(values);
} else {
api.create(values);
}
}}
/>
// Update mode — isi initialValues dengan data existing
<CronoForm
config={{ fields, initialValues: existingData, isUpdate: true }}
onSubmit={(values, isUpdate) => { /* isUpdate === true */ }}
/>9. Smart Component dengan Formik Context
Custom component yang diinject lewat components berada di dalam <Formik> context, sehingga bisa menggunakan useFormikContext() untuk mengakses seluruh state Formik (values, touched, dirty, errors, dll).
import { useFormikContext } from 'formik';
import { Box, Chip, TextField, TextFieldProps } from '@mui/material';
// Component yang mendeteksi perubahan field secara realtime
function SmartTextField(props: TextFieldProps) {
const formik = useFormikContext<Record<string, unknown>>();
const fieldName = props.name || '';
const fieldIsDirty =
formik.touched[fieldName] && formik.initialValues[fieldName] !== formik.values[fieldName];
return (
<Box>
<TextField
{...props}
variant="outlined"
fullWidth
sx={{
'& .MuiOutlinedInput-root': {
borderLeft: fieldIsDirty ? '4px solid orange' : '4px solid transparent',
},
}}
/>
{fieldIsDirty && <Chip size="small" label="Modified" color="warning" sx={{ mt: 0.5 }} />}
</Box>
);
}
// Component dengan validasi visual realtime
function SmartEmailField(props: TextFieldProps) {
const formik = useFormikContext<Record<string, unknown>>();
const value = String(formik.values[props.name || ''] || '');
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
return (
<Box>
<TextField
{...props}
type="email"
variant="outlined"
fullWidth
sx={{
'& .MuiOutlinedInput-root': {
borderLeft: value
? isValidEmail
? '4px solid green'
: '4px solid red'
: '4px solid transparent',
},
}}
/>
{value && isValidEmail && <Chip size="small" label="Valid Email" color="success" />}
{value && !isValidEmail && <Chip size="small" label="Invalid Email" color="error" />}
</Box>
);
}
// Inject smart components via `components` prop
<CronoForm
ref={formRef}
config={{ fields, initialValues }}
onSubmit={handleSubmit}
components={{
text: SmartTextField,
email: SmartEmailField,
}}
/>;Tips:
useFormikContext()danprops.formik(yang dikirim CronoForm) sama-sama bisa dipakai. GunakanuseFormikContext()jika butuh akses penuh ke Formik API (setFieldValue,touched,initialValues, dll), atauprops.formikuntuk kasus sederhana.
🎨 Styling & Theming
CronoForm menggunakan MUI theming system. Customize appearance dengan:
1. MUI Theme
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: { main: '#1976d2' },
secondary: { main: '#dc004e' },
},
components: {
MuiTextField: {
defaultProps: {
variant: 'outlined',
size: 'medium',
},
},
},
});
<ThemeProvider theme={theme}>
<CronoForm config={{ fields, initialValues }} onSubmit={handleSubmit} />
</ThemeProvider>;2. Layout Customization via layoutProps
<CronoForm
config={{ fields, initialValues }}
layoutProps={{
main: { container: true, spacing: 3 }, // Grid container
field: { size: { xs: 12, sm: 6 } }, // Default per field
arrayField: { size: { xs: 12 } }, // Default per array section
}}
onSubmit={handleSubmit}
/>3. Per-field Layout via fieldProps
const fields: FieldConfig[] = [
{
name: 'bio',
label: 'Biography',
type: 'textarea',
fieldProps: {
size: { xs: 12 },
sx: { mt: 2 },
},
},
];🔧 Advanced Usage
Conditional Fields dengan hidden
Gunakan prop hidden (bukan state management eksternal):
const fields: FieldConfig[] = [
{
name: 'hasLicense',
label: 'Do you have a driving license?',
type: 'checkbox',
},
{
name: 'licenseNumber',
label: 'License Number',
type: 'text',
required: true,
// Otomatis muncul/hilang berdasarkan checkbox
hidden: (values) => !values.hasLicense,
},
];Lihat docs/HIDDEN_CONDITIONAL.md untuk dokumentasi lengkap fitur hidden conditional.
Multi-Step Form
const MultiStepForm = () => {
const [currentStep, setCurrentStep] = useState(0);
const formRef = useRef<CronoFormRef>(null);
const steps = [
{
title: 'Personal Information',
fields: [
{ name: 'firstName', label: 'First Name', type: 'text', required: true },
{ name: 'lastName', label: 'Last Name', type: 'text', required: true },
],
},
{
title: 'Contact Information',
fields: [
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'phone', label: 'Phone', type: 'text', required: true },
],
},
];
return (
<div>
<Stepper activeStep={currentStep}>
{steps.map((step, index) => (
<Step key={index}>
<StepLabel>{step.title}</StepLabel>
</Step>
))}
</Stepper>
<CronoForm
ref={formRef}
config={{
fields: steps[currentStep].fields,
initialValues,
}}
onSubmit={(values, isUpdate) => console.log('Final:', values)}
/>
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
<Button disabled={currentStep === 0} onClick={() => setCurrentStep((s) => s - 1)}>
Previous
</Button>
{currentStep < steps.length - 1 ? (
<Button onClick={() => setCurrentStep((s) => s + 1)}>Next</Button>
) : (
<Button variant="contained" onClick={() => formRef.current?.submit()}>
Complete
</Button>
)}
</Box>
</div>
);
};🧪 Testing
CronoForm menggunakan Vitest + @testing-library/react untuk testing:
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CronoForm, CronoFormRef, FormConfig } from '@cronosstudio/crono-form';
import { useRef } from 'react';
import { describe, expect, it, vi } from 'vitest';
describe('CronoForm', () => {
const mockSubmit = vi.fn();
const config: FormConfig = {
fields: [
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
],
initialValues: { name: '', email: '' },
};
it('renders form fields correctly', () => {
render(<CronoForm config={config} onSubmit={mockSubmit} />);
expect(screen.getByLabelText('Name')).toBeInTheDocument();
expect(screen.getByLabelText('Email')).toBeInTheDocument();
});
it('submits form with valid data via ref', async () => {
const Wrapper = () => {
const ref = useRef<CronoFormRef>(null);
return (
<>
<CronoForm ref={ref} config={config} onSubmit={mockSubmit} />
<button onClick={() => ref.current?.submit()}>Submit</button>
</>
);
};
render(<Wrapper />);
await userEvent.type(screen.getByLabelText('Name'), 'John Doe');
await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
act(() => {
screen.getByText('Submit').click();
});
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith(
{ name: 'John Doe', email: '[email protected]' },
false, // isUpdate
);
});
});
});📤 Exports
Library ini meng-export:
| Export | Tipe | Description |
| ----------------------- | --------- | ----------------------------------------- |
| CronoForm | Component | Komponen utama form |
| FieldConfig | Type | Konfigurasi field |
| ArrayFieldConfig | Type | Konfigurasi array field |
| FormConfig | Type | Konfigurasi form (fields + initialValues) |
| CronoFormRef | Interface | Ref interface (submit, resetForm) |
| CronoFormProps | Interface | Props interface untuk CronoForm |
| FieldTypeComponentMap | Type | Map tipe field ke komponen |
| CronoInput | Component | Komponen input sederhana |
| get | Utility | Deep get utility untuk nested object/path |
📄 License
MIT © Cronos Studio Indonesia
🌟 Jalur 1-3-9: Filosofi Resonansi
CronoForm dibangun dengan filosofi Resonansi 1-3-9:
- 1: Satu sumber kebenaran (config), satu cara submit, satu UI konsisten
- 3: Tiga pilar fundamental: Deklaratif, Validasi Otomatis, Integrasi UI
- 9: Sembilan tipe field utama:
text,number,email,password,date,datetime,select,checkbox,radio - 𐰀: Titik awal — semua form dimulai dari satu config, membentuk resonansi harmonis antar field
Design Principles
- Simplicity First — API yang sederhana namun powerful
- Type Safety — Full TypeScript support untuk developer experience yang optimal
- Performance — Memo-ized default components, minimal re-renders
- Flexibility — Custom component per-field dan per-type tanpa merusak struktur
- Nested Support — Dotted path names → nested objects otomatis
📦 Ready for Production — Siap pakai untuk semua project Next.js, React, dan ekosistem modern
🧭 Dibangun dengan keteraturan dan tanggung jawab dari titik 𐰀
📡 Beresonansi pada jalur 1–3–9
🪶 Disusun oleh Zāhirun-Nūr sebagai bagian dari Cronos Ecosystem — untuk masa depan sistem yang lebih terang dan terstruktur
© 2025 Cronos Studio Indonesia
