JSON Schema to Live Forms: Dynamic Forms Made Easy
Adrian Miu

Adrian Miu @adrian_miu

Joined:
May 24, 2025

JSON Schema to Live Forms: Dynamic Forms Made Easy

Publish Date: Jun 19
0 0

Imagine building forms that adapt in real-time to user input, server data, or business logic changes—without writing a single line of component code. Picture an admin panel where non-technical users can define form structures, or a multi-tenant application where each client gets custom forms tailored to their workflow.

This isn't science fiction. It's the power of dynamic forms, and with Enforma's schema-driven architecture, it's simpler than you might think.

If you've ever found yourself copy-pasting form components, writing endless conditional rendering logic, or struggling to maintain forms that need to change frequently, dynamic forms are the solution you've been looking for.

Introduction: The Power of Dynamic Forms

What Are Dynamic Forms?

Dynamic forms are forms whose structure, fields, validation rules, and behavior are determined at runtime rather than being hardcoded in your components. Instead of defining form layouts in Vue templates, you describe them using JSON configurations that can be stored in databases, loaded from APIs, or even modified by end users.

// Traditional static form - hardcoded structure
<template>
  <form>
    <input name="firstName" />
    <input name="lastName" />
    <input name="email" />
    <!-- Fixed structure, requires code changes to modify -->
  </form>
</template>

// Dynamic form - runtime-defined structure
const formSchema = {
  firstName: { type: 'field', label: 'First Name', required: true },
  lastName: { type: 'field', label: 'Last Name', required: true },
  email: { type: 'field', label: 'Email', rules: 'required|email' }
}
// Same form, but completely configurable without code changes
Enter fullscreen mode Exit fullscreen mode

Why Dynamic Forms Are Powerful

1. Server-Driven UIs
Load form configurations from your backend, enabling you to modify forms without deploying new code.

2. User-Configurable Forms
Let non-technical users create and modify forms through admin interfaces.

3. Multi-Tenant Applications
Provide different form structures for different clients or use cases.

4. A/B Testing Forms
Easily test different form layouts and field configurations.

5. Conditional Complex Logic
Create forms that adapt based on user permissions, subscription levels, or business rules.

Use Cases: Where Dynamic Forms Shine

Content Management Systems

Challenge: Different content types need different input fields. Blog posts need titles and content, events need dates and locations, products need prices and categories.

Dynamic Solution:

// Load content type schemas from server
const blogPostSchema = {
  title: { type: 'field', label: 'Title', rules: 'required' },
  content: { type: 'field', label: 'Content', inputComponent: 'textarea' },
  tags: { type: 'repeatable', subfields: { name: { label: 'Tag' } } }
}

const eventSchema = {
  title: { type: 'field', label: 'Event Name', rules: 'required' },
  startDate: { type: 'field', label: 'Start Date', inputComponent: 'datepicker' },
  venue: { type: 'field', label: 'Venue', rules: 'required' },
  capacity: { type: 'field', label: 'Max Attendees', inputComponent: 'number' }
}

const productSchema = {
  name: { type: 'field', label: 'Product Name', rules: 'required' },
  price: { type: 'field', label: 'Price', inputComponent: 'currency' },
  variants: { 
    type: 'repeatable_table',
    subfields: {
      size: { label: 'Size' },
      color: { label: 'Color' },
      sku: { label: 'SKU', rules: 'required' }
    }
  }
}

// One component handles all content types
<EnformaSchema :schema="currentContentTypeSchema" />
Enter fullscreen mode Exit fullscreen mode

Admin Dashboards

Challenge: Different user roles need different form fields. Super admins see all fields, managers see some, regular users see basic fields only.

Dynamic Solution:

// Server returns schema based on user permissions
const getUserProfileSchema = (userRole) => {
  const baseSchema = {
    firstName: { type: 'field', label: 'First Name', required: true },
    lastName: { type: 'field', label: 'Last Name', required: true },
    email: { type: 'field', label: 'Email', rules: 'required|email' }
  }

  if (userRole === 'manager' || userRole === 'admin') {
    baseSchema.department = {
      type: 'field',
      label: 'Department',
      inputComponent: 'select',
      inputProps: { options: ['Sales', 'Marketing', 'Engineering'] }
    }
  }

  if (userRole === 'admin') {
    baseSchema.permissions = {
      type: 'repeatable',
      label: 'Permissions',
      subfields: {
        resource: { label: 'Resource', rules: 'required' },
        actions: { 
          label: 'Actions',
          inputComponent: 'multiselect',
          inputProps: { options: ['read', 'write', 'delete'] }
        }
      }
    }
  }

  return baseSchema
}
Enter fullscreen mode Exit fullscreen mode

