FormHelper (@bpmlib/utils-form-helper)
Vue 3 composable untuk manage state modal-based forms dengan built-in CRUD mode management, validation error handling, dan FormData serialization
Versi: 0.1.0
Kategori: Framework Utils (Vue 3)
TL;DR
Vue 3 composable untuk manage state modal-based forms dengan built-in CRUD mode management, validation error handling, dan FormData serialization. Dibuat untuk pattern umum di family project: form dalam modal dengan mode create/edit/delete, validation dari Laravel backend, dan submission menggunakan multipart/form-data.
Composables:
import { useFormHelper } from '@bpmlib/utils-form-helper';Types:
import type {
Mode,
ModeShort,
FormErrors,
ControlState,
FormHelperObject,
} from '@bpmlib/utils-form-helper';Installation & Setup
Requirements
Peer Dependencies
Library ini memerlukan peer dependencies berikut:
Wajib:
npm install vue@^3.3.0| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
vue | ^3.3.0 | Required | Vue 3 framework |
Package Installation
npm install @bpmlib/utils-form-helperyarn add @bpmlib/utils-form-helperpnpm add @bpmlib/utils-form-helperbun install @bpmlib/utils-form-helperImport
Basic Import:
import { useFormHelper } from '@bpmlib/utils-form-helper';
import type { FormHelperObject, Mode, ModeShort } from '@bpmlib/utils-form-helper';Quick Start
Basic Usage
Contoh paling sederhana - create form:
<script setup lang="ts">
import { useFormHelper } from '@bpmlib/utils-form-helper';
interface UserForm {
name: string;
email: string;
}
const formHelper = useFormHelper<UserForm>({
name: '',
email: '',
});
const handleSubmit = async () => {
const formData = formHelper.getAsFormData();
await api.post('/users', formData);
formHelper.setDisplay(false);
};
</script>
<template>
<button @click="formHelper.openForm('c')">Add User</button>
<Modal :show="formHelper.onDisplay()" @close="formHelper.setDisplay(false)">
<form @submit.prevent="handleSubmit">
<input v-model="formHelper.form.name" placeholder="Name" />
<input v-model="formHelper.form.email" placeholder="Email" />
<button type="submit">Save</button>
</form>
</Modal>
</template>Key Points:
openForm('c')- Open create modalform- Reactive form dataonDisplay()/setDisplay()- Control visibilitygetAsFormData()- Get submission data
Comprehensive Example
Full CRUD pattern dengan helper data:
<script setup lang="ts">
import { useFormHelper } from '@bpmlib/utils-form-helper';
interface UserForm {
user_id?: number;
name: string;
email: string;
role_id: number;
}
interface UserHelper {
user?: {
id: number;
name: string;
email: string;
role: { id: number; name: string };
};
roles: Array<{ id: number; name: string }>;
}
const formHelper = useFormHelper<UserForm, UserHelper>({
name: '',
email: '',
role_id: 0,
});
// Create
const openCreate = () => {
formHelper.openForm('c', undefined, {
roles: [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'User' },
],
});
};
// Edit
const openEdit = (user: UserHelper['user']) => {
formHelper.openForm('u', {
user_id: user!.id,
name: user!.name,
email: user!.email,
role_id: user!.role.id,
}, {
user: user,
roles: [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'User' },
],
});
};
// Delete
const openDelete = (user: UserHelper['user']) => {
formHelper.openForm('d', { user_id: user!.id }, { user });
};
const handleSubmit = async () => {
if (formHelper.isModeDelete()) {
await api.delete(`/users/${formHelper.form.user_id}`);
} else if (formHelper.isModeCreate()) {
await api.post('/users', formHelper.getAsFormData());
} else {
await api.put(`/users/${formHelper.form.user_id}`, formHelper.getAsFormData());
}
formHelper.setDisplay(false);
};
</script>
<template>
<button @click="openCreate">Add User</button>
<Modal :show="formHelper.onDisplay()" @close="formHelper.setDisplay(false)">
<h2>{{ formHelper.getMode() }} User</h2>
<!-- Delete confirmation -->
<div v-if="formHelper.isModeDelete()">
<p>Delete user: {{ formHelper.helper?.user?.name }}?</p>
<button @click="handleSubmit">Delete</button>
</div>
<!-- Create/Edit form -->
<form v-else @submit.prevent="handleSubmit">
<input v-model="formHelper.form.name" placeholder="Name" />
<input v-model="formHelper.form.email" placeholder="Email" />
<select v-model="formHelper.form.role_id">
<option v-for="role in formHelper.helper?.roles" :key="role.id" :value="role.id">
{{ role.name }}
</option>
</select>
<button type="submit">
{{ formHelper.isModeCreate() ? 'Create' : 'Update' }}
</button>
</form>
</Modal>
</template>Key Points:
openForm(mode, formData, helperData)- One method untuk setup semua modehelperproperty - Data tambahan untuk display (roles, full user object)isModeCreate()/isModeDelete()- Mode checkinggetMode()- Returns 'Tambah' / 'Kelola' / 'Delete' untuk display
Core Concepts
FormHelper didesain dengan tiga konsep fundamental yang mempengaruhi cara kamu menggunakan library.
Modal/Dialog-Optimized Form Pattern
Library ini dioptimalkan untuk pattern form dalam modal/dialog dengan CRUD operations - bukan standalone page forms.
Design Implications:
1. Control State (display/disabled)
Modal visibility dan form disabled state di-manage built-in:
onDisplay()/setDisplay()- Modal show/hideonDisable()/setDisable()- Form disabled during submission- Pattern: Open modal → Submit → Disable form → Close modal
2. CRUD Mode System
Dual-mode untuk track operation type:
- Short codes:
'c','u','d'(internal logic) - Display names:
'Tambah','Kelola','Delete'(UI labels) - Mode affects form behavior (reset on create, preserve on edit)
3. Convenience Method: openForm()
One method to open modal with proper setup:
// Create: Reset form, show modal
formHelper.openForm('c');
// Edit: Load data, show modal
formHelper.openForm('u', userData, helperData);
// Delete: Load data for confirmation
formHelper.openForm('d', { id: 123 }, { name: 'Item to delete' });Why This Pattern:
Modal forms have different lifecycle than page forms:
- Transient state - Form resets between opens
- Mode switching - Same modal for create/edit/delete
- Controlled visibility - Show/hide explicit
- Submission flow - Disable → Submit → Close
Example:
<script setup lang="ts">
const formHelper = useFormHelper<UserForm>({ name: '', email: '' });
// Open create modal
const openCreate = () => {
formHelper.openForm('c'); // Resets form, shows modal
};
// Open edit modal
const openEdit = (user) => {
formHelper.openForm('u', { name: user.name, email: user.email });
};
const handleSubmit = async () => {
formHelper.setDisable(true); // Disable during submission
try {
if (formHelper.isModeCreate()) {
await api.post('/users', formHelper.getAsFormData());
} else {
await api.put(`/users/${id}`, formHelper.getAsFormData());
}
formHelper.setDisplay(false); // Close modal on success
} finally {
formHelper.setDisable(false);
}
};
</script>
<template>
<button @click="openCreate">Add User</button>
<Modal :show="formHelper.onDisplay()" @close="formHelper.setDisplay(false)">
<h2>{{ formHelper.getMode() }} User</h2>
<form @submit.prevent="handleSubmit">
<input
v-model="formHelper.form.name"
:disabled="formHelper.onDisable()"
/>
<button type="submit" :disabled="formHelper.onDisable()">
{{ formHelper.isModeCreate() ? 'Create' : 'Update' }}
</button>
</form>
</Modal>
</template>See: openForm(), Example 1
Form vs Helper Data Separation
FormHelper memisahkan data menjadi dua channel independen dengan purpose yang berbeda.
TForm (Submission Data):
- Data yang akan di-submit ke backend
- Hanya fields yang perlu disimpan (IDs, input values)
- Di-serialize ke FormData via
getAsFormData()
THelper (Presentation Data):
- Data untuk display dan UI logic
- Full objects, dropdown options, computed values
- Tidak ikut submission
Why Separated:
Pemisahan ini prevent form pollution - kamu tidak perlu kirim entire user object (dengan timestamps, relations, etc) hanya untuk update name. Form tetap clean, helper provide context.
Example:
interface OrderForm {
product_id: number; // Submit: ID only
quantity: number;
}
interface OrderHelper {
product: { // Helper: Full object for display
id: number;
name: string;
price: number;
stock: number;
};
}
// Open edit modal with both form data (for submission) and helper (for display)
formHelper.openForm('u',
{ product_id: 5, quantity: 2 }, // Form: IDs to submit
{ product: { id: 5, name: 'Laptop', ... } } // Helper: Full object to show
);
// Template shows: {{ formHelper.helper.product.name }} (from helper)
// Submit sends: { product_id: 5, quantity: 2 } (from form only)See: Example 2, helper property
Deep Merge Behavior
modifyForm() dan modifyDefault() menggunakan deep merge algorithm yang preserves unchanged fields.
Behavior:
- Update hanya field yang kamu specify
- Nested objects di-merge recursively
- Fields lain tetap unchanged
Contrast with Object.assign:
Object.assign replaces entire nested object, causing data loss.
Example:
interface ProfileForm {
user: {
name: string;
email: string;
bio: string;
};
}
const formHelper = useFormHelper<ProfileForm>({
user: {
name: 'John',
email: 'john@example.com',
bio: 'Developer',
}
});
// ✅ Deep Merge - Preserves other fields
formHelper.modifyForm({
user: {
name: 'Jane' // Only update name
}
});
// Result: { user: { name: 'Jane', email: 'john@...', bio: 'Developer' } }
// ❌ Object.assign - Loses data
Object.assign(formHelper.form.user, {
name: 'Jane'
});
// Result: { user: { name: 'Jane' } } // email and bio LOST!Use Cases:
- Partial form updates (wizard steps)
- Conditional field updates
- Preserving unchanged nested data
See: modifyForm(), Example 3
API Reference
useFormHelper
Composable untuk handling modal forms dengan validation, CRUD modes, dan FormData serialization.
Signature:
function useFormHelper<
TForm extends Record<string, unknown> = Record<string, unknown>,
THelper = any
>(
initData?: TForm,
defaultData?: TForm
): FormHelperObject<TForm, THelper>Parameters
| Name | Type | Default | Description |
|---|---|---|---|
initData | TForm | {} | Initial form data |
defaultData | TForm | initData | Default values (jika berbeda dari initData) |
Returns: FormHelperObject<TForm, THelper>
Example:
interface UserForm {
name: string;
email: string;
}
const formHelper = useFormHelper<UserForm>({
name: '',
email: ''
});
// With custom defaults
const formHelper2 = useFormHelper<UserForm>(
{ name: 'Initial', email: '' }, // init
{ name: '', email: '' } // defaults for reset
);FormHelperObject
Return object dari useFormHelper composable.
interface FormHelperObject<TForm, THelper> {
// State Properties
form: TForm;
control: ControlState;
default: TForm;
errors: FormErrors;
helper: THelper | undefined;
// Display & Control Methods
onDisplay(): boolean;
onDisable(): boolean;
setDisplay(val: boolean): void;
setDisable(val: boolean): void;
allFalse(): void;
// Form Data Methods
setDefault(): void;
modifyDefault(newObj: TForm): void;
modifyForm(newObj: TForm, defaultToo?: boolean): void;
getFormRaw(): TForm;
getFormField(keyOrPath: any): any;
// Error Handling Methods
setErrors(err: FormErrors): void;
getErrors(key?: string): string[];
hasError(fieldName: string): boolean;
clearFormError(): void;
// Mode Management Methods
getMode(): Mode;
getModeShort(): ModeShort;
setMode(crud?: ModeShort): void;
isModeCreate(): boolean;
isModeUpdate(): boolean;
isModeDelete(): boolean;
isFormMode(): boolean;
isModeIn(mode: Mode | ModeShort): boolean;
// Serialization Methods
getAsFormData(...): FormData;
// Convenience Methods
openForm(mode: ModeShort, formData?: TForm, helperData?: THelper): void;
// Deprecated Methods
get<K>(targetKey?: K): ...;
toggleDisplay(val?: boolean): void;
toggleDisable(val?: boolean): void;
}Contains:
- State Properties
- Display & Control Methods
- Form Data Methods
- Error Handling Methods
- Mode Management Methods
- Serialization Methods
- Convenience Methods
- Deprecated Methods
State Properties
form
form: TFormReactive form data object.
⚠️ Penting - Penggunaan .value:
- Di
<script>: Tidak perlu.value(bukan Ref) →formHelper.form.name - Di
<template>: Langsung access →formHelper.form.name
control
control: ControlStateUI control state untuk modal display dan form disabled state.
Structure:
interface ControlState {
display: boolean; // Modal visibility
disabled: boolean; // Form disabled state
}default
default: TFormDefault form values (getter/setter). Values di-deep clone untuk prevent reference issues.
Example:
// Get defaults
console.log(formHelper.default);
// Set new defaults
formHelper.default = { name: 'New Default', email: '' };
// Reset form to defaults
formHelper.setDefault();errors
errors: FormErrorsValidation errors dalam flat dot notation (Laravel-compatible).
Structure:
interface FormErrors {
[key: string]: string[];
}Example:
{
"name": ["The name field is required"],
"email": ["Invalid email format"],
"address.street": ["Street is required"]
}helper
helper: THelper | undefinedOptional helper/presentation data (getter/setter). Uses deep merge saat di-set.
Example:
// Set helper
formHelper.helper = {
roles: [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'User' },
]
};
// Get helper
const roles = formHelper.helper?.roles;
// Clear helper
formHelper.helper = undefined;Display & Control Methods
onDisplay()
onDisplay(): booleanCheck if modal is currently displayed.
Returns: boolean - true if displayed
onDisable()
onDisable(): booleanCheck if form is currently disabled.
Returns: boolean - true if disabled
setDisplay()
setDisplay(val: boolean): voidSet modal visibility.
Parameters:
val- true to show, false to hide
setDisable()
setDisable(val: boolean): voidSet form disabled state.
Parameters:
val- true to disable, false to enable
allFalse()
allFalse(): voidReset all control states to false (hide modal and enable form).
Form Data Methods
setDefault()
setDefault(): voidReset form ke default values (deep cloned).
modifyDefault()
modifyDefault(newObj: TForm): voidUpdate stored default values menggunakan deep merge Lihat selengkapnya.
Parameters:
newObj- New default values to merge
modifyDefault()
Method untuk update default values dengan deep merge, preserving unchanged fields.
Signature:
modifyDefault(newObj: TForm): voidParameters:
newObj- Partial form data to merge into defaults
Contoh:
const formHelper = useFormHelper({
user: {
name: 'John',
email: 'john@example.com',
role: 'user'
}
});
// Update only name in defaults
formHelper.modifyDefault({
user: {
name: 'Jane'
}
});
// Defaults now: { user: { name: 'Jane', email: 'john@...', role: 'user' } }
// Email and role preservedUse Case:
Gunakan saat kamu ingin change defaults tanpa lose existing default values untuk fields lain. Berguna untuk dynamic default values based on user settings atau application state.
modifyForm()
modifyForm(newObj: TForm, defaultToo?: boolean): voidUpdate form values menggunakan deep merge Lihat selengkapnya.
Parameters:
newObj- New form values to mergedefaultToo- Also update default values if true
modifyForm()
Method untuk update form data dengan deep merge, preserving unchanged fields.
Signature:
modifyForm(newObj: TForm, defaultToo?: boolean): voidParameters:
newObj- Partial form data to mergedefaultToo- If true, also update defaults with same values (default: false)
Contoh:
interface ProfileForm {
user: {
name: string;
email: string;
bio: string;
};
settings: {
theme: string;
notifications: boolean;
};
}
const formHelper = useFormHelper<ProfileForm>({
user: {
name: 'John',
email: 'john@example.com',
bio: 'Developer'
},
settings: {
theme: 'dark',
notifications: true
}
});
// Update only name - other fields preserved
formHelper.modifyForm({
user: {
name: 'Jane'
}
});
// Result: user.name = 'Jane', email and bio unchanged
// Update multiple nested fields
formHelper.modifyForm({
user: { bio: 'Senior Developer' },
settings: { theme: 'light' }
});
// Update and save as new defaults
formHelper.modifyForm({
settings: { theme: 'system' }
}, true);
// Now 'system' becomes new default themeUse Case:
Gunakan untuk:
- Partial form updates (wizard steps)
- Conditional field updates
- Preserving unchanged nested data
Important: Ini berbeda dengan Object.assign() yang akan replace entire nested object dan lose data.
See: Deep Merge Behavior, Example 3
getFormRaw()
getFormRaw(): TFormGet non-reactive (raw) copy of entire form data.
Returns: Raw copy of form object
Example:
const formData = formHelper.getFormRaw();
// Modify without affecting reactive form
formData.extraField = 'value';getFormField()
getFormField(keyOrPath: any): anyGet field value dengan dot notation support.
Parameters:
keyOrPath- Field key atau path (e.g., 'name', 'address.street', 'tags.0')
Returns: Field value, atau undefined jika path tidak ditemukan
Example:
formHelper.getFormField('name'); // Direct field
formHelper.getFormField('address.street'); // Nested field
formHelper.getFormField('tags.0'); // Array element (dot)
formHelper.getFormField('tags[0]'); // Array element (bracket)Error Handling Methods
setErrors()
setErrors(err: FormErrors): voidSet validation errors (replaces all existing errors).
Parameters:
err- Error object in flat dot notation
Example:
// Laravel error format
formHelper.setErrors({
"name": ["The name field is required"],
"email": ["Invalid email format"],
"address.street": ["Street is required"]
});getErrors()
getErrors(key?: string): string[]Get error messages untuk specific field.
Parameters:
key- Field name in dot notation (optional)
Returns: Array of error messages, empty array if no errors
Example:
const nameErrors = formHelper.getErrors('name');
// ['The name field is required']
const allErrors = formHelper.getErrors();
// []hasError()
hasError(fieldName: string): booleanCheck if field has validation errors.
Parameters:
fieldName- Field name in dot notation
Returns: boolean - true if field has errors
clearFormError()
clearFormError(): voidClear all validation errors.
Mode Management Methods
Methods untuk manage CRUD operation mode. Library menggunakan dual-mode system:
- Short codes (
'c','u','d') untuk internal operations - Bahasa Indonesia (
'Tambah','Kelola','Delete') untuk user display
getMode()
getMode(): ModeGet current mode (long form).
Returns: 'Tambah' | 'Kelola' | 'Delete'
getModeShort()
getModeShort(): ModeShortGet current mode (short code).
Returns: 'c' | 'u' | 'd'
setMode()
setMode(crud?: ModeShort): voidSet CRUD mode.
Parameters:
crud- Mode short code:'c'(create),'u'(update),'d'(delete). Default:'c'
isModeCreate()
isModeCreate(): booleanCheck if current mode is create.
Returns: boolean - true if mode is 'Tambah'
isModeUpdate()
isModeUpdate(): booleanCheck if current mode is update.
Returns: boolean - true if mode is 'Kelola'
isModeDelete()
isModeDelete(): booleanCheck if current mode is delete.
Returns: boolean - true if mode is 'Delete'
isFormMode()
isFormMode(): booleanCheck if current mode uses form (create or update, not delete).
Returns: boolean - true if create or update
isModeIn()
isModeIn(mode: Mode | ModeShort): booleanCheck if current mode matches provided mode Lihat selengkapnya.
Parameters:
mode- Mode to check (accepts both short and long format)
Returns: boolean - true if matches
isModeIn()
Method untuk check if current mode matches provided mode. Accepts both short code dan long form.
Signature:
isModeIn(mode: Mode | ModeShort): booleanParameters:
mode- Mode to check, dapat berupa short code ('c','u','d') atau long form ('Tambah','Kelola','Delete')
Returns: boolean - true jika current mode matches
Contoh:
// Check using short code
formHelper.isModeIn('c'); // Check if create
formHelper.isModeIn('u'); // Check if update
// Check using long form
formHelper.isModeIn('Tambah'); // Same as isModeIn('c')
formHelper.isModeIn('Kelola'); // Same as isModeIn('u')
// Use in conditions
if (formHelper.isModeIn('c') || formHelper.isModeIn('u')) {
// Show form for both create and update
}Use Case:
Berguna saat kamu perlu check mode dengan flexible format, terutama saat accept mode dari external source atau user input yang bisa dalam format apapun.
Serialization Methods
getAsFormData()
getAsFormData(
arrayAsJson?: boolean,
objectAsJson?: boolean,
prefix?: string,
skipKeys?: string[]
): FormDataConvert form to FormData untuk multipart/form-data submission Lihat selengkapnya.
Parameters:
arrayAsJson- Serialize arrays as JSON string (default: false)objectAsJson- Serialize objects as JSON string (default: false)prefix- Add prefix to all keysskipKeys- Array of keys to exclude
Returns: FormData - Ready untuk submission
getAsFormData()
Method untuk serialize form data ke FormData object dengan berbagai serialization options.
Signature:
getAsFormData(
arrayAsJson?: boolean,
objectAsJson?: boolean,
prefix?: string,
skipKeys?: Array<string>
): FormDataParameters:
| Name | Type | Default | Description |
|---|---|---|---|
arrayAsJson | boolean | false | Serialize arrays as JSON string instead of bracket notation |
objectAsJson | boolean | false | Serialize objects as JSON string instead of nested keys |
prefix | string | '' | Add prefix to all keys |
skipKeys | string[] | [] | Array of keys to exclude from serialization |
Behavior:
| Type | Default Serialization | With JSON Option |
|---|---|---|
Array | field[0]=a&field[1]=b | field=["a","b"] |
Object | field[key1]=val1&field[key2]=val2 | field={"key1":"val1",...} |
Boolean | 1 or 0 | Same |
File/Blob | As-is (binary) | Same |
null/undefined | Empty string | Same |
Contoh:
interface ProductForm {
name: string;
tags: string[];
price: { amount: number; currency: string };
image: File | null;
_internal: string;
}
const formHelper = useFormHelper<ProductForm>({
name: 'Laptop',
tags: ['electronics', 'computers'],
price: { amount: 1200, currency: 'USD' },
image: null,
_internal: 'not-submitted'
});
// Basic usage - bracket notation for arrays/objects
const formData1 = formHelper.getAsFormData();
// name=Laptop
// tags[0]=electronics
// tags[1]=computers
// price[amount]=1200
// price[currency]=USD
// image=
// Arrays as JSON
const formData2 = formHelper.getAsFormData(true);
// name=Laptop
// tags=["electronics","computers"]
// price[amount]=1200
// price[currency]=USD
// Objects as JSON
const formData3 = formHelper.getAsFormData(false, true);
// name=Laptop
// tags[0]=electronics
// tags[1]=computers
// price={"amount":1200,"currency":"USD"}
// Both as JSON
const formData4 = formHelper.getAsFormData(true, true);
// name=Laptop
// tags=["electronics","computers"]
// price={"amount":1200,"currency":"USD"}
// With prefix
const formData5 = formHelper.getAsFormData(false, false, 'product_');
// product_name=Laptop
// product_tags[0]=electronics
// product_tags[1]=computers
// Skip internal fields
const formData6 = formHelper.getAsFormData(false, false, '', ['_internal']);
// name=Laptop
// tags[0]=electronics
// tags[1]=computers
// price[amount]=1200
// price[currency]=USD
// (_internal not included)
// Real-world: Submit to Laravel with file upload
const handleSubmit = async () => {
const formData = formHelper.getAsFormData(true, true);
await axios.post('/api/products', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
};Use Case:
Laravel expects multipart/form-data untuk file uploads. Method ini automatically handle:
- File/Blob objects (sent as binary)
- Boolean values (converted to '1' or '0')
- Null/undefined (converted to empty string)
- Arrays/Objects (flexible serialization)
Laravel Backend Example:
// With bracket notation (default)
$request->input('tags.0'); // 'electronics'
$request->input('price.amount'); // '1200'
// With JSON option
$tags = json_decode($request->input('tags'));
$price = json_decode($request->input('price'));Catatan: Untuk complex nested structures atau large arrays, gunakan JSON serialization (arrayAsJson: true, objectAsJson: true) untuk reduce query string length.
Convenience Methods
openForm()
openForm(mode: ModeShort, formData?: TForm, helperData?: THelper): voidOpen modal form with proper setup (all-in-one method) Lihat selengkapnya.
Parameters:
mode- CRUD mode:'c'(create),'u'(update),'d'(delete)formData- Optional data to prefill formhelperData- Optional presentation/lookup data
openForm()
All-in-one method untuk open modal dengan proper setup: set mode, handle form data, manage helper, dan show modal.
Signature:
openForm(mode: ModeShort, formData?: TForm, helperData?: THelper): voidParameters:
| Name | Type | Required | Description |
|---|---|---|---|
mode | ModeShort | Yes | CRUD mode: 'c' (create), 'u' (update), 'd' (delete) |
formData | TForm | No | Data untuk prefill form (for edit/delete) |
helperData | THelper | No | Presentation/lookup data |
Behavior by Mode:
| Mode | Form Data | Helper Data | Display |
|---|---|---|---|
'c' (Create) | Reset to defaults | Set if provided, clear if not | Show modal |
'u' (Update) | Merge provided data | Set if provided, keep existing if not | Show modal |
'd' (Delete) | Merge provided data | Set if provided, keep existing if not | Show modal |
Contoh:
interface UserForm {
user_id?: number;
name: string;
email: string;
role_id: number;
}
interface UserHelper {
user?: {
id: number;
name: string;
email: string;
role: { id: number; name: string };
};
roles: Array<{ id: number; name: string }>;
}
const formHelper = useFormHelper<UserForm, UserHelper>({
name: '',
email: '',
role_id: 0,
});
// Create mode - empty form with helper data
const openCreate = () => {
formHelper.openForm('c', undefined, {
roles: [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'User' },
]
});
// Form: { name: '', email: '', role_id: 0 } (reset to defaults)
// Helper: { roles: [...] }
// Modal: shown
};
// Update mode - prefill form with data and helper
const openEdit = (user: UserHelper['user']) => {
formHelper.openForm('u', {
user_id: user!.id,
name: user!.name,
email: user!.email,
role_id: user!.role.id,
}, {
user: user,
roles: [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'User' },
]
});
// Form: { user_id: 123, name: 'John', ... } (merged with existing)
// Helper: { user: {...}, roles: [...] }
// Modal: shown
};
// Delete mode - just ID for deletion with helper for display
const openDelete = (user: UserHelper['user']) => {
formHelper.openForm('d', { user_id: user!.id }, { user });
// Form: { user_id: 123 }
// Helper: { user: {...} } (untuk show confirmation: "Delete John Doe?")
// Modal: shown
};What It Does:
- Sets mode: Internal mode state updated
- Resets control:
allFalse()- disable=false, display will be set to true at end - Handles form data:
- Create (
'c'): Reset to defaults (ignore formData) - Update/Delete: Merge formData into form (deep merge)
- Create (
- Handles helper data:
- If provided: Set helper
- If not provided and mode is create: Clear helper
- If not provided and mode is update/delete: Keep existing helper
- Shows modal:
setDisplay(true)
Use Case:
Simplify modal opening logic - instead of manually calling setMode(), modifyForm(), helper = ..., setDisplay(true), just call one method. Especially useful untuk CRUD operations dengan consistent pattern.
See: Modal/Dialog-Optimized Form Pattern
Deprecated Methods
Methods berikut masih available untuk backward compatibility tapi not recommended untuk new code.
get()
get<K extends keyof TForm>(targetKey?: K): K extends keyof TForm ? TForm[K] : TForm⚠️ Deprecated: Use form.fieldName directly, getFormField(key) for single field, or getFormRaw() for entire form.
toggleDisplay()
toggleDisplay(val?: boolean): void⚠️ Deprecated: Use setDisplay(boolean) instead.
toggleDisable()
toggleDisable(val?: boolean): void⚠️ Deprecated: Use setDisable(boolean) instead.
Types
Mode
type Mode = 'Tambah' | 'Kelola' | 'Delete'CRUD operation modes dalam Bahasa Indonesia.
'Tambah'- Create/Add'Kelola'- Update/Manage'Delete'- Delete
ModeShort
type ModeShort = 'c' | 'u' | 'd'Short code representation of CRUD modes.
'c'- Create (Tambah)'u'- Update (Kelola)'d'- Delete
FormErrors
interface FormErrors {
[key: string]: string[];
}Validation errors structure menggunakan flat dot notation (Laravel compatible).
Example:
{
"name": ["The name field is required"],
"address.street": ["The street field is required"],
"tags.0": ["Invalid tag format"]
}ControlState
interface ControlState {
display: boolean;
disabled: boolean;
}UI control state untuk modal display dan form disabled state.
Properties:
display- Whether modal/form is visibledisabled- Whether form inputs are disabled
Examples
Practical patterns untuk advanced usage.
Contains:
1. Validation Error Handling
Handling Laravel validation errors dengan flat dot notation.
<script setup lang="ts">
import { useFormHelper } from '@bpmlib/utils-form-helper';
interface UserForm {
name: string;
email: string;
address: {
street: string;
city: string;
};
}
const formHelper = useFormHelper<UserForm>({
name: '',
email: '',
address: {
street: '',
city: '',
},
});
const handleSubmit = async () => {
formHelper.setDisable(true);
formHelper.clearFormError();
try {
await api.post('/users', formHelper.getAsFormData());
formHelper.setDisplay(false);
} catch (error: any) {
// Laravel returns: { errors: { "name": ["Required"], "address.street": [...] } }
if (error.response?.data?.errors) {
formHelper.setErrors(error.response.data.errors);
}
} finally {
formHelper.setDisable(false);
}
};
</script>
<template>
<Modal :show="formHelper.onDisplay()" @close="formHelper.setDisplay(false)">
<form @submit.prevent="handleSubmit">
<!-- Basic field error -->
<div class="field">
<input
v-model="formHelper.form.name"
:disabled="formHelper.onDisable()"
placeholder="Name"
/>
<span v-if="formHelper.hasError('name')" class="error">
{{ formHelper.getErrors('name')[0] }}
</span>
</div>
<!-- Email field -->
<div class="field">
<input
v-model="formHelper.form.email"
:disabled="formHelper.onDisable()"
placeholder="Email"
/>
<span v-if="formHelper.hasError('email')" class="error">
{{ formHelper.getErrors('email')[0] }}
</span>
</div>
<!-- Nested field error (dot notation) -->
<div class="field">
<input
v-model="formHelper.form.address.street"
:disabled="formHelper.onDisable()"
placeholder="Street"
/>
<span v-if="formHelper.hasError('address.street')" class="error">
{{ formHelper.getErrors('address.street')[0] }}
</span>
</div>
<div class="field">
<input
v-model="formHelper.form.address.city"
:disabled="formHelper.onDisable()"
placeholder="City"
/>
<span v-if="formHelper.hasError('address.city')" class="error">
{{ formHelper.getErrors('address.city')[0] }}
</span>
</div>
<button type="submit" :disabled="formHelper.onDisable()">
Save
</button>
</form>
</Modal>
</template>Key Takeaways:
setErrors()- Accept Laravel error format directlyhasError(field)- Check if field has errors before showinggetErrors(field)- Get error messages array (show first message)clearFormError()- Clear all errors before resubmit- Supports dot notation untuk nested fields (
'address.street') setDisable()/onDisable()- Prevent double-submit during processing
2. Form vs Helper Data Separation
Demonstrates TForm (submission) vs THelper (presentation) pattern dengan order form example.
<script setup lang="ts">
import { computed } from 'vue';
import { useFormHelper } from '@bpmlib/utils-form-helper';
interface OrderForm {
product_id: number;
quantity: number;
notes: string;
}
interface OrderHelper {
products: Array<{
id: number;
name: string;
price: number;
stock: number;
}>;
}
const formHelper = useFormHelper<OrderForm, OrderHelper>({
product_id: 0,
quantity: 1,
notes: '',
});
const openCreate = () => {
formHelper.openForm('c', undefined, {
products: [
{ id: 1, name: 'Laptop', price: 1200, stock: 10 },
{ id: 2, name: 'Mouse', price: 25, stock: 50 },
{ id: 3, name: 'Keyboard', price: 75, stock: 30 },
],
});
};
// Computed from helper data
const selectedProduct = computed(() =>
formHelper.helper?.products.find(p => p.id === formHelper.form.product_id)
);
const total = computed(() =>
(selectedProduct.value?.price || 0) * formHelper.form.quantity
);
const canSubmit = computed(() =>
formHelper.form.product_id > 0 &&
formHelper.form.quantity > 0 &&
(formHelper.form.quantity <= (selectedProduct.value?.stock || 0))
);
const handleSubmit = async () => {
await api.post('/orders', formHelper.getAsFormData());
formHelper.setDisplay(false);
};
</script>
<template>
<button @click="openCreate">Create Order</button>
<Modal :show="formHelper.onDisplay()" @close="formHelper.setDisplay(false)">
<form @submit.prevent="handleSubmit">
<!-- Product dropdown - from helper.products -->
<div class="field">
<label>Product</label>
<select v-model="formHelper.form.product_id">
<option value="0">Select Product</option>
<option
v-for="product in formHelper.helper?.products"
:key="product.id"
:value="product.id"
>
{{ product.name }} - ${{ product.price }} (Stock: {{ product.stock }})
</option>
</select>
</div>
<!-- Show selected product info from helper (computed) -->
<div v-if="selectedProduct" class="product-info">
<p><strong>Price:</strong> ${{ selectedProduct.price }}</p>
<p><strong>Available:</strong> {{ selectedProduct.stock }} units</p>
</div>
<!-- Quantity - editable in form -->
<div class="field">
<label>Quantity</label>
<input
v-model.number="formHelper.form.quantity"
type="number"
min="1"
:max="selectedProduct?.stock || 999"
/>
</div>
<!-- Computed total from helper data -->
<div class="total-section">
<p><strong>Total:</strong> ${{ total }}</p>
</div>
<!-- Notes - editable in form -->
<div class="field">
<label>Notes</label>
<textarea v-model="formHelper.form.notes"></textarea>
</div>
<button type="submit" :disabled="!canSubmit">
Create Order
</button>
</form>
</Modal>
</template>Key Takeaways:
- Form: Hanya field yang di-submit (product_id, quantity, notes)
- Helper: Full objects untuk display + dropdowns (products array)
- Helper provides context tanpa pollute form data
- Computed values derived dari helper data (selectedProduct, total)
- Submit hanya kirim form, tidak include helper
- Clean separation: IDs in form, full objects in helper
3. Partial Form Updates (Deep Merge)
Deep merge behavior untuk update nested fields tanpa losing data.
<script setup lang="ts">
import { useFormHelper } from '@bpmlib/utils-form-helper';
interface ProfileForm {
user: {
name: string;
email: string;
bio: string;
};
preferences: {
theme: string;
notifications: {
email: boolean;
sms: boolean;
};
};
}
const formHelper = useFormHelper<ProfileForm>({
user: {
name: 'John Doe',
email: 'john@example.com',
bio: 'Software developer',
},
preferences: {
theme: 'dark',
notifications: {
email: true,
sms: false,
},
},
});
// Scenario 1: Update single nested field
const updateName = () => {
formHelper.modifyForm({
user: {
name: 'Jane Doe',
// email and bio unchanged
}
});
// Result:
// user: {
// name: 'Jane Doe', ← Updated
// email: 'john@...', ← Unchanged
// bio: 'Software...', ← Unchanged
// }
};
// Scenario 2: Update deeply nested field
const updateEmailNotification = () => {
formHelper.modifyForm({
preferences: {
notifications: {
email: false,
// sms unchanged
}
}
});
// Result:
// preferences: {
// theme: 'dark', ← Unchanged
// notifications: {
// email: false, ← Updated
// sms: false, ← Unchanged
// }
// }
};
// Scenario 3: Compare with Object.assign (wrong way)
const wrongWayWithAssign = () => {
// ❌ This replaces entire nested object
Object.assign(formHelper.form.preferences, {
notifications: {
email: false,
}
});
// Result: LOST sms field!
// preferences: {
// theme: 'dark',
// notifications: {
// email: false, ← Only this remains
// // sms LOST!
// }
// }
};
// Scenario 4: Update and save as new defaults
const updateAndSaveDefaults = () => {
formHelper.modifyForm(
{
preferences: {
theme: 'light',
}
},
true // Also update defaults
);
// Now theme: 'light' becomes new default
// Subsequent setDefault() will reset to 'light', not 'dark'
};
// Reset to defaults after modifications
const resetAfterModify = () => {
// Modify some fields
formHelper.modifyForm({
user: { name: 'Changed' },
preferences: { theme: 'light' },
});
console.log(formHelper.form.user.name); // 'Changed'
// Reset to original defaults
formHelper.setDefault();
console.log(formHelper.form.user.name); // 'John Doe'
};
</script>
<template>
<div>
<h2>Deep Merge Examples</h2>
<button @click="updateName">Update Name Only</button>
<button @click="updateEmailNotification">Update Email Notification</button>
<button @click="updateAndSaveDefaults">Update & Save Defaults</button>
<button @click="resetAfterModify">Reset After Modify</button>
<div class="current-state">
<h3>Current Form State:</h3>
<pre>{{ JSON.stringify(formHelper.form, null, 2) }}</pre>
</div>
<div class="defaults">
<h3>Default Values:</h3>
<pre>{{ JSON.stringify(formHelper.default, null, 2) }}</pre>
</div>
</div>
</template>Key Takeaways:
modifyForm()uses deep merge, preserves unchanged fields- Update single field: only that field changes
- Update nested: only specified nested fields change
Object.assign()would replace entire nested object (data loss!)defaultToo: trueupdates both form and defaults- Useful for wizard forms, partial updates, incremental data collection
setDefault()always resets to stored defaults (unless modified withdefaultToo)