Skip to content

Form Component Overview

The <form-component> is a dynamic form renderer that creates forms based on a spec schema. It supports validation, nested fields, dynamic updates, and external control through browser hooks.

The component is registered as a Custom Element:

import { defineCustomElement } from 'vue';
import FormCe from '@/form/view/Form.ce.vue';
import { useCore } from './store/core';
// Expose hooks to browser context
window.$useFormCore = useCore();
// Register custom element
customElements.define('form-component', defineCustomElement(FormCe));

  • Spec-Driven: Forms are defined by JSON schemas
  • Validation: Built-in validation using Yup schemas
  • Nested Fields: Support for collections, arrays, and dynamic nested structures
  • Theming: Light and dark mode support
  • External Control: Hooks for subscribing to changes and modifying form structure
  • Action Buttons: Custom actions on fields (e.g., “Add Connection”)

<script setup lang="ts">
import { ref } from 'vue';
const formId = 'contact-form';
const formSpec = {
type: 'collection',
name: 'contact',
spec: [
{
type: 'text',
name: 'firstName',
label: 'First Name',
required: true,
},
{
type: 'text',
name: 'lastName',
label: 'Last Name',
required: true,
},
{
type: 'email',
name: 'email',
label: 'Email Address',
required: true,
},
{
type: 'text',
name: 'message',
label: 'Message',
multiline: true,
},
],
};
const initialValues = ref({
firstName: '',
lastName: '',
email: '',
message: '',
});
function handleSubmit(data: unknown) {
console.log('Form submitted:', data);
}
function handleChange(data: unknown) {
console.log('Form changed:', data);
}
</script>
<template>
<form-component
:id="formId"
theme="light"
:spec="formSpec"
:values="initialValues"
submit-button-text="Send Message"
@submit="handleSubmit"
@change="handleChange"
/>
</template>

Selects can define nested fields that appear when an option is selected:

const spec = {
type: 'collection',
spec: [
{
type: 'select',
name: 'paymentMethod',
label: 'Payment Method',
options: [
{
label: 'Credit Card',
value: 'credit_card',
nested: [
{ type: 'text', name: 'cardNumber', label: 'Card Number', required: true },
{ type: 'text', name: 'expiry', label: 'MM/YY', required: true },
{ type: 'text', name: 'cvv', label: 'CVV', required: true },
],
},
{
label: 'PayPal',
value: 'paypal',
nested: [
{ type: 'email', name: 'paypalEmail', label: 'PayPal Email', required: true },
],
},
{
label: 'Bank Transfer',
value: 'bank',
// No nested fields
},
],
},
],
};

Create repeatable field groups:

const spec = {
type: 'collection',
spec: [
{
type: 'array',
name: 'contacts',
label: 'Emergency Contacts',
spec: {
type: 'collection',
spec: [
{ type: 'text', name: 'name', label: 'Name', required: true },
{ type: 'tel', name: 'phone', label: 'Phone', required: true },
{ type: 'text', name: 'relationship', label: 'Relationship' },
],
},
},
],
};

Fields can have action buttons for triggering external behavior:

const spec = {
type: 'collection',
spec: [
{
type: 'select',
name: 'integration',
label: 'Integration',
options: [],
action: {
label: 'Add New',
value: 'add.integration',
},
},
],
};
// Listen for the action
form.addEventListener('action', (e) => {
const [action, value] = e.detail;
if (action.value === 'add.integration') {
// Open integration creation dialog
openIntegrationDialog();
}
});

Fields can be marked as “advanced” and hidden by default:

const spec = {
type: 'collection',
spec: [
{ type: 'text', name: 'name', label: 'Name', required: true },
{ type: 'text', name: 'description', label: 'Description' },
// Advanced options (hidden by default)
{ type: 'number', name: 'timeout', label: 'Timeout (ms)', advanced: true },
{ type: 'boolean', name: 'debug', label: 'Debug Mode', advanced: true },
],
};

src/form/
├── form.ts # Custom element registration
├── store/
│ ├── core.ts # Browser hooks (window.$useFormCore)
│ └── workspace.ts # Form instance management
├── view/
│ └── Form.ce.vue # Main component
└── types/
└── index.ts # Type definitions