Survey and Form Builders

Challenge: Non-technical users need to create complex forms without developer intervention.

Dynamic Solution:

// Form builder saves this configuration
const surveySchema = {
  intro_section: {
    type: 'section',
    title: 'Personal Information'
  },
  age: {
    type: 'field',
    section: 'intro_section',
    label: 'What is your age?',
    inputComponent: 'select',
    inputProps: {
      options: ['18-25', '26-35', '36-45', '46-55', '55+']
    }
  },
  experience_section: {
    type: 'section',
    title: 'Work Experience',
    if: '${form.values().age !== "18-25"}' // Conditional section
  },
  current_role: {
    type: 'field',
    section: 'experience_section',
    label: 'Current Job Role',
    if: '${form.values().age !== "18-25"}'
  }
}

// Survey gets rendered without any hardcoded components
<EnformaSchema :schema="loadedSurveySchema" />
Enter fullscreen mode Exit fullscreen mode

How Enforma Processes JSON Schemas

Basic Schema Structure

Enforma uses a simple but powerful schema format:

const schema = {
  // SECTIONS - Group related fields
  personal_info: {
    type: 'section',
    title: 'Personal Information',
    titleComponent: 'h3'
  },

  // FIELDS - Individual form inputs
  firstName: {
    type: 'field',
    section: 'personal_info', // Belongs to personal_info section
    label: 'First Name',
    required: true,
    rules: 'required|min_length:2',
    inputProps: { placeholder: 'Enter your first name' }
  },

  // REPEATABLE FIELDS - Dynamic arrays
  skills: {
    type: 'repeatable_table',
    section: 'personal_info',
    subfields: {
      name: { label: 'Skill Name', rules: 'required' },
      level: { 
        label: 'Level',
        inputComponent: 'select',
        inputProps: { options: ['Beginner', 'Intermediate', 'Advanced'] }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Schema Rendering Process

  1. Schema Parsing: Enforma analyzes the schema structure
  2. Component Resolution: Maps schema types to Vue components
  3. Dynamic Evaluation: Processes expressions and conditional logic
  4. Render Tree Creation: Builds the component tree
  5. Reactive Updates: Updates components when data changes
<template>
  <!-- This single component renders the entire dynamic form -->
  <Enforma
    :schema="dynamicSchema"
    :data="formData"
    :rules="validationRules"
    :context="formContext"
  />
</template>

<script setup>
// Schema can come from anywhere - API, database, user input
const dynamicSchema = await fetchFormSchema('user-profile')

// Data structure matches the schema field names
const formData = {
  firstName: '',
  skills: []
}
</script>
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Building a Dynamic Job Application Form

Let's build a comprehensive job application form that adapts based on the job type and applicant responses:

// This schema could be stored in a database and loaded dynamically
const jobApplicationSchema = {
  // Basic Information Section
  basic_section: {
    type: 'section',
    title: 'Basic Information',
    titleComponent: 'h2'
  },

  firstName: {
    type: 'field',
    section: 'basic_section',
    label: 'First Name',
    required: true,
    rules: 'required|min_length:2'
  },

  lastName: {
    type: 'field',
    section: 'basic_section',
    label: 'Last Name',
    required: true,
    rules: 'required|min_length:2'
  },

  email: {
    type: 'field',
    section: 'basic_section',
    label: 'Email Address',
    required: true,
    rules: 'required|email',
    inputProps: { type: 'email' }
  },

  // Job-specific section that adapts based on job type
  position_section: {
    type: 'section',
    title: 'Position Information',
    titleComponent: 'h2'
  },

  jobType: {
    type: 'field',
    section: 'position_section',
    label: 'Job Type',
    required: true,
    inputComponent: 'select',
    inputProps: {
      options: ['Frontend Developer', 'Backend Developer', 'Full Stack Developer', 'Designer', 'Manager']
    }
  },

  // Technical Skills - Only for developer roles
  technical_section: {
    type: 'section',
    title: 'Technical Skills',
    titleComponent: 'h2',
    if: '${["Frontend Developer", "Backend Developer", "Full Stack Developer"].includes(form.values().jobType)}'
  },

  programmingLanguages: {
    type: 'repeatable_table',
    section: 'technical_section',
    if: '${["Frontend Developer", "Backend Developer", "Full Stack Developer"].includes(form.values().jobType)}',
    subfields: {
      language: {
        label: 'Programming Language',
        rules: 'required',
        inputComponent: 'select',
        inputProps: {
          options: ['JavaScript', 'TypeScript', 'Python', 'Java', 'Go', 'PHP', 'C#']
        }
      },
      experience: {
        label: 'Years of Experience',
        rules: 'required|numeric|gte:0',
        inputComponent: 'number',
        inputProps: { min: 0, max: 20 }
      },
      proficiency: {
        label: 'Proficiency Level',
        rules: 'required',
        inputComponent: 'select',
        inputProps: { options: ['Beginner', 'Intermediate', 'Advanced', 'Expert'] }
      }
    }
  },

  // Frontend-specific fields
  frontendFrameworks: {
    type: 'repeatable',
    section: 'technical_section',
    if: '${["Frontend Developer", "Full Stack Developer"].includes(form.values().jobType)}',
    label: 'Frontend Frameworks',
    subfields: {
      framework: {
        label: 'Framework',
        rules: 'required',
        inputComponent: 'select',
        inputProps: { options: ['Vue.js', 'React', 'Angular', 'Svelte'] }
      },
      projects: {
        label: 'Number of Projects',
        rules: 'required|numeric|gte:1',
        inputComponent: 'number'
      }
    }
  },

  // Backend-specific fields
  backendTechnologies: {
    type: 'repeatable_table',
    section: 'technical_section',
    if: '${["Backend Developer", "Full Stack Developer"].includes(form.values().jobType)}',
    subfields: {
      technology: {
        label: 'Backend Technology',
        rules: 'required',
        inputComponent: 'select',
        inputProps: { options: ['Node.js', 'Express', 'Django', 'Laravel', 'Spring Boot'] }
      },
      experience: {
        label: 'Years',
        rules: 'required|numeric|gte:0',
        inputComponent: 'number'
      }
    }
  },

  // Design Skills - Only for designer role
  design_section: {
    type: 'section',
    title: 'Design Skills',
    titleComponent: 'h2',
    if: '${form.values().jobType === "Designer"}'
  },

  designTools: {
    type: 'repeatable',
    section: 'design_section',
    if: '${form.values().jobType === "Designer"}',
    label: 'Design Tools',
    subfields: {
      tool: {
        label: 'Design Tool',
        rules: 'required',
        inputComponent: 'select',
        inputProps: { options: ['Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator'] }
      },
      proficiency: {
        label: 'Proficiency',
        rules: 'required',
        inputComponent: 'select',
        inputProps: { options: ['Beginner', 'Intermediate', 'Advanced', 'Expert'] }
      }
    }
  },

  portfolio: {
    type: 'field',
    section: 'design_section',
    if: '${form.values().jobType === "Designer"}',
    label: 'Portfolio URL',
    rules: 'required|url',
    inputProps: { placeholder: 'https://your-portfolio.com' }
  },

  // Management Experience - Only for manager role
  management_section: {
    type: 'section',
    title: 'Management Experience',
    titleComponent: 'h2',
    if: '${form.values().jobType === "Manager"}'
  },

  teamSize: {
    type: 'field',
    section: 'management_section',
    if: '${form.values().jobType === "Manager"}',
    label: 'Largest Team Size Managed',
    rules: 'required|integer|gte:1',
    inputComponent: 'number',
    inputProps: { min: 1 }
  },

  managementYears: {
    type: 'field',
    section: 'management_section',
    if: '${form.values().jobType === "Manager"}',
    label: 'Years of Management Experience',
    rules: 'required|numeric|gte:0',
    inputComponent: 'number',
    inputProps: { min: 0 }
  },

  // Experience Section - Always shown
  experience_section: {
    type: 'section',
    title: 'Work Experience',
    titleComponent: 'h2'
  },

  workExperience: {
    type: 'repeatable',
    section: 'experience_section',
    min: 1,
    max: 10,
    subfields: {
      company: {
        label: 'Company',
        rules: 'required'
      },
      position: {
        label: 'Position',
        rules: 'required'
      },
      startDate: {
        label: 'Start Date',
        rules: 'required|date',
        inputComponent: 'datepicker'
      },
      endDate: {
        label: 'End Date',
        rules: 'date|date_after:@workExperience.*.startDate',
        inputComponent: 'datepicker'
      },
      current: {
        label: 'Current Position',
        inputComponent: 'checkbox'
      },
      responsibilities: {
        label: 'Key Responsibilities',
        inputComponent: 'textarea',
        inputProps: { rows: 4 }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the Dynamic Schema

<template>
  <div class="job-application">
    <h1>Job Application Form</h1>

    <Enforma
      :schema="jobApplicationSchema"
      :data="applicationData"
      :context="formContext"
      :submit-handler="submitApplication"
    />
  </div>
</template>

<script setup>
// The entire form is driven by the schema
const applicationData = {
  firstName: '',
  lastName: '',
  email: '',
  jobType: '',
  programmingLanguages: [],
  frontendFrameworks: [],
  backendTechnologies: [],
  designTools: [],
  portfolio: '',
  teamSize: null,
  managementYears: null,
  workExperience: [
    {
      company: '',
      position: '',
      startDate: '',
      endDate: '',
      current: false,
      responsibilities: ''
    }
  ]
}

// Context provides additional data and functions
const formContext = {
  // Could include user permissions, API endpoints, etc.
  userRole: 'applicant',
  availablePositions: ['Frontend Developer', 'Backend Developer']
}

const submitApplication = async (data) => {
  console.log('Submitting application:', data)
  // Submit to your API
  return true
}
</script>
Enter fullscreen mode Exit fullscreen mode

Adding Validation to Dynamic Fields

Schema-Based Validation

Validation rules can be embedded directly in the schema:

const dynamicFormSchema = {
  email: {
    type: 'field',
    label: 'Email',
    rules: 'required|email',
    messages: {
      required: 'Email is required',
      email: 'Please enter a valid email address'
    }
  },

  password: {
    type: 'field',
    label: 'Password',
    inputProps: { type: 'password' },
    rules: 'required|min_length:8|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/',
    messages: {
      required: 'Password is required',
      min_length: 'Password must be at least 8 characters',
      regex: 'Password must contain lowercase, uppercase and number'
    }
  },

  confirmPassword: {
    type: 'field',
    label: 'Confirm Password',
    inputProps: { type: 'password' },
    rules: 'required|same:@password',
    messages: {
      required: 'Please confirm your password',
      same: 'Passwords must match'
    }
  },

  // Array validation
  skills: {
    type: 'repeatable',
    subfields: {
      name: {
        label: 'Skill Name',
        rules: 'required|min_length:2',
        messages: {
          required: 'Skill name is required'
        }
      },
      level: {
        label: 'Skill Level',
        rules: 'required|in_list:beginner,intermediate,advanced',
        inputComponent: 'select',
        inputProps: { options: ['beginner', 'intermediate', 'advanced'] }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Validation Rules

Create validation rules that adapt based on form state:

const adaptiveValidationSchema = {
  userType: {
    type: 'field',
    label: 'User Type',
    inputComponent: 'select',
    inputProps: { options: ['individual', 'business'] },
    rules: 'required'
  },

  // Individual users need personal info
  firstName: {
    type: 'field',
    label: 'First Name',
    if: '${form.values().userType === "individual"}',
    rules: 'required_if:userType,individual'
  },

  lastName: {
    type: 'field',
    label: 'Last Name', 
    if: '${form.values().userType === "individual"}',
    rules: 'required_if:userType,individual'
  },

  // Business users need company info
  companyName: {
    type: 'field',
    label: 'Company Name',
    if: '${form.values().userType === "business"}',
    rules: 'required_if:userType,business'
  },

  taxId: {
    type: 'field',
    label: 'Tax ID',
    if: '${form.values().userType === "business"}',
    rules: 'required_if:userType,business|regex:/^[0-9]{2}-[0-9]{7}$/',
    messages: {
      regex: 'Tax ID must be in format XX-XXXXXXX'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling Schema Changes Dynamically

Live Schema Updates

Update schemas in real-time based on user actions or external events:

<template>
  <div>
    <!-- Admin controls -->
    <div class="admin-panel" v-if="isAdmin">
      <h3>Form Configuration</h3>
      <button @click="addField">Add Field</button>
      <button @click="toggleSection('skills')">Toggle Skills Section</button>
    </div>

    <!-- Dynamic form that updates live -->
    <Enforma
      :schema="liveSchema"
      :data="formData"
      :key="schemaVersion" 
    />
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const schemaVersion = ref(0)
const liveSchema = reactive({
  name: {
    type: 'field',
    label: 'Name',
    required: true
  }
})

const addField = () => {
  const fieldName = `dynamicField${Date.now()}`
  liveSchema[fieldName] = {
    type: 'field',
    label: 'Dynamic Field',
    required: false
  }
  schemaVersion.value++
}

const toggleSection = (sectionName) => {
  if (liveSchema[sectionName]) {
    delete liveSchema[sectionName]
  } else {
    liveSchema[sectionName] = {
      type: 'section',
      title: 'Skills Section'
    }
  }
  schemaVersion.value++
}
</script>
Enter fullscreen mode Exit fullscreen mode

Server-Driven Schema Updates

Load and update schemas from your backend:

// API-driven schema management
class DynamicFormManager {
  constructor() {
    this.schema = reactive({})
    this.formData = reactive({})
  }

  async loadSchema(formId) {
    try {
      const response = await fetch(`/api/forms/${formId}/schema`)
      const schemaData = await response.json()

      // Replace current schema
      Object.assign(this.schema, schemaData.schema)
      Object.assign(this.formData, schemaData.defaultData)

      return true
    } catch (error) {
      console.error('Failed to load schema:', error)
      return false
    }
  }

  async updateField(fieldName, fieldConfig) {
    // Update locally first for immediate feedback
    this.schema[fieldName] = fieldConfig

    // Sync with server
    try {
      await fetch(`/api/forms/schema/field`, {
        method: 'PATCH',
        body: JSON.stringify({ fieldName, fieldConfig })
      })
    } catch (error) {
      console.error('Failed to sync field update:', error)
    }
  }

  async addConditionalField(condition, fieldConfig) {
    const fieldName = `conditional_${Date.now()}`

    this.schema[fieldName] = {
      ...fieldConfig,
      if: condition
    }

    // Sync with server
    await this.syncSchema()
  }

  async syncSchema() {
    await fetch('/api/forms/schema', {
      method: 'PUT',
      body: JSON.stringify({ schema: this.schema })
    })
  }
}

// Usage
const formManager = new DynamicFormManager()
await formManager.loadSchema('user-registration')
Enter fullscreen mode Exit fullscreen mode

Schema Versioning and Migration

Handle schema changes gracefully:

class SchemaVersionManager {
  constructor() {
    this.migrations = {
      '1.0.0': this.migrateToV1,
      '1.1.0': this.migrateToV1_1,
      '2.0.0': this.migrateToV2
    }
  }

  async loadSchema(formId) {
    const schemaData = await fetch(`/api/forms/${formId}`).then(r => r.json())

    // Check if migration is needed
    if (schemaData.version !== this.currentVersion) {
      return this.migrateSchema(schemaData)
    }

    return schemaData.schema
  }

  migrateSchema(schemaData) {
    let { schema, version } = schemaData

    // Apply migrations in order
    for (const [targetVersion, migrationFn] of Object.entries(this.migrations)) {
      if (this.isVersionNewer(targetVersion, version)) {
        schema = migrationFn(schema)
        version = targetVersion
      }
    }

    return schema
  }

  migrateToV1_1(schema) {
    // Example: Convert old 'type' field to new 'inputComponent'
    Object.keys(schema).forEach(key => {
      const field = schema[key]
      if (field.type === 'field' && field.fieldType) {
        field.inputComponent = field.fieldType
        delete field.fieldType
      }
    })
    return schema
  }

  migrateToV2(schema) {
    // Example: Restructure validation rules
    Object.keys(schema).forEach(key => {
      const field = schema[key]
      if (field.validation) {
        field.rules = field.validation.join('|')
        delete field.validation
      }
    })
    return schema
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Ideas: Plugin-Based Form Extensions

Custom Field Types

Extend Enforma with your own field types:

// Register custom field type
const customFieldTypes = {
  'location-picker': {
    component: LocationPickerField,
    defaultProps: {
      apiKey: process.env.GOOGLE_MAPS_API_KEY
    }
  },
  'rich-text-editor': {
    component: RichTextEditor,
    defaultProps: {
      toolbar: ['bold', 'italic', 'link']
    }
  },
  'signature-pad': {
    component: SignaturePad,
    defaultProps: {
      width: 400,
      height: 200
    }
  }
}

// Use in schema
const extendedSchema = {
  address: {
    type: 'field',
    label: 'Your Location',
    inputComponent: 'location-picker',
    inputProps: {
      defaultLocation: 'San Francisco, CA'
    }
  },

  description: {
    type: 'field',
    label: 'Description',
    inputComponent: 'rich-text-editor',
    inputProps: {
      minLength: 100
    }
  },

  signature: {
    type: 'field',
    label: 'Digital Signature',
    inputComponent: 'signature-pad',
    required: true
  }
}
Enter fullscreen mode Exit fullscreen mode

Schema Plugins System

Create a plugin system for schema transformations:

class SchemaPluginSystem {
  constructor() {
    this.plugins = []
  }

  addPlugin(plugin) {
    this.plugins.push(plugin)
  }

  processSchema(schema, context) {
    return this.plugins.reduce((processedSchema, plugin) => {
      return plugin.transform(processedSchema, context)
    }, schema)
  }
}

// Example plugins
const conditionalFieldsPlugin = {
  name: 'conditional-fields',
  transform(schema, context) {
    // Add conditional logic based on user permissions
    if (!context.user.canEditAdvanced) {
      Object.keys(schema).forEach(key => {
        if (schema[key].advanced) {
          delete schema[key]
        }
      })
    }
    return schema
  }
}

const localizationPlugin = {
  name: 'localization',
  transform(schema, context) {
    // Translate labels based on user locale
    const locale = context.user.locale || 'en'

    Object.keys(schema).forEach(key => {
      const field = schema[key]
      if (field.label && field.translations) {
        field.label = field.translations[locale] || field.label
      }
    })
    return schema
  }
}

// Usage
const pluginSystem = new SchemaPluginSystem()
pluginSystem.addPlugin(conditionalFieldsPlugin)
pluginSystem.addPlugin(localizationPlugin)

const processedSchema = pluginSystem.processSchema(originalSchema, {
  user: { canEditAdvanced: false, locale: 'es' }
})
Enter fullscreen mode Exit fullscreen mode

Complete Example: Multi-Tenant Survey Platform

Here's a comprehensive example showing all concepts together:

<template>
  <div class="survey-platform">
    <div class="survey-header">
      <h1>{{ surveyTitle }}</h1>
      <p>{{ surveyDescription }}</p>
    </div>

    <!-- Dynamic form renders based on loaded schema -->
    <Enforma
      :schema="processedSchema"
      :data="responseData"
      :context="surveyContext"
      :submit-handler="submitSurvey"
      @field-change="handleFieldChange"
    >
      <!-- Custom slot for special field types -->
      <template #field(rating)="{ field }">
        <StarRating 
          :value="field.value"
          :max="field.inputProps.max || 5"
          @input="field.events.input"
        />
      </template>
    </Enforma>

    <!-- Progress indicator -->
    <div class="survey-progress">
      <div class="progress-bar">
        <div 
          class="progress-fill"
          :style="{ width: `${progressPercent}%` }"
        ></div>
      </div>
      <span>{{ progressPercent }}% Complete</span>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'

const props = defineProps({
  surveyId: { type: String, required: true },
  userId: { type: String, required: true }
})

// Reactive state
const surveyTitle = ref('')
const surveyDescription = ref('')
const originalSchema = reactive({})
const responseData = reactive({})
const processedSchema = computed(() => processSchemaWithLogic(originalSchema))

// Survey context for dynamic expressions
const surveyContext = reactive({
  userId: props.userId,
  startTime: Date.now(),

  // Helper functions for schema expressions
  calculateAge(birthYear) {
    return new Date().getFullYear() - birthYear
  },

  hasAnswered(fieldName) {
    return !!responseData[fieldName]
  },

  getConditionalQuestions(category) {
    // Load additional questions based on previous answers
    return this.questionBank[category] || []
  }
})

// Progress calculation
const progressPercent = computed(() => {
  const totalFields = Object.keys(processedSchema.value).filter(
    key => processedSchema.value[key].type === 'field'
  ).length

  const answeredFields = Object.values(responseData).filter(
    value => value !== null && value !== '' && value !== undefined
  ).length

  return totalFields > 0 ? Math.round((answeredFields / totalFields) * 100) : 0
})

// Load survey configuration
onMounted(async () => {
  try {
    const surveyData = await fetch(`/api/surveys/${props.surveyId}`).then(r => r.json())

    surveyTitle.value = surveyData.title
    surveyDescription.value = surveyData.description

    // Process schema with tenant-specific rules
    Object.assign(originalSchema, surveyData.schema)

    // Initialize response data
    initializeResponseData(surveyData.schema)

  } catch (error) {
    console.error('Failed to load survey:', error)
  }
})

// Initialize form data based on schema
const initializeResponseData = (schema) => {
  Object.keys(schema).forEach(key => {
    const field = schema[key]
    if (field.type === 'field') {
      responseData[key] = field.defaultValue || null
    } else if (field.type === 'repeatable') {
      responseData[key] = []
    }
  })
}

// Process schema with conditional logic
const processSchemaWithLogic = (schema) => {
  const processed = { ...schema }

  // Add conditional fields based on previous answers
  if (responseData.experienceLevel === 'advanced') {
    processed.advancedQuestions = {
      type: 'section',
      title: 'Advanced Questions'
    }

    processed.technicalChallenge = {
      type: 'field',
      section: 'advancedQuestions',
      label: 'Describe your biggest technical challenge',
      inputComponent: 'textarea',
      required: true
    }
  }

  return processed
}

// Handle field changes for dynamic behavior
const handleFieldChange = ({ fieldName, value }) => {
  console.log(`Field ${fieldName} changed to:`, value)

  // Trigger conditional logic
  if (fieldName === 'country') {
    updateStateOptions(value)
  }

  if (fieldName === 'age' && value > 65) {
    addSeniorDiscountField()
  }
}

// Dynamic form modifications
const updateStateOptions = (country) => {
  if (originalSchema.state) {
    const stateOptions = getStatesByCountry(country)
    originalSchema.state.inputProps.options = stateOptions
  }
}

const addSeniorDiscountField = () => {
  if (!originalSchema.seniorDiscount) {
    originalSchema.seniorDiscount = {
      type: 'field',
      label: 'Apply Senior Discount',
      inputComponent: 'checkbox',
      defaultValue: true
    }
    responseData.seniorDiscount = true
  }
}

// Submit survey response
const submitSurvey = async (data) => {
  try {
    const response = await fetch(`/api/surveys/${props.surveyId}/responses`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId: props.userId,
        responses: data,
        completedAt: new Date().toISOString(),
        timeTaken: Date.now() - surveyContext.startTime
      })
    })

    if (response.ok) {
      alert('Survey submitted successfully!')
      return true
    } else {
      throw new Error('Submission failed')
    }
  } catch (error) {
    console.error('Survey submission error:', error)
    alert('Failed to submit survey. Please try again.')
    return false
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Final Thoughts: The Future of Form Development

Dynamic forms represent a fundamental shift in how we approach form development. Instead of writing components for every possible form variation, we define flexible schemas that can adapt to any requirement.

The Benefits You Gain

✓ Rapid Development: Create complex forms in minutes, not hours

✓ Non-Technical Empowerment: Allow business users to modify forms

✓ Consistent UI: Maintain design system compliance automatically

✓ Easy Maintenance: Update forms without code deployment

✓ Enhanced Flexibility: Adapt forms based on any criteria

✓ Better Testing: Test form logic separately from presentation

Best Practices for Dynamic Forms

  1. Start Simple: Begin with basic schemas and add complexity gradually
  2. Plan Your Schema Structure: Design schemas that are intuitive and maintainable
  3. Use Validation Liberally: Embed validation rules in schemas for consistency
  4. Implement Graceful Fallbacks: Handle schema loading errors elegantly
  5. Version Your Schemas: Plan for schema evolution and migration
  6. Test Thoroughly: Validate schemas in different contexts and configurations

Advanced Extensions

The possibilities with dynamic forms are endless:

  • AI-Generated Forms: Use machine learning to generate optimal form structures
  • A/B Testing Integration: Automatically test different form configurations
  • Analytics Integration: Track form performance and optimize based on data
  • Multi-Language Support: Dynamically translate forms based on user locale
  • Accessibility Enhancements: Automatically apply accessibility improvements

Ready to Transform Your Forms?

Dynamic forms with Enforma eliminate the traditional boundaries between developers and business requirements. Whether you're building admin panels, survey platforms, or complex data entry systems, schema-driven forms provide the flexibility and power you need.

Start building your first dynamic form today:

npm install @encolajs/enforma
Enter fullscreen mode Exit fullscreen mode

Explore Dynamic Form Examples →

The future of form development is dynamic, flexible, and powerful. With Enforma's schema-driven architecture, that future is available today.

Comments 0 total

    Add comment