FormBuilder Internals
FormBuilder Internals
Section titled “FormBuilder Internals”The FormBuilder is the core component that transforms spec definitions into rendered forms. Understanding its internals helps when debugging, extending, or optimizing form behavior.
Architecture Overview
Section titled “Architecture Overview”┌─────────────────────────────────────────────────────────┐│ Form.ce.vue ││ (Custom Element - entry point) │├─────────────────────────────────────────────────────────┤│ FormBuilder.vue ││ (Form layout, validation, submission) │├─────────────────────────────────────────────────────────┤│ FieldIterator.vue ││ (Recursively renders fields based on type) │├─────────────────────────────────────────────────────────┤│ FieldText │ FieldSelect │ FieldNumber │ FieldArray ││ (Individual field components) │└─────────────────────────────────────────────────────────┘Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
src/ui/FormBuilder/FormBuilder.vue | Main form component |
src/ui/FormBuilder/FieldIterator.vue | Recursive field renderer |
src/ui/FormBuilder/lib/parser.ts | Spec parsing and validation |
src/form/store/workspace.ts | Form instance management |
src/ui/types.ts | TypeScript type definitions |
Parser Functions
Section titled “Parser Functions”parseDefaults(spec, withNested?)
Section titled “parseDefaults(spec, withNested?)”Extracts default values from a spec definition.
function parseDefaults(field: Field, withNested = false): Record<string, any>What it does:
- Recursively traverses the spec
- Collects
defaultvalues from each field - Creates a nested object matching the spec structure
Example:
const spec = { type: 'collection', spec: [ { type: 'text', name: 'name', default: 'John' }, { type: 'number', name: 'age', default: 25 }, { type: 'collection', name: 'settings', spec: [ { type: 'boolean', name: 'notifications', default: true }, ], }, ],};
const defaults = parseDefaults(spec);// {// name: 'John',// age: 25,// settings: {// notifications: true// }// }generateSchema(spec, isRoot?)
Section titled “generateSchema(spec, isRoot?)”Creates a Yup validation schema from a spec.
function generateSchema( field: Field, isRoot = false): Yup.SchemaWhat it does:
- Recursively processes each field
- Applies validation rules based on field type and properties
- Returns a composed Yup schema
Field Type to Yup Mapping:
| Field Type | Yup Type | Validations Applied |
|---|---|---|
text | Yup.string() | .required() if required |
email | Yup.string() | .email(), .required() |
number | Yup.number() | .min(), .max(), .required() |
boolean | Yup.boolean() | .required() |
select | Yup.mixed() | .required(), .oneOf() |
collection | Yup.object() | Nested validation |
array | Yup.array() | Item validation |
Example:
const spec = { type: 'collection', spec: [ { type: 'email', name: 'email', required: true }, { type: 'number', name: 'age', min: 18, max: 120 }, ],};
const schema = generateSchema(spec, true);// Equivalent to:// Yup.object({// email: Yup.string().email().required(),// age: Yup.number().min(18).max(120),// })Field Rendering
Section titled “Field Rendering”FieldIterator
Section titled “FieldIterator”The FieldIterator component recursively renders fields:
<!-- FieldIterator.vue (simplified) --><template> <template v-for="field in fields" :key="field.name"> <!-- Skip hidden fields --> <template v-if="!field.hidden">
<!-- Collection: render children --> <div v-if="field.type === 'collection'"> <FieldIterator :fields="field.spec" :path="getPath(field)" /> </div>
<!-- Array: render add/remove UI --> <FieldArrayComponent v-else-if="field.type === 'array'" />
<!-- Select: render dropdown --> <FieldSelectComponent v-else-if="field.type === 'select'" />
<!-- Text/Number/etc: render input --> <FieldInputComponent v-else />
</template> </template></template>Workspace Store
Section titled “Workspace Store”The useFormWorkspace composable manages form instances:
Form Instance Structure
Section titled “Form Instance Structure”interface FormInstance { spec: Field; // Current spec definition data: Record<string, any>; // Current form values initialValues: Record<string, any>; // Original values}Key Functions
Section titled “Key Functions”initForm(id, config)
Section titled “initForm(id, config)”Creates or updates a form instance:
function initForm(id: string, config: { spec: Field; data: Record<string, any> }): voidupdateData(id, path, value)
Section titled “updateData(id, path, value)”Updates a specific field value:
function updateData(id: string, path: string, value: any): voidsetSpecInPath(id, path, newSpec)
Section titled “setSpecInPath(id, path, newSpec)”Dynamically replaces a field’s spec:
function setSpecInPath(id: string, path: string, newSpec: Field): voidHow it works:
- Parses the dot-notation path (
"user.address.city") - Recursively traverses the spec tree
- Replaces the target field with the new spec
- Updates corresponding initial values
Validation Flow
Section titled “Validation Flow”┌──────────────────────────────────────────────────────────┐│ 1. User clicks Submit │├──────────────────────────────────────────────────────────┤│ 2. FormBuilder.onSubmit() called │├──────────────────────────────────────────────────────────┤│ 3. ValidateSpecialInputError() ││ - Checks custom input validations │├──────────────────────────────────────────────────────────┤│ 4. VeeValidate validate() ││ - Runs Yup schema against form data │├──────────────────────────────────────────────────────────┤│ 5. Check results ││ ├─ Has errors → emit('error', errors) ││ └─ No errors → emit('submit', data, meta) │└──────────────────────────────────────────────────────────┘Advanced Topics
Section titled “Advanced Topics”Nested Field Resolution
Section titled “Nested Field Resolution”When a select has nested options, the FormBuilder:
- Watches for value changes on the select
- Finds the matching option
- Injects the nested fields into the form
- Manages the nested field values
// Option with nested fields{ label: 'Credit Card', value: 'credit_card', nested: [ { type: 'text', name: 'cardNumber' }, { type: 'text', name: 'expiry' }, ],}
// When selected, form structure becomes:{ paymentMethod: 'credit_card', cardNumber: '', expiry: '',}Template Support
Section titled “Template Support”Fields can use template for computed values:
{ type: 'text', name: 'fullName', template: '{{firstName}} {{lastName}}', readonly: true,}The FormBuilder evaluates templates using the current form data.
Extending FormBuilder
Section titled “Extending FormBuilder”Adding a Custom Field Type
Section titled “Adding a Custom Field Type”- Create the field component:
<template> <input type="color" :value="modelValue" @input="emit('update:modelValue', $event.target.value)" /></template>
<script setup>defineProps(['modelValue']);defineEmits(['update:modelValue']);</script>- Register in FieldIterator:
<FieldColor v-else-if="field.type === 'color'" v-model="formData[field.name]"/>- Add type to the TypeScript definition:
type FieldColor = FieldBase & { type: 'color'; presets?: string[];};
type Field = /* existing types */ | FieldColor;- Add Yup validation in parser:
case 'color': return Yup.string().matches(/^#[0-9a-f]{6}$/i, 'Invalid color');Performance Considerations
Section titled “Performance Considerations”-
Reactive spec: Changes to spec trigger full re-render. Use
setSpecInPathfor targeted updates. -
Large arrays: Array fields with many items can slow rendering. Consider pagination.
-
Debounced changes: The
onChangeevent is debounced to prevent excessive updates. -
Schema caching: Generated Yup schemas are cached per form instance.