Docs

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

  1. Regular table columns appear first
  2. Relation fields appear after column fields
  3. 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 isNullable setting)
  • 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:

  1. Backend validates all permissions on API calls
  2. Frontend checks permissions before showing UI elements
  3. Menus hide options user cannot access
  4. 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

  1. Initial State: Form loads with original data
  2. Change Detection: Any modification triggers change tracking
  3. Save Button: Only enabled when changes are detected
  4. 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 state
  • formEditorRef: Reference to FormEditor component
  • @has-changed: Event emitted when form state changes
  • confirmChanges(): 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

  1. Change Detection: Tracks when form data is modified
  2. Reset Button: Appears only when changes exist
  3. Confirmation: Asks user to confirm before discarding
  4. 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

  1. Always Confirm: Use confirmation dialog for destructive actions
  2. Clear Messaging: Explain what will be lost
  3. Positioning: Place reset button on the left side
  4. 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

  1. Fill in required fields
  2. Validation runs automatically
  3. Click "Create" to save
  4. Success notification and redirect

Update Flow

  1. Modify existing values
  2. Changes tracked automatically
  3. Click "Save" when ready
  4. 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.