Form System
Form System Enfyra's form system automatically generates forms from your database schema. Forms are dynamic, validated, and fully integrated with permissions and relations. How Forms Work When you create a table in Enfyra, the system automatically generates forms for creating and
Form System
Enfyra's form system automatically generates forms from your database schema. Forms are dynamic, validated, and fully integrated with permissions and relations.
How Forms Work
When you create a table in Enfyra, the system automatically generates forms for creating and editing records. These forms:
- Display appropriate input types based on field types
- Validate data according to schema rules
- Handle relations between tables
- Respect user permissions
Field Types and Input Components
Basic Field Types
| Field Type | Input Component | Description |
|---|---|---|
| varchar/string | Text input | Single-line text field |
| text | Textarea | Multi-line text area |
| int/number | Number input | Numeric input with validation |
| boolean | Toggle switch | On/off toggle |
| date/timestamp | Date picker | Calendar date selector |
| uuid | UUID field | Auto-generated unique ID with copy button |
Advanced Field Types
| Field Type | Input Component | Description |
|---|---|---|
| richtext | Rich text editor | WYSIWYG editor with formatting tools (configure via column.metadata.richText) |
| code | Code editor | Syntax-highlighted code input |
| simple-json | JSON editor | JSON input with validation |
| enum | Dropdown | Single selection from options |
| array-select | Multi-select | Multiple selections from options |
Rich Text Editor (column.metadata.richText)
For richtext columns, you can customize the editor by setting metadata.richText on the column. This is configured in the table schema (column metadata field as JSON).
Example column metadata:
{
"richText": {
"customButtons": [
{ "name": "codeinline", "text": "Code", "tooltip": "Inline code", "format": "code" },
{ "name": "codeblock", "text": "Code Block", "tooltip": "Code block", "format": "pre" }
],
"formats": {
"code": {
"inline": true,
"tag": "code",
"css": {
"backgroundColor": "#2d2d2d",
"color": "#ccc",
"padding": "2px 4px",
"borderRadius": "3px",
"fontFamily": "monospace"
}
},
"pre": {
"block": true,
"tag": "pre",
"css": {
"light": {
"backgroundColor": "#e1e1e1",
"padding": "12px",
"borderRadius": "4px",
"overflow": "auto",
"fontFamily": "monospace",
"whiteSpace": "pre",
"color": "#333"
},
"dark": {
"backgroundColor": "#1e1e1e",
"padding": "12px",
"borderRadius": "4px",
"overflow": "auto",
"fontFamily": "monospace",
"whiteSpace": "pre",
"color": "#ccc"
}
}
}
}
}
}
Format types
| Property | Description |
|---|---|
inline: true |
Inline element (e.g. <code>, <span>). Default tag is span. |
block: true or omit |
Block element (e.g. <pre>, <div>). Default when neither inline nor wrapper is set. |
wrapper: true |
Wrapper node that contains other blocks (e.g. <blockquote>). |
tag |
HTML tag name (e.g. "code", "pre"). Optional; uses format key if not specified. |
Format properties
css: CSS styles injected as stylesheet (not inline). Supports:- Flat object: applies to both light and dark themes
{ light: {...}, dark: {...} }: theme-specific styles- Function:
(theme: 'light' | 'dark') => Record<string, string> classes: CSS classes for the tag (string, array, or function)attributes: HTML attributes for the element
Toolbar
Default toolbar: clear | h1 h2 h3 h4 h5 h6 | bold italic underline strike | bullist numlist | alignleft aligncenter alignright alignjustify | link image table blockquote hr | codeblock
Add custom buttons via customButtons and reference them in toolbar string. Each custom button can use format to toggle a format (e.g. format: "code" toggles the code format).
Special Field Types
| Field Type | Component | Description |
|---|---|---|
| relation | Relation Picker | Select related records |
| permission | Permission Builder | Configure access rules |
Form Layout
Responsive Grid Layout
- Forms use a 2-column grid on desktop, 1-column on mobile
- Full-width fields automatically span both columns:
- Rich text editors
- Code editors
- JSON editors
- Long text areas
Field Ordering
- Regular table columns appear first
- Relation fields appear after column fields
- Custom ordering via configuration
Form Validation
Required Fields
- Fields marked with red asterisk (*) are required
- Validation runs before form submission
- Clear error messages explain what's missing
Validation Rules
- Required: Field cannot be empty (based on
isNullablesetting) - Type: Input must match field type (number, date, etc.)
- Format: Special formats like email, URL (when configured)
- Custom: Business logic validation via hooks
Error Display
- Field-level error messages appear below inputs
- Errors clear automatically when corrected
- Toast notifications for form-level issues
Working with Relations
One-to-One / Many-to-One
- Single selection from related table
- Shows selected record as badge
- Click to change selection
One-to-Many / Many-to-Many
- Multiple selection from related table
- Shows count of selected records
- Manage selections in modal
See Relation Picker System for detailed usage
Permission Integration
Forms respect user permissions automatically:
Field-Level Permissions
- Fields only show if user has read permission
- Edit capability based on update permission
- Delete options require delete permission
Form-Level Permissions
- Create forms require create permission on table
- Edit forms require update permission
- Save button disabled without proper permissions
How Permissions Work:
- Backend validates all permissions on API calls
- Frontend checks permissions before showing UI elements
- Menus hide options user cannot access
- Forms disable fields user cannot edit
See Permission Builder for technical details
Special Form Features
Copy Field Values
- Click copy icon next to any field
- Useful for debugging or sharing data
- Works with all field types
Field Placeholders
- Helpful hints appear in empty fields
- Configured per-field in table settings
- Guide users on expected input
Field Descriptions
- Rich text descriptions below field labels
- Provide context and instructions
- Set in table column configuration
Default Values
- Pre-filled values for new records
- Different defaults per field type
- Can be dynamic (like current date)
Form States
Loading State
- Skeleton loaders while fetching data
- Type-specific loading animations
- Smooth transitions when ready
Edit Mode
- Change detection tracks modifications
- Save button enables only with changes
- Confirmation on unsaved changes
Form Change Detection
Enfyra automatically tracks form changes to provide better user experience:
How It Works
- Initial State: Form loads with original data
- Change Detection: Any modification triggers change tracking
- Save Button: Only enabled when changes are detected
- Reset: Changes reset after successful save
Implementation Pattern
const { register: registerHeaderActions } = useHeaderActionRegistry();
// In your page component
const hasFormChanges = ref(false);
const formEditorRef = ref();
// Template
<FormEditor
ref="formEditorRef"
v-model="form"
v-model:errors="errors"
@has-changed="(hasChanged) => hasFormChanges = hasChanged"
table-name="user_definition"
:loading="loading"
/>
// Save function
async function save() {
// ... save logic
await updateUser({ body: form.value });
// Reset form changes after successful save
formEditorRef.value?.confirmChanges();
}
// Header action
registerHeaderActions([
{
id: "save-user",
label: "Save",
disabled: computed(() => !hasFormChanges.value),
submit: save,
},
]);
Key Components
hasFormChanges: Reactive boolean tracking form stateformEditorRef: Reference to FormEditor component@has-changed: Event emitted when form state changesconfirmChanges(): Method to reset change detection
Benefits
- Better UX: Save button only appears when needed
- Data Safety: Prevents accidental saves
- Clear Feedback: Users know when changes exist
- Automatic Reset: No manual state management needed
Reset Button Pattern
Enfyra provides a consistent reset button pattern to discard form changes:
How It Works
- Change Detection: Tracks when form data is modified
- Reset Button: Appears only when changes exist
- Confirmation: Asks user to confirm before discarding
- Restore: Returns form to original state
Implementation Pattern
const { register: registerHeaderActions } = useHeaderActionRegistry();
// In your page component
const { confirm } = useConfirm();
const hasFormChanges = ref(false);
const formEditorRef = ref();
// Reset function
async function handleReset() {
const ok = await confirm({
title: "Discard Changes",
content: "Are you sure you want to discard all changes? This action cannot be undone.",
});
if (ok) {
// Reset form to original state
form.value = formChanges.discardChanges(form.value);
formEditorRef.value?.confirmChanges();
}
}
// Header actions
registerHeaderActions([
{
id: "save-user",
label: "Save",
disabled: computed(() => !hasFormChanges.value),
submit: save,
},
{
id: "reset-user",
label: "Reset",
variant: "outline",
color: "gray",
side: "left",
show: computed(() => hasFormChanges.value),
onClick: handleReset,
},
]);
Using useSchema Pattern
For forms using useSchema, you can leverage the built-in discardChanges function:
// Using useSchema composable
const { formChanges } = useSchema(tableName);
// Initialize form changes tracking
onMounted(async () => {
const data = await fetchData();
form.value = data;
formChanges.update(data); // Track original state
});
// Reset function
async function handleReset() {
const ok = await confirm({
title: "Discard Changes",
content: "Are you sure you want to discard all changes?",
});
if (ok) {
// Use useSchema's discardChanges
form.value = formChanges.discardChanges(form.value);
formEditorRef.value?.confirmChanges();
}
}
Key Features
- Conditional Display: Reset button only shows when changes exist
- Confirmation Dialog: Prevents accidental data loss
- Deep Reset: Restores all form fields to original values
- State Sync: Updates form change detection state
- Consistent UX: Same pattern across all forms
Best Practices
- Always Confirm: Use confirmation dialog for destructive actions
- Clear Messaging: Explain what will be lost
- Positioning: Place reset button on the left side
- State Management: Always call
confirmChanges()after reset
Disabled Fields
- System fields cannot be edited
- Generated fields are read-only
- Conditional disabling via configuration
Form Submission
Create Flow
- Fill in required fields
- Validation runs automatically
- Click "Create" to save
- Success notification and redirect
Update Flow
- Modify existing values
- Changes tracked automatically
- Click "Save" when ready
- Instant feedback on success
Error Handling
- API errors show as notifications
- Field errors highlight specific inputs
- Retry capability for network issues
Advanced Configuration
FieldMap System
The fieldMap prop allows you to customize form behavior for specific fields without modifying the database schema. Pass it to FormEditor or FormEditorLazy (same props; lazy defers loading the heavy editor bundle — use in drawers and secondary views).
<FormEditorLazy
v-model="form"
:table-name="tableName"
:field-map="fieldMap"
/>
Sections (grouped fields)
Pass sections to render fields in named blocks. Field order within a section follows the fields array. Any schema field not listed in a section is rendered after the sections (still ordered by the default schema rules).
const sections = [
{
id: 'main',
title: 'General',
fields: ['name', 'slug', 'description'],
},
{
id: 'meta',
title: 'Metadata',
hideHeading: true,
class: 'surface-card rounded-xl p-4 ring-1 ring-[var(--surface-panel-ring)]',
fields: ['createdAt'],
},
];
| Section field | Description |
|---|---|
id |
Stable key for the block |
title |
Optional heading above the block |
hideHeading |
If true, title is not shown |
headingClass |
Override heading typography |
class |
Wrapper around the section’s field grid (e.g. card panel) |
rootClass |
Extra class on the outer section block |
fields |
Ordered list of field names |
Basic FieldMap Usage
// Example: Change field type
const fieldMap = computed(() => ({
description: {
type: 'richtext' // Make description a rich text field
}
}));
// Example: Span full width on desktop grid (layout="grid" uses md:grid-cols-2)
const fieldMap = computed(() => ({
content: {
fieldProps: {
class: 'md:col-span-2'
}
}
}));
// Example: Custom placeholder
const fieldMap = computed(() => ({
email: {
placeholder: '[email protected]'
}
}));
// Example: Disable a field
const fieldMap = computed(() => ({
type: {
disabled: true // Field cannot be edited
}
}));
Filtering Enum Options
For enum and array-select fields, you can filter which options appear in the dropdown:
Exclude Options:
// Hide specific options from enum dropdown
const fieldMap = computed(() => ({
storageType: {
excludedOptions: ['Local Storage', 'Legacy System']
}
}));
Include Only Specific Options:
// Show only specific options in enum dropdown
const fieldMap = computed(() => ({
status: {
includedOptions: ['Active', 'Pending', 'Review']
}
}));
Complete Example - Storage Configuration:
// In storage config create page
const fieldMap = computed(() => ({
type: {
excludedOptions: ['Local Storage'] // Hide Local Storage from create form
}
}));
// In storage config detail page
const fieldMap = computed(() => ({
type: {
disabled: true, // Cannot change type after creation
excludedOptions: ['Local Storage'] // Still hide it
}
}));
Field-Level Permissions
You can control field visibility using the permission option in fieldMap. Fields are automatically shown if no permission is specified, or only shown when the user has the required permission:
// Example: Show field only to users with specific permission
const fieldMap = computed(() => ({
adminNotes: {
permission: {
and: [
{ route: '/user_definition', actions: ['update'] }
]
}
}
}));
// Example: Show field with OR condition
const fieldMap = computed(() => ({
sensitiveData: {
permission: {
or: [
{ route: '/admin_panel', actions: ['read'] },
{ route: '/reports', actions: ['read'] }
]
}
}
}));
Note: If permission is not specified, the field is automatically shown to all users. This allows you to selectively restrict fields without affecting others.
FieldMap Options Reference
| Option | Type | Description |
|---|---|---|
type |
string |
Override field input type (richtext, code, etc.) |
disabled |
boolean |
Make field read-only |
placeholder |
string |
Custom placeholder text |
permission |
PermissionCondition |
Control field visibility based on user permissions (see Permission Builder) |
excludedOptions |
string[] |
Hide these options from enum/array-select dropdowns |
includedOptions |
string[] |
Show only these options in enum/array-select dropdowns |
fieldProps |
object |
Passed to the field row; use class for grid spans (e.g. md:col-span-2 when layout="grid") |
booleanWrapperClass |
string |
Replaces the default flex wrapper classes for boolean fields (label + toggle row) |
fieldWrapperClass |
string |
Extra classes on the outer wrapper for non-boolean fields |
componentProps |
object |
Merged into the control; class is merged with internal control classes |
label / description |
string |
Override column label / help HTML |
hideLabel / hideDescription |
boolean |
Hide label or description |
component |
Component |
Custom input component (modelValue / update:modelValue) |
options |
any[] |
Override all options (use with excludedOptions/includedOptions for filtering) |
Note: excludedOptions and includedOptions work with the original enum options from your schema. They filter the options, not replace them.
Field Visibility
- Include/exclude specific fields
- Conditional visibility based on values
- Role-based field display
Integration with Header Actions
Save Button
- Appears in header when editing
- Disabled when no changes
- Shows loading state during save
Delete Button
- Available when viewing existing record
- Requires delete permission
- Confirmation dialog prevents accidents
Custom Actions
- Add custom buttons via configuration
- Integrate with form data
- Permission-controlled visibility
Best Practices
Form Design
- Keep forms focused and concise
- Group related fields together
- Use descriptions for complex fields
- Provide helpful placeholders
Validation
- Validate early and often
- Provide clear error messages
- Use frontend and backend validation
- Consider user experience
Performance
- Lazy load heavy components
- Use pagination for relation pickers
- Optimize validation logic
- Cache schema when possible
Common Scenarios
Multi-Step Forms
While Enfyra uses single-page forms by default, you can:
- Use hooks to create wizard-style flows
- Conditionally show/hide sections
- Save drafts between steps
Dependent Fields
- Use hooks to update fields based on others
- Calculate values automatically
- Validate field combinations
File Uploads
- File fields integrate with file system
- Drag-and-drop support
- Progress indicators for uploads
Troubleshooting
Form Not Loading
- Check table schema exists
- Verify user has read permission
- Ensure API endpoint is accessible
Validation Errors
- Review field requirements
- Check data types match schema
- Verify custom validation logic
Save Failures
- Confirm user has update permission
- Check network connectivity
- Review API error messages
Field Map (Custom Field Config)
Use :field-map prop on FormEditorLazy to customize individual fields.
<FormEditorLazy
v-model="form"
:table-name="'my_table'"
:field-map="{
name: { label: 'Full Name', description: 'Enter your full name' },
status: { hideDescription: true },
config: { component: MyCustomEditor, componentProps: { lang: 'json' } },
secret: { hideLabel: true },
}"
/>
Supported Config Options
| Option | Type | Description |
|---|---|---|
label |
string | Override field label |
description |
string | Override field description text (below input) |
hideLabel |
boolean | Hide label + description section entirely |
hideDescription |
boolean | Hide only description, keep label |
component |
string | Component | Replace field with custom component (receives v-model) |
componentProps |
object | Extra props passed to custom component |
type |
string | Override field type (e.g. 'code', 'method-selector') |
disabled |
boolean | Disable the field |
placeholder |
string | Override placeholder text |
permission |
object | Permission condition to show/hide field |
fieldProps |
object | Extra props on the wrapper element |
Custom Component Example
Create a component that accepts modelValue + emits update:modelValue:
<template>
<div>
<UInput :model-value="modelValue" @update:model-value="$emit('update:modelValue', $event)" />
<p>Custom UI here</p>
</div>
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
Then use in field-map:
const MyEditor = resolveComponent('MyCustomEditor');
const fieldMap = {
myField: { component: MyEditor, componentProps: { extra: 'prop' } }
};
The form system provides everything needed for data entry while maintaining security, validation, and excellent user experience.