@cronosstudio/crono-form
v2.1.1
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 + 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, regex, custom rules) | | 🎨 Integrasi MUI | UI modern dan konsisten dengan Material-UI components | | 🔄 Array Field & Nested | Dukungan lengkap untuk field array (FieldArray) dan nested form | | 🛠️ Custom Component | Override komponen field sesuai kebutuhan dengan component mapping | | ⚡ Ref-based Submit | Submit form via ref, cocok untuk dialog/modal/wizard | | 📱 Responsive | Grid layout otomatis dan responsive design | | 🌐 TypeScript | Full TypeScript support dengan strict typing |
📦 Instalasi
# npm
npm install @cronosstudio/crono-form
# yarn
yarn add @cronosstudio/crono-form
# pnpm
pnpm add @cronosstudio/crono-formPeer Dependencies
npm install @mui/material @emotion/react @emotion/styled formik yup🎯 Quick Start
import React from 'react';
import { CronoForm, FieldConfig } from '@cronosstudio/crono-form';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme();
const fieldConfig: FieldConfig[] = [
{
name: 'firstName',
label: 'First Name',
type: 'text',
required: true,
gridProps: { xs: 12, sm: 6 },
},
{
name: 'lastName',
label: 'Last Name',
type: 'text',
required: true,
gridProps: { xs: 12, sm: 6 },
},
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
gridProps: { xs: 12 },
},
{
name: 'age',
label: 'Age',
type: 'number',
min: 18,
max: 100,
gridProps: { xs: 12, sm: 4 },
},
{
name: 'newsletter',
label: 'Subscribe to Newsletter',
type: 'checkbox',
gridProps: { xs: 12, sm: 8 },
},
];
const initialValues = {
firstName: '',
lastName: '',
email: '',
age: '',
newsletter: false,
};
function MyForm() {
const handleSubmit = (values: any) => {
console.log('Form submitted:', values);
};
return (
<ThemeProvider theme={theme}>
<CronoForm
config={{
fields: fieldConfig,
initialValues,
}}
onSubmit={handleSubmit}
submitButtonText="Submit Form"
resetButtonText="Reset"
showResetButton
/>
</ThemeProvider>
);
}
export default MyForm;🔧 API Reference
CronoForm Props
| Prop | Type | Default | Description |
| ------------------ | ----------------------------- | --------------------- | ---------------------------------------------------------- |
| config | FormConfig | required | Konfigurasi form (fields, initialValues, validationSchema) |
| onSubmit | (values: any) => void | required | Callback function saat form di-submit |
| submitButtonText | string | "Submit" | Text untuk submit button |
| resetButtonText | string | "Reset" | Text untuk reset button |
| showResetButton | boolean | false | Tampilkan reset button |
| showSubmitButton | boolean | true | Tampilkan submit button |
| components | ComponentMap | defaultComponentMap | Custom component mapping |
| formRef | React.Ref<FormikProps<any>> | undefined | Ref untuk akses form instance |
| containerProps | ContainerProps | {} | Props untuk MUI Container |
| gridProps | GridProps | {} | Props untuk MUI Grid container |
FieldConfig Interface
interface FieldConfig {
name: string; // Field name (harus unique)
label?: string; // Label yang ditampilkan
type: FieldType; // Tipe field
required?: boolean; // Apakah field required
disabled?: boolean; // Apakah field disabled
placeholder?: string; // Placeholder text
helperText?: string; // Helper text di bawah field
hidden?: boolean | ((values: Record<string, any>) => boolean); // Conditional hide field
// Validation
min?: number; // Minimum value (untuk number/date)
max?: number; // Maximum value (untuk number/date)
minLength?: number; // Minimum string length
maxLength?: number; // Maximum string length
pattern?: string; // Regex pattern
customValidation?: (value: any) => string | undefined;
// Options (untuk select/radio)
options?: Array<{
value: any;
label: string;
disabled?: boolean;
}>;
// Layout
gridProps?: GridProps; // MUI Grid item props
// Field-specific props
multiline?: boolean; // Textarea (untuk text type)
rows?: number; // Rows untuk textarea
multiple?: boolean; // Multiple select
// Custom props yang akan di-pass ke component
[key: string]: any;
}Supported Field Types
| Type | Component | Description |
| ------------- | ---------- | --------------------------- |
| text | TextField | Text input field |
| email | TextField | Email input dengan validasi |
| password | TextField | Password input |
| number | TextField | Number input |
| tel | TextField | Telephone input |
| url | TextField | URL input |
| textarea | TextField | Multi-line text area |
| date | TextField | Date picker |
| datetime | TextField | Date time picker |
| time | TextField | Time picker |
| select | Select | Dropdown select |
| multiselect | Select | Multiple select |
| checkbox | Checkbox | Single checkbox |
| radio | RadioGroup | Radio button group |
| switch | Switch | Toggle switch |
📚 Examples
1. Form dengan Validasi Custom
import * as Yup from 'yup';
const fieldConfig: FieldConfig[] = [
{
name: 'username',
label: 'Username',
type: 'text',
required: true,
minLength: 3,
maxLength: 20,
pattern: '^[a-zA-Z0-9_]+$',
helperText: 'Only letters, numbers, and underscores allowed',
},
{
name: 'password',
label: 'Password',
type: 'password',
required: true,
customValidation: (value: string) => {
if (value && value.length < 8) {
return 'Password must be at least 8 characters';
}
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return 'Password must contain uppercase, lowercase, and number';
}
},
},
{
name: 'confirmPassword',
label: 'Confirm Password',
type: 'password',
required: true,
},
];
// Custom validation schema
const validationSchema = Yup.object({
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Please confirm your password'),
});
<CronoForm
config={{
fields: fieldConfig,
initialValues: { username: '', password: '', confirmPassword: '' },
validationSchema,
}}
onSubmit={handleSubmit}
/>;2. Form dengan Select dan Options
const fieldConfig: 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' },
],
gridProps: { xs: 12, sm: 6 },
},
{
name: 'skills',
label: 'Skills',
type: 'multiselect',
options: [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'react', label: 'React' },
{ value: 'node', label: 'Node.js' },
{ value: 'python', label: 'Python' },
],
gridProps: { 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)' },
],
gridProps: { xs: 12 },
},
];3. Form dengan Conditional Hidden Fields
const fieldConfig: FieldConfig[] = [
{
name: 'accountType',
label: 'Account Type',
type: 'select',
required: true,
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
],
gridProps: { xs: 12 },
},
{
name: 'companyName',
label: 'Company Name',
type: 'text',
required: true,
// Field hanya muncul jika accountType adalah 'business'
hidden: (values) => values.accountType !== 'business',
gridProps: { xs: 12 },
},
{
name: 'taxId',
label: 'Tax ID',
type: 'text',
// Field disembunyikan secara statis
hidden: true,
gridProps: { xs: 12 },
},
];4. Form dengan Array Fields
import { FieldArray } from 'formik';
const fieldConfig: FieldConfig[] = [
{
name: 'contacts',
label: 'Contact Information',
type: 'array',
fields: [
{
name: 'name',
label: 'Name',
type: 'text',
required: true,
gridProps: { xs: 12, sm: 6 },
},
{
name: 'phone',
label: 'Phone',
type: 'tel',
required: true,
gridProps: { xs: 12, sm: 6 },
},
],
},
];
const initialValues = {
contacts: [{ name: '', phone: '' }],
};4. Custom Component Override
import { TextField, Rating } from '@mui/material';
// Custom rating component
const RatingField = ({ field, form, ...props }: any) => {
return (
<Rating
{...field}
{...props}
onChange={(event, value) => {
form.setFieldValue(field.name, value);
}}
/>
);
};
// Custom component map
const customComponents = {
rating: RatingField,
// Override default text component
text: (props: any) => (
<TextField {...props} variant="filled" sx={{ '& .MuiFilledInput-root': { borderRadius: 2 } }} />
),
};
<CronoForm
config={{ fields: fieldConfig, initialValues }}
components={customComponents}
onSubmit={handleSubmit}
/>;5. Form dengan Ref untuk External Control
import { useRef } from 'react';
import { FormikProps } from 'formik';
function MyComponent() {
const formRef = useRef<FormikProps<any>>(null);
const handleExternalSubmit = () => {
if (formRef.current) {
formRef.current.submitForm();
}
};
const handleExternalReset = () => {
if (formRef.current) {
formRef.current.resetForm();
}
};
const handleValidate = () => {
if (formRef.current) {
formRef.current.validateForm().then((errors) => {
console.log('Validation errors:', errors);
});
}
};
return (
<div>
<CronoForm
config={{ fields: fieldConfig, initialValues }}
formRef={formRef}
showSubmitButton={false} // Hide default submit button
onSubmit={handleSubmit}
/>
<div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
<Button onClick={handleExternalSubmit} variant="contained">
Custom Submit
</Button>
<Button onClick={handleExternalReset} variant="outlined">
Custom Reset
</Button>
<Button onClick={handleValidate} variant="text">
Validate
</Button>
</div>
</div>
);
}🎨 Styling & Theming
CronoForm menggunakan MUI theming system. Anda bisa 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 {...props} />
</ThemeProvider>;2. CSS-in-JS dengan sx prop
const fieldConfig: FieldConfig[] = [
{
name: 'email',
label: 'Email',
type: 'email',
sx: {
'& .MuiOutlinedInput-root': {
borderRadius: 2,
'&:hover': {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'primary.main',
},
},
},
},
},
];3. Container & Grid Styling
<CronoForm
config={{ fields: fieldConfig, initialValues }}
containerProps={{
maxWidth: 'md',
sx: { mt: 4, mb: 4 },
}}
gridProps={{
spacing: 3,
sx: { padding: 2 },
}}
onSubmit={handleSubmit}
/>🔧 Advanced Usage
1. Conditional Fields
const ConditionalForm = () => {
const [formValues, setFormValues] = useState({});
const fieldConfig: FieldConfig[] = [
{
name: 'hasLicense',
label: 'Do you have a driving license?',
type: 'checkbox',
},
// Conditional field
...(formValues.hasLicense
? [
{
name: 'licenseNumber',
label: 'License Number',
type: 'text',
required: true,
},
]
: []),
];
return (
<CronoForm
config={{ fields: fieldConfig, initialValues }}
onSubmit={handleSubmit}
onValuesChange={setFormValues} // Track form values
/>
);
};2. Multi-Step Form
const MultiStepForm = () => {
const [currentStep, setCurrentStep] = useState(0);
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: 'tel', required: true },
],
},
];
const handleNext = () => {
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
}
};
const handlePrev = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
return (
<div>
<Stepper activeStep={currentStep}>
{steps.map((step, index) => (
<Step key={index}>
<StepLabel>{step.title}</StepLabel>
</Step>
))}
</Stepper>
<CronoForm
config={{
fields: steps[currentStep].fields,
initialValues,
}}
showSubmitButton={currentStep === steps.length - 1}
submitButtonText="Complete"
onSubmit={handleSubmit}
/>
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
<Button disabled={currentStep === 0} onClick={handlePrev}>
Previous
</Button>
<Button disabled={currentStep === steps.length - 1} onClick={handleNext}>
Next
</Button>
</Box>
</div>
);
};🧪 Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CronoForm } from '@cronosstudio/crono-form';
describe('CronoForm', () => {
const mockSubmit = jest.fn();
const fieldConfig = [
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
];
const initialValues = { name: '', email: '' };
beforeEach(() => {
mockSubmit.mockClear();
});
it('renders form fields correctly', () => {
render(<CronoForm config={{ fields: fieldConfig, initialValues }} onSubmit={mockSubmit} />);
expect(screen.getByLabelText('Name')).toBeInTheDocument();
expect(screen.getByLabelText('Email')).toBeInTheDocument();
});
it('validates required fields', async () => {
render(<CronoForm config={{ fields: fieldConfig, initialValues }} onSubmit={mockSubmit} />);
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeInTheDocument();
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
expect(mockSubmit).not.toHaveBeenCalled();
});
it('submits form with valid data', async () => {
render(<CronoForm config={{ fields: fieldConfig, initialValues }} onSubmit={mockSubmit} />);
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'John Doe' },
});
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: '[email protected]' },
});
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: '[email protected]',
});
});
});
});📄 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 - Optimized rendering dan minimal re-renders
- Flexibility - Mudah dikustomisasi tanpa merusak struktur dasar
- Accessibility - ARIA labels dan keyboard navigation built-in
📦 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
