The Spec Paradigm
The Spec Paradigm
Section titled “The Spec Paradigm”The Spec Paradigm is the foundational concept behind ion-components’ dynamic forms. A “spec” is a JSON schema that defines the structure, types, validation, and behavior of form fields.
What is a Spec?
Section titled “What is a Spec?”A spec is a hierarchical structure that describes:
- Field types (text, select, collection, array, etc.)
- Validation rules (required, min/max, patterns)
- Labels and placeholders
- Default values
- Nested structures (collections within collections)
- Dynamic behavior (actions, conditional fields)
Basic Structure
Section titled “Basic Structure”Every spec has a type property that determines its behavior:
// Root spec - always a collectionconst spec = { type: 'collection', name: 'form', spec: [ // Field definitions here ],};Field Types
Section titled “Field Types”Text Input
Section titled “Text Input”Simple text input field.
{ type: 'text', name: 'firstName', label: 'First Name', placeholder: 'Enter your first name', required: true,}Email Input
Section titled “Email Input”Text field with email validation.
{ type: 'email', name: 'email', label: 'Email Address', required: true,}Password Input
Section titled “Password Input”Masked text input for passwords.
{ type: 'password', name: 'password', label: 'Password', required: true,}Number Input
Section titled “Number Input”Numeric input with optional min/max.
{ type: 'number', name: 'quantity', label: 'Quantity', min: 1, max: 100, default: 1,}Boolean / Switch
Section titled “Boolean / Switch”Toggle switch or checkbox.
{ type: 'boolean', name: 'subscribe', label: 'Subscribe to newsletter', default: false,}Select Dropdown
Section titled “Select Dropdown”Single selection from options.
{ type: 'select', name: 'country', label: 'Country', options: [ { label: 'United States', value: 'us' }, { label: 'Canada', value: 'ca' }, { label: 'Mexico', value: 'mx' }, ], required: true,}Multi-Select
Section titled “Multi-Select”Multiple selection from options.
{ type: 'select', name: 'skills', label: 'Skills', multiple: true, options: [ { label: 'JavaScript', value: 'js' }, { label: 'Python', value: 'py' }, { label: 'Go', value: 'go' }, { label: 'Rust', value: 'rust' }, ],}Textarea
Section titled “Textarea”Multi-line text input.
{ type: 'text', name: 'description', label: 'Description', multiline: true,}URL Input
Section titled “URL Input”Text field with URL validation.
{ type: 'url', name: 'website', label: 'Website URL', placeholder: 'https://example.com',}Date/DateTime
Section titled “Date/DateTime”Date and datetime pickers.
{ type: 'date', name: 'birthDate', label: 'Birth Date',}
{ type: 'datetime-local', name: 'eventTime', label: 'Event Date & Time',}Complex Types
Section titled “Complex Types”Collection
Section titled “Collection”Groups multiple fields together. The root spec is always a collection.
{ type: 'collection', name: 'address', label: 'Address', spec: [ { type: 'text', name: 'street', label: 'Street' }, { type: 'text', name: 'city', label: 'City' }, { type: 'text', name: 'state', label: 'State' }, { type: 'text', name: 'zip', label: 'ZIP Code' }, ],}Resulting data structure:
{ "address": { "street": "123 Main St", "city": "New York", "state": "NY", "zip": "10001" }}Repeatable group of fields (list).
{ 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' }, ], },}Resulting data structure:
{ "contacts": [ { "name": "John Doe", "phone": "555-1234", "relationship": "Father" }, { "name": "Jane Doe", "phone": "555-5678", "relationship": "Spouse" } ]}FieldBase Properties
Section titled “FieldBase Properties”All field types inherit these base properties:
interface FieldBase { id?: string; // Unique identifier icon?: string; // Icon to display name?: string | null; // Field name (key in data) label?: string; // Display label required?: boolean; // Whether field is required action?: Item; // Action button configuration template?: string; // Template for display readonly?: boolean; // Read-only mode hidden?: boolean; // Hide the field advanced?: boolean; // Show only in "advanced" mode}Field Type Reference
Section titled “Field Type Reference”type Field = | FieldCollection | FieldArray | FieldInput | FieldText | FieldNumber | FieldSelect;
// Collection of fieldstype FieldCollection = FieldBase & { type: 'collection'; spec: Array<Field>;};
// Repeatable arraytype FieldArray = FieldBase & { type: 'array'; spec: Field;};
// Text-based inputstype FieldText = FieldBase & { type: | 'text' | 'password' | 'email' | 'url' | 'tel' | 'binary' | 'date' | 'datetime-local'; multiline?: boolean; placeholder?: string;};
// Numeric inputtype FieldNumber = FieldBase & { type: 'number'; min?: number; max?: number;};
// Select dropdowntype FieldSelect = FieldBase & { type: 'select'; options?: Item[] | SelectPlaceholderOptions | SelectNestedOptions; multiple?: boolean; searchable?: boolean; placeholder?: string;};Select Options
Section titled “Select Options”Simple Options
Section titled “Simple Options”{ type: 'select', name: 'status', options: [ { label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }, { label: 'Pending', value: 'pending' }, ],}Options with Descriptions
Section titled “Options with Descriptions”{ type: 'select', name: 'plan', options: [ { label: 'Basic', value: 'basic', description: '$9/month - 1 user, 1GB storage', }, { label: 'Pro', value: 'pro', description: '$29/month - 5 users, 10GB storage', }, { label: 'Enterprise', value: 'enterprise', description: 'Custom pricing - unlimited', }, ],}Options with Icons
Section titled “Options with Icons”{ type: 'select', name: 'integration', options: [ { label: 'Shopify', value: 'shopify', icon: 'shopify' }, { label: 'WooCommerce', value: 'woo', icon: 'woo' }, { label: 'Magento', value: 'magento', icon: 'magento' }, ],}Nested Options (Dynamic Fields)
Section titled “Nested Options (Dynamic Fields)”Select options can define nested fields that appear when selected:
{ 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: 'Expiry (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 - nothing extra appears }, ],}Actions
Section titled “Actions”Fields can have action buttons for triggering external behavior:
{ type: 'select', name: 'connection', label: 'API Connection', options: [], // Will be populated dynamically action: { label: 'Add Connection', value: 'add.connection', icon: 'plus', },}The action triggers an event that the parent can handle:
<form-component @action="handleAction" />
<script setup>function handleAction(action, value) { if (action.value === 'add.connection') { openConnectionDialog(); }}</script>Advanced Fields
Section titled “Advanced Fields”Fields can be marked as “advanced” to hide them by default:
const spec = { type: 'collection', spec: [ { type: 'text', name: 'name', label: 'Name', required: true }, { type: 'url', name: 'url', label: 'URL', required: true }, // Advanced options - hidden by default { type: 'number', name: 'timeout', label: 'Timeout (ms)', advanced: true }, { type: 'number', name: 'retries', label: 'Max Retries', advanced: true }, { type: 'boolean', name: 'debug', label: 'Debug Mode', advanced: true }, ],};The FormBuilder automatically shows a toggle to reveal advanced options when any field has advanced: true.
Complete Example
Section titled “Complete Example”const userProfileSpec = { type: 'collection', name: 'profile', spec: [ // Basic Info { type: 'text', name: 'firstName', label: 'First Name', required: true }, { type: 'text', name: 'lastName', label: 'Last Name', required: true }, { type: 'email', name: 'email', label: 'Email', required: true }, { type: 'tel', name: 'phone', label: 'Phone' },
// Address (nested collection) { type: 'collection', name: 'address', label: 'Address', spec: [ { type: 'text', name: 'street', label: 'Street Address' }, { type: 'text', name: 'city', label: 'City' }, { type: 'text', name: 'state', label: 'State' }, { type: 'text', name: 'zip', label: 'ZIP Code' }, { type: 'select', name: 'country', label: 'Country', options: [ { label: 'United States', value: 'US' }, { label: 'Canada', value: 'CA' }, ], }, ], },
// Preferences { type: 'select', name: 'language', label: 'Preferred Language', options: [ { label: 'English', value: 'en' }, { label: 'Spanish', value: 'es' }, { label: 'French', value: 'fr' }, ], }, { type: 'boolean', name: 'newsletter', label: 'Subscribe to Newsletter' },
// Advanced { type: 'text', name: 'timezone', label: 'Timezone', advanced: true }, { type: 'boolean', name: 'betaFeatures', label: 'Enable Beta Features', advanced: true }, ],};
// Resulting data structure after form submission:const userData = { firstName: 'John', lastName: 'Doe', phone: '555-1234', address: { street: '123 Main St', city: 'New York', state: 'NY', zip: '10001', country: 'US', }, language: 'en', newsletter: true, timezone: 'America/New_York', betaFeatures: false,};