Refactor Form Components and Enhance JavaScript Execution

- Removed unnecessary event propagation handling in ComponentPreview.vue for cleaner interaction.
- Adjusted FormBuilderConfiguration.vue to ensure proper min attribute placement for maxFiles input.
- Updated FormScriptEngine.vue to allow global CSS injection and added new notification helper functions for success, error, and info messages.
- Enhanced FormTemplatesModal.vue with a new CSS & JavaScript test form template, demonstrating real-time calculations and dynamic field updates.
- Improved documentation to include a comprehensive JavaScript API reference and updated user guides for better clarity on dynamic form functionalities.
- Added new fields to the JSON schema for custom scripts and CSS, enhancing form configuration capabilities.
- Updated formBuilder.js to include a method for updating preview form data, improving state management during form interactions.
This commit is contained in:
Afiq 2025-05-27 11:11:10 +08:00
parent 33982fb093
commit 72c61184ae
12 changed files with 2429 additions and 196 deletions

View File

@ -47,9 +47,6 @@
:multiple="component.props.multiple || undefined"
:maxSize="component.props.maxSize || undefined"
:maxFiles="component.props.maxFiles || undefined"
:preserve-events="isPreview"
@input.capture.stop="isPreview ? $event.stopPropagation() : null"
@click.capture.stop="isPreview ? $event.stopPropagation() : null"
:classes="component.type === 'checkbox' ? {
wrapper: 'mb-1',
options: 'space-y-0.5'
@ -124,7 +121,7 @@
</div>
</div>
</div>
<!-- Divider -->
<div v-else-if="component.type === 'divider'" class="py-4">
<hr class="border-gray-200" />
@ -349,7 +346,7 @@
</div>
<!-- Unknown Component Type Fallback -->
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>

View File

@ -446,8 +446,8 @@
name="maxFiles"
v-model="configModel.maxFiles"
help="Maximum number of files"
min="1"
/>
min="1"
/>
</div>
<div class="flex items-center">

View File

@ -1,7 +1,7 @@
<template>
<div>
<!-- Custom CSS Injection -->
<component :is="'style'" v-if="customCSS" scoped>
<!-- Custom CSS Injection - Remove scoped to allow global styling -->
<component :is="'style'" v-if="customCSS">
{{ customCSS }}
</component>
</div>
@ -44,16 +44,31 @@ const emit = defineEmits(['field-change', 'form-submit', 'field-validate']);
const scriptContext = ref(null);
const fieldChangeHandlers = ref(new Map());
const isScriptInitialized = ref(false);
const previousFormData = ref({});
// Create safe execution context
const createScriptContext = () => {
const context = {
// Form field interaction methods
getField: (fieldName) => {
return props.formData[fieldName];
},
setField: (fieldName, value) => {
// Try to find the FormKit input element and update it directly
const fieldElement = document.querySelector(`[data-name="${fieldName}"] input, [data-name="${fieldName}"] select, [data-name="${fieldName}"] textarea`);
if (fieldElement) {
fieldElement.value = value;
// Trigger input event to notify FormKit of the change
const inputEvent = new Event('input', { bubbles: true });
fieldElement.dispatchEvent(inputEvent);
// Also trigger change event
const changeEvent = new Event('change', { bubbles: true });
fieldElement.dispatchEvent(changeEvent);
}
// Also emit the event for the parent component
emit('field-change', { fieldName, value });
},
@ -107,6 +122,46 @@ const createScriptContext = () => {
});
},
// Add missing helper functions
showSuccess: (message) => {
// Create a simple success notification
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg z-50';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 3000);
},
showError: (message) => {
// Create a simple error notification
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded shadow-lg z-50';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 5000);
},
showInfo: (message) => {
// Create a simple info notification
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-blue-500 text-white px-4 py-2 rounded shadow-lg z-50';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 3000);
},
// Utility functions
console: {
log: (...args) => console.log('[Form Script]', ...args),
@ -134,7 +189,13 @@ const createScriptContext = () => {
String: String,
// Number utilities
Number: Number
Number: Number,
// Additional utility functions
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearInterval: clearInterval
};
return context;
@ -183,20 +244,26 @@ const executeScript = (script, context) => {
scriptFunction.call(null, context);
}
} catch (error) {
console.error('Script execution error:', error);
// Could emit an error event here for user feedback
console.error('[FormScriptEngine] Script execution error:', error);
// Show error to user if showError function is available
if (context.showError) {
context.showError(`Script Error: ${error.message}`);
}
}
};
// Initialize script engine
const initializeScript = async () => {
if (!props.customScript || isScriptInitialized.value) return;
await nextTick(); // Ensure DOM is ready
const initializeScript = () => {
fieldChangeHandlers.value.clear();
// Create script context
scriptContext.value = createScriptContext();
if (props.formEvents.onLoad) {
// Store initial form data
previousFormData.value = { ...props.formData };
// Execute onLoad script if enabled
if (props.formEvents.onLoad && props.customScript) {
executeScript(props.customScript, scriptContext.value);
}
@ -205,44 +272,63 @@ const initializeScript = async () => {
// Handle field changes
const handleFieldChange = (fieldName, newValue, oldValue) => {
if (!fieldChangeHandlers.value.has(fieldName)) return;
if (!fieldChangeHandlers.value.has(fieldName)) {
return;
}
const handlers = fieldChangeHandlers.value.get(fieldName);
handlers.forEach(handler => {
handlers.forEach((handler, index) => {
try {
if (typeof handler === 'function') {
handler.call(scriptContext.value, newValue, oldValue);
}
} catch (error) {
console.error(`Error in field change handler for '${fieldName}':`, error);
console.error(`[FormScriptEngine] Error in onFieldChange handler for "${fieldName}":`, error);
if (scriptContext.value?.showError) {
scriptContext.value.showError(`Field change handler error for ${fieldName}: ${error.message}`);
}
}
});
};
// Watch for form data changes
watch(() => props.formData, (newData, oldData) => {
if (!isScriptInitialized.value || !props.formEvents.onFieldChange) return;
// Compare and trigger handlers for changed fields
Object.keys(newData).forEach(fieldName => {
if (newData[fieldName] !== oldData?.[fieldName]) {
handleFieldChange(fieldName, newData[fieldName], oldData?.[fieldName]);
}
});
}, { deep: true });
// Watch for script changes and reinitialize
watch(() => props.customScript, () => {
if (isScriptInitialized.value) {
watch(() => props.customScript, (newScript, oldScript) => {
if (newScript !== oldScript) {
isScriptInitialized.value = false;
fieldChangeHandlers.value.clear();
initializeScript();
}
});
// Watch for form data changes - the elegant, performant way
watch(() => props.formData, (newData, oldData) => {
if (!isScriptInitialized.value || !props.formEvents.onFieldChange) {
return;
}
// Compare with our stored previousFormData to detect changes
Object.keys(newData).forEach(fieldName => {
const newValue = newData[fieldName];
const oldValue = previousFormData.value[fieldName];
if (newValue !== oldValue) {
handleFieldChange(fieldName, newValue, oldValue);
}
});
// Update previousFormData for next comparison
previousFormData.value = { ...newData };
}, { deep: true, immediate: false });
// Watch for CSS changes and update
watch(() => props.customCSS, () => {
// CSS will be automatically updated by the template reactivity
});
// Lifecycle hooks
onMounted(() => {
initializeScript();
setTimeout(() => {
initializeScript();
}, 500); // Delay to ensure parent DOM and initial props are settled
});
onUnmounted(() => {
@ -259,13 +345,12 @@ defineExpose({
},
getContext: () => scriptContext.value,
reinitialize: () => {
isScriptInitialized.value = false;
fieldChangeHandlers.value.clear();
initializeScript();
isScriptInitialized.value = false;
initializeScript();
}
});
</script>
<style scoped>
<style>
/* Component doesn't render visible content */
</style>

View File

@ -141,6 +141,210 @@ const activeCategory = ref('all');
// Templates data
const templates = [
{
id: 'test-css-js',
name: 'CSS & JavaScript Test',
description: 'Simple test form to verify custom CSS and JavaScript functionality works correctly.',
category: 'advanced',
image: null,
hasScript: true,
hasCSS: true,
componentCount: 6,
components: [
{
type: 'heading',
name: 'Heading',
category: 'Layout',
icon: 'material-symbols:title',
defaultProps: {
value: 'CSS & JavaScript Test Form',
level: 1,
width: '100%',
gridColumn: 'span 12'
}
},
{
type: 'paragraph',
name: 'Paragraph',
category: 'Layout',
icon: 'material-symbols:text-snippet-outline',
defaultProps: {
value: 'This form tests custom CSS styling and JavaScript functionality. Try changing the values to see the scripts in action!',
width: '100%',
gridColumn: 'span 12'
}
},
{
type: 'text',
name: 'Text Field',
category: 'Basic Inputs',
icon: 'material-symbols:text-fields',
defaultProps: {
type: 'text',
label: 'Your Name',
name: 'user_name',
placeholder: 'Enter your name',
help: 'This field will trigger JavaScript when changed',
validation: 'required',
width: '50%',
gridColumn: 'span 6'
}
},
{
type: 'number',
name: 'Number',
category: 'Basic Inputs',
icon: 'material-symbols:counter-1-outline',
defaultProps: {
type: 'number',
label: 'Quantity',
name: 'quantity',
placeholder: '1',
help: 'Change this to see the total calculation',
min: 1,
max: 100,
validation: 'required',
width: '25%',
gridColumn: 'span 3'
}
},
{
type: 'number',
name: 'Number',
category: 'Basic Inputs',
icon: 'material-symbols:counter-1-outline',
defaultProps: {
type: 'number',
label: 'Price ($)',
name: 'price',
placeholder: '10.00',
help: 'Price per item',
min: 0,
step: 0.01,
validation: 'required',
width: '25%',
gridColumn: 'span 3'
}
},
{
type: 'text',
name: 'Text Field',
category: 'Basic Inputs',
icon: 'material-symbols:text-fields',
defaultProps: {
type: 'text',
label: 'Total Amount',
name: 'total',
placeholder: '$0.00',
help: 'This will be calculated automatically',
width: '100%',
gridColumn: 'span 12'
}
}
],
script: `// Welcome message when form loads
this.showSuccess('CSS & JavaScript test form loaded successfully!');
// Greet user when name changes
this.onFieldChange('user_name', (value) => {
if (value && value.trim()) {
this.showInfo('Hello, ' + value.trim() + '! Welcome to the test form.');
}
});
// Calculate total when quantity or price changes
this.onFieldChange(['quantity', 'price'], () => {
const quantity = parseFloat(this.getField('quantity')) || 0;
const price = parseFloat(this.getField('price')) || 0;
const total = quantity * price;
this.setField('total', '$' + total.toFixed(2));
if (total > 0) {
this.console.log('Total calculated:', total);
if (total > 50) {
this.showSuccess('Great! Total is over $50');
} else if (total > 100) {
this.showInfo('Wow! Total is over $100');
}
}
});
// Initialize with default values when form loads
setTimeout(() => {
this.setField('quantity', 2);
this.setField('price', 15.50);
}, 500);`,
css: `.form-container {
background: linear-gradient(135deg, #fef7ff 0%, #f3e8ff 100%);
border: 3px solid #a855f7;
border-radius: 20px;
padding: 2rem;
box-shadow: 0 20px 40px rgba(168, 85, 247, 0.2);
}
/* Style the name field with a special border */
.form-field[data-name="user_name"] {
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
border: 2px solid #10b981;
border-radius: 12px;
padding: 1rem;
}
.form-field[data-name="user_name"] input {
background: transparent;
border: none;
font-weight: 600;
color: #065f46;
}
/* Style the quantity field */
.form-field[data-name="quantity"] {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 2px solid #f59e0b;
border-radius: 12px;
padding: 1rem;
}
/* Style the price field */
.form-field[data-name="price"] {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
border: 2px solid #3b82f6;
border-radius: 12px;
padding: 1rem;
}
/* Style the total field with special emphasis */
.form-field[data-name="total"] {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
border: 3px solid #ef4444;
border-radius: 16px;
padding: 1.5rem;
text-align: center;
}
.form-field[data-name="total"] input {
background: transparent;
border: none;
font-size: 1.5rem;
font-weight: bold;
color: #dc2626;
text-align: center;
}
/* Add some hover effects */
.form-field:hover {
transform: translateY(-2px);
transition: all 0.3s ease;
}
/* Style the heading */
h1 {
color: #7c3aed;
text-shadow: 2px 2px 4px rgba(124, 58, 237, 0.3);
}`
},
{
id: 'contact-form',
name: 'Contact Form',
@ -2187,6 +2391,7 @@ this.onFieldChange('budget', (value) => {
// Update display somewhere in the form
console.log('Budget formatted:', formatted);
this.showInfo('Budget updated: ' + formatted);
});
// OTP validation
@ -2201,18 +2406,32 @@ this.onFieldChange('otp_code', (value) => {
}
}, 1000);
}
});`,
});
// Initialize form
console.log('Advanced form script loaded successfully!');`,
css: `.form-container {
background: #f8fafc;
border-radius: 12px;
background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 100%);
border-radius: 16px;
padding: 2rem;
border: 1px solid #e2e8f0;
border: 2px solid #e2e8f0;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.form-field[data-name="budget"] {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 2px solid #f59e0b;
border-radius: 12px;
padding: 1rem;
font-weight: bold;
font-size: 1.1rem;
}
.form-field[data-name="info_display"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.form-field[data-name="dropzone"] {
@ -2225,6 +2444,8 @@ this.onFieldChange('otp_code', (value) => {
.form-field[data-name="dropzone"]:hover {
border-color: #3b82f6;
background: #eff6ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.form-field[data-name="otp_code"] input {
@ -2232,6 +2453,19 @@ this.onFieldChange('otp_code', (value) => {
font-family: monospace;
font-size: 1.2rem;
letter-spacing: 0.5rem;
background: #f0f9ff;
border: 2px solid #3b82f6;
}
/* Animated submit button */
.form-field[data-name="submit_advanced"] button {
background: linear-gradient(45deg, #3b82f6, #1d4ed8);
transition: all 0.3s ease;
}
.form-field[data-name="submit_advanced"] button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
}`
}
];

View File

@ -117,8 +117,8 @@ watch(
></Icon>
</div>
<div class="modal-body">
<slot name="body"></slot>
<slot v-if="!$slots.body"></slot>
<slot name="body"></slot>
<slot v-if="!$slots.body"></slot>
</div>
<div v-if="!hideFooter" class="modal-footer">
<slot name="footer"></slot>

View File

@ -19,6 +19,8 @@ The documentation is organized into the following sections:
### Form Builder
- [User Guide](./form-builder/USER_GUIDE.md) - How to use the Form Builder
- [Technical Guide](./form-builder/TECHNICAL_GUIDE.md) - Technical details of the Form Builder implementation
- [JavaScript API Reference](./form-builder/JAVASCRIPT_API.md) - Complete JavaScript API for dynamic forms and calculations
- [Grid System Guide](./form-builder/grid-system.md) - Visual grid system and layout documentation
### Process Execution
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface
@ -48,4 +50,15 @@ When contributing to this documentation:
If you have questions about this documentation or need help with the system, please contact the development team.
Last updated: July 10, 2024
Last updated: December 2024
## Recent Updates
### December 2024 - Major Form Builder Enhancements
- **JavaScript Execution Engine**: Added FormScriptEngine component for real-time calculations
- **Dynamic Field Updates**: Implemented onFieldChange and onLoad event handlers
- **Real-time Calculations**: Forms now support live mathematical calculations and field dependencies
- **Enhanced Debugging**: Comprehensive logging and error handling for JavaScript execution
- **API Documentation**: Complete JavaScript API reference with practical examples
- **Performance Optimizations**: Improved change detection and memory management
- **Security Enhancements**: Sandboxed JavaScript execution with input validation

View File

@ -0,0 +1,767 @@
# Form Builder JavaScript API Reference
## Overview
The Form Builder JavaScript API enables developers to create dynamic, interactive forms with real-time calculations, conditional logic, and automated field updates. This API is powered by the FormScriptEngine component that executes JavaScript code safely within form contexts.
## Quick Start
### Basic Form Script Structure
```javascript
// Form initialization - runs once when form loads
onLoad: function() {
console.log('Form is loading...');
// Set default values
setField('status', 'new');
setField('date_created', new Date().toISOString().split('T')[0]);
}
// Field change handler - runs when any field value changes
onFieldChange: function(fieldName, value) {
console.log('Field changed:', fieldName, '=', value);
// Handle specific field changes
if (fieldName === 'quantity' || fieldName === 'price') {
updateTotal();
}
}
// Helper functions (optional)
function updateTotal() {
const quantity = getField('quantity') || 0;
const price = getField('price') || 0;
const total = quantity * price;
setField('total', total.toFixed(2));
}
```
## Core Functions
### setField(fieldName, value)
Updates a form field value and triggers UI refresh.
**Parameters:**
- `fieldName` (string): The name/ID of the form field to update
- `value` (any): The new value to set for the field
**Returns:** `void`
**Examples:**
```javascript
// Set text field
setField('user_name', 'John Doe');
// Set number field
setField('age', 25);
// Set date field
setField('birth_date', '1999-01-15');
// Set boolean field
setField('is_active', true);
// Set calculated field
setField('total_amount', 149.99);
// Clear field
setField('notes', '');
```
**Notes:**
- Field names are case-sensitive and must match the form field's `name` attribute exactly
- Setting a field value will trigger the `onFieldChange` handler for that field
- Values are automatically converted to appropriate types based on field type
### getField(fieldName)
Retrieves the current value of a form field.
**Parameters:**
- `fieldName` (string): The name/ID of the form field to retrieve
**Returns:** The current field value (type depends on field type)
**Examples:**
```javascript
// Get text field value
const userName = getField('user_name'); // Returns string
// Get number field value with default
const quantity = getField('quantity') || 0; // Returns number or 0
// Get date field value
const birthDate = getField('birth_date'); // Returns string in YYYY-MM-DD format
// Get boolean field value
const isActive = getField('is_active'); // Returns boolean
// Check if field exists and has value
const email = getField('email');
if (email) {
console.log('Email provided:', email);
}
```
**Notes:**
- Returns `undefined` if field doesn't exist
- Returns empty string `''` for empty text fields
- Returns `false` for unchecked boolean fields
- Use logical OR `||` to provide default values for empty fields
## Event Handlers
### onLoad
Executes once when the form initially loads. Use for initialization, setting default values, and performing initial calculations.
**Signature:**
```javascript
onLoad: function() {
// Initialization code here
}
```
**Common Use Cases:**
```javascript
onLoad: function() {
// Set default values
setField('country', 'United States');
setField('currency', 'USD');
setField('created_date', new Date().toISOString().split('T')[0]);
// Initialize calculated fields
setField('subtotal', 0);
setField('tax_amount', 0);
setField('total', 0);
// Set up initial state
const userRole = getField('user_role');
if (userRole === 'admin') {
setField('permissions', 'full_access');
}
// Perform initial calculations
calculateTotals();
}
```
### onFieldChange
Executes whenever a user changes any field value. Receives the field name and new value as parameters.
**Signature:**
```javascript
onFieldChange: function(fieldName, value) {
// Handle field changes here
}
```
**Parameters:**
- `fieldName` (string): Name of the field that changed
- `value` (any): New value of the field
**Common Use Cases:**
```javascript
onFieldChange: function(fieldName, value) {
console.log('Field updated:', fieldName, '=', value);
// Handle specific fields
switch(fieldName) {
case 'quantity':
case 'unit_price':
calculateLineTotal();
break;
case 'country':
updateTaxRate(value);
updateShippingOptions(value);
break;
case 'subscription_type':
updateFeatures(value);
break;
case 'birth_date':
calculateAge(value);
break;
}
// Handle multiple related fields
if (['first_name', 'last_name'].includes(fieldName)) {
updateFullName();
}
// Conditional logic
if (fieldName === 'has_insurance' && value === true) {
setField('insurance_provider', '');
setField('policy_number', '');
}
}
```
## Practical Examples
### Invoice Calculator
```javascript
onLoad: function() {
// Initialize invoice
setField('invoice_number', 'INV-' + Date.now());
setField('invoice_date', new Date().toISOString().split('T')[0]);
setField('tax_rate', 8.5); // 8.5% default tax
// Initialize line items
setField('quantity', 1);
setField('unit_price', 0);
setField('subtotal', 0);
setField('tax_amount', 0);
setField('total', 0);
}
onFieldChange: function(fieldName, value) {
// Recalculate when quantity, price, or tax rate changes
if (['quantity', 'unit_price', 'tax_rate'].includes(fieldName)) {
const quantity = parseFloat(getField('quantity')) || 0;
const unitPrice = parseFloat(getField('unit_price')) || 0;
const taxRate = parseFloat(getField('tax_rate')) || 0;
const subtotal = quantity * unitPrice;
const taxAmount = (subtotal * taxRate) / 100;
const total = subtotal + taxAmount;
setField('subtotal', subtotal.toFixed(2));
setField('tax_amount', taxAmount.toFixed(2));
setField('total', total.toFixed(2));
}
}
```
### User Registration with Validation
```javascript
onLoad: function() {
setField('registration_date', new Date().toISOString().split('T')[0]);
setField('account_status', 'pending');
}
onFieldChange: function(fieldName, value) {
// Email validation
if (fieldName === 'email') {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailPattern.test(value);
setField('email_valid', isValid);
setField('email_status', isValid ? 'Valid' : 'Invalid email format');
}
// Password strength
if (fieldName === 'password') {
let strength = 'Weak';
if (value.length >= 8) {
if (/[A-Z]/.test(value) && /[a-z]/.test(value) && /\d/.test(value)) {
strength = 'Strong';
} else {
strength = 'Medium';
}
}
setField('password_strength', strength);
}
// Full name combination
if (['first_name', 'last_name'].includes(fieldName)) {
const firstName = getField('first_name') || '';
const lastName = getField('last_name') || '';
const fullName = (firstName + ' ' + lastName).trim();
setField('full_name', fullName);
}
}
```
### E-commerce Order Form
```javascript
onLoad: function() {
setField('order_date', new Date().toISOString().split('T')[0]);
setField('shipping_method', 'standard');
setField('gift_wrap', false);
// Initialize pricing
updateShipping();
}
onFieldChange: function(fieldName, value) {
// Product selection
if (fieldName === 'product') {
const prices = {
'laptop': 999.99,
'mouse': 29.99,
'keyboard': 79.99,
'monitor': 299.99
};
const price = prices[value] || 0;
setField('unit_price', price);
}
// Quantity and pricing updates
if (['quantity', 'unit_price'].includes(fieldName)) {
const quantity = parseFloat(getField('quantity')) || 0;
const unitPrice = parseFloat(getField('unit_price')) || 0;
const itemTotal = quantity * unitPrice;
setField('item_total', itemTotal.toFixed(2));
updateOrderTotal();
}
// Shipping calculation
if (['shipping_method', 'country'].includes(fieldName)) {
updateShipping();
}
// Gift wrap fee
if (fieldName === 'gift_wrap') {
const giftWrapFee = value ? 9.99 : 0;
setField('gift_wrap_fee', giftWrapFee.toFixed(2));
updateOrderTotal();
}
}
function updateShipping() {
const method = getField('shipping_method');
const country = getField('country');
let shippingCost = 0;
if (country === 'US') {
switch(method) {
case 'standard': shippingCost = 5.99; break;
case 'express': shippingCost = 14.99; break;
case 'overnight': shippingCost = 24.99; break;
}
} else {
switch(method) {
case 'standard': shippingCost = 19.99; break;
case 'express': shippingCost = 39.99; break;
}
}
setField('shipping_cost', shippingCost.toFixed(2));
updateOrderTotal();
}
function updateOrderTotal() {
const itemTotal = parseFloat(getField('item_total')) || 0;
const shippingCost = parseFloat(getField('shipping_cost')) || 0;
const giftWrapFee = parseFloat(getField('gift_wrap_fee')) || 0;
const orderTotal = itemTotal + shippingCost + giftWrapFee;
setField('order_total', orderTotal.toFixed(2));
}
```
### Loan Calculator
```javascript
onLoad: function() {
// Set default loan parameters
setField('loan_amount', 100000);
setField('interest_rate', 4.5);
setField('loan_term_years', 30);
// Calculate initial payment
calculateLoanPayment();
}
onFieldChange: function(fieldName, value) {
if (['loan_amount', 'interest_rate', 'loan_term_years'].includes(fieldName)) {
calculateLoanPayment();
}
}
function calculateLoanPayment() {
const principal = parseFloat(getField('loan_amount')) || 0;
const annualRate = parseFloat(getField('interest_rate')) || 0;
const years = parseFloat(getField('loan_term_years')) || 0;
if (principal > 0 && annualRate > 0 && years > 0) {
const monthlyRate = (annualRate / 100) / 12;
const numberOfPayments = years * 12;
const monthlyPayment = principal *
(monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) /
(Math.pow(1 + monthlyRate, numberOfPayments) - 1);
const totalPayments = monthlyPayment * numberOfPayments;
const totalInterest = totalPayments - principal;
setField('monthly_payment', monthlyPayment.toFixed(2));
setField('total_payments', totalPayments.toFixed(2));
setField('total_interest', totalInterest.toFixed(2));
} else {
setField('monthly_payment', '0.00');
setField('total_payments', '0.00');
setField('total_interest', '0.00');
}
}
```
## Available Global Objects
### Math Object
Full JavaScript Math object with all methods:
```javascript
// Common math operations
const rounded = Math.round(getField('price') * 1.1);
const randomId = Math.floor(Math.random() * 1000000);
const maxValue = Math.max(getField('value1'), getField('value2'));
// Advanced math
const power = Math.pow(getField('base'), getField('exponent'));
const squareRoot = Math.sqrt(getField('area'));
```
### Date Object
Full JavaScript Date object for date/time operations:
```javascript
// Current date/time
const now = new Date();
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
// Date calculations
const birthDate = new Date(getField('birth_date'));
const age = Math.floor((Date.now() - birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
// Date formatting
const formatted = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
```
### String Methods
Standard JavaScript string methods:
```javascript
// String manipulation
const name = getField('user_name').toUpperCase();
const initials = getField('first_name').charAt(0) + getField('last_name').charAt(0);
const slug = getField('title').toLowerCase().replace(/\s+/g, '-');
```
### Number Parsing
Standard parsing functions:
```javascript
// Parse numbers
const quantity = parseInt(getField('quantity_str')) || 0;
const price = parseFloat(getField('price_str')) || 0.0;
// Validation
const isValidNumber = !isNaN(parseFloat(getField('amount')));
```
### Console Logging
Console object for debugging:
```javascript
// Debug logging
console.log('Processing field:', fieldName, 'with value:', value);
console.warn('Invalid input detected:', value);
console.error('Calculation failed:', error);
// Grouped logging
console.group('Order Calculation');
console.log('Subtotal:', subtotal);
console.log('Tax:', tax);
console.log('Total:', total);
console.groupEnd();
```
## Best Practices
### Error Handling
```javascript
onFieldChange: function(fieldName, value) {
try {
if (fieldName === 'price') {
const price = parseFloat(value);
if (isNaN(price)) {
console.warn('Invalid price value:', value);
setField('price_error', 'Please enter a valid number');
return;
}
if (price < 0) {
console.warn('Negative price not allowed:', price);
setField('price_error', 'Price cannot be negative');
return;
}
// Clear error and proceed
setField('price_error', '');
calculateTotal(price);
}
} catch (error) {
console.error('Error processing field change:', error);
}
}
```
### Performance Optimization
```javascript
// Cache calculations
let cachedTaxRate = null;
function getTaxRate(country) {
if (cachedTaxRate === null) {
const rates = {
'US': 8.5,
'CA': 12.0,
'UK': 20.0
};
cachedTaxRate = rates[country] || 0;
}
return cachedTaxRate;
}
// Minimize calculations
onFieldChange: function(fieldName, value) {
// Only calculate when relevant fields change
if (['quantity', 'price', 'tax_rate'].includes(fieldName)) {
// Batch related calculations
updateAllTotals();
}
// Avoid recalculating if value hasn't actually changed
const currentTotal = getField('total');
const newTotal = calculateNewTotal();
if (currentTotal !== newTotal) {
setField('total', newTotal);
}
}
```
### Data Validation
```javascript
// Validation helper functions
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function isValidPhone(phone) {
return /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(phone);
}
function validateCreditCard(number) {
// Luhn algorithm
const digits = number.replace(/\D/g, '');
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i]);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
// Use in field change handler
onFieldChange: function(fieldName, value) {
if (fieldName === 'email') {
const valid = isValidEmail(value);
setField('email_valid', valid);
setField('email_message', valid ? 'Valid email' : 'Invalid email format');
}
}
```
## Common Patterns
### Conditional Field Display
```javascript
// Simulate showing/hiding fields by setting values
onFieldChange: function(fieldName, value) {
if (fieldName === 'account_type') {
if (value === 'business') {
setField('company_name', '');
setField('tax_id', '');
setField('show_business_fields', 'true');
} else {
setField('show_business_fields', 'false');
}
}
}
```
### Multi-step Calculations
```javascript
// Break complex calculations into steps
function calculateShippingCost() {
const weight = parseFloat(getField('package_weight')) || 0;
const distance = parseFloat(getField('shipping_distance')) || 0;
const method = getField('shipping_method');
// Step 1: Base cost by weight
let baseCost = 0;
if (weight <= 1) baseCost = 5.99;
else if (weight <= 5) baseCost = 12.99;
else baseCost = 5.99 + (weight - 1) * 2.50;
// Step 2: Distance multiplier
let distanceMultiplier = 1;
if (distance > 500) distanceMultiplier = 1.5;
else if (distance > 100) distanceMultiplier = 1.2;
// Step 3: Method adjustment
let methodMultiplier = 1;
switch(method) {
case 'express': methodMultiplier = 2; break;
case 'overnight': methodMultiplier = 3; break;
}
// Final calculation
const finalCost = baseCost * distanceMultiplier * methodMultiplier;
setField('shipping_cost', finalCost.toFixed(2));
setField('shipping_breakdown', `Base: $${baseCost} × Distance: ${distanceMultiplier} × Method: ${methodMultiplier}`);
}
```
### Form State Management
```javascript
onLoad: function() {
// Initialize form state
setField('form_state', 'initialized');
setField('validation_errors', '');
setField('completion_percentage', 0);
}
onFieldChange: function(fieldName, value) {
// Update completion percentage
updateCompletionPercentage();
// Track form state
setField('form_state', 'editing');
setField('last_modified', new Date().toISOString());
}
function updateCompletionPercentage() {
const requiredFields = ['first_name', 'last_name', 'email', 'phone'];
let completedFields = 0;
requiredFields.forEach(field => {
if (getField(field) && getField(field).trim() !== '') {
completedFields++;
}
});
const percentage = Math.round((completedFields / requiredFields.length) * 100);
setField('completion_percentage', percentage);
}
```
## Debugging and Troubleshooting
### Common Issues
**1. Field names don't match**
```javascript
// ❌ Wrong - case sensitive
setField('User_Name', 'John'); // Field is actually 'user_name'
// ✅ Correct
setField('user_name', 'John');
```
**2. Missing error handling**
```javascript
// ❌ Without error handling
const price = parseFloat(getField('price'));
const total = price * quantity; // Will fail if price is invalid
// ✅ With error handling
const price = parseFloat(getField('price')) || 0;
const quantity = parseFloat(getField('quantity')) || 0;
const total = price * quantity;
```
**3. Infinite loops in onFieldChange**
```javascript
// ❌ Can cause infinite loop
onFieldChange: function(fieldName, value) {
if (fieldName === 'total') {
setField('total', value * 1.1); // This will trigger onFieldChange again!
}
}
// ✅ Avoid triggering changes for calculated fields
onFieldChange: function(fieldName, value) {
if (fieldName === 'quantity' || fieldName === 'price') {
const total = getField('quantity') * getField('price');
setField('total', total); // Only update when input fields change
}
}
```
### Debug Techniques
```javascript
// Enable detailed logging
onFieldChange: function(fieldName, value) {
console.group(`Field Change: ${fieldName}`);
console.log('New Value:', value);
console.log('Current Form Data:', {
quantity: getField('quantity'),
price: getField('price'),
total: getField('total')
});
// Your calculation logic here
console.groupEnd();
}
// Add validation checkpoints
function calculateTotal() {
const quantity = getField('quantity');
const price = getField('price');
console.log('Calculation inputs:', { quantity, price });
if (isNaN(quantity) || isNaN(price)) {
console.error('Invalid inputs for calculation');
return;
}
const total = quantity * price;
console.log('Calculated total:', total);
setField('total', total.toFixed(2));
}
```
---
*This API reference covers all available JavaScript functionality in the Form Builder. For implementation examples and integration details, see the [Technical Guide](TECHNICAL_GUIDE.md) and [User Guide](USER_GUIDE.md).*
Last updated: December 2024

View File

@ -438,11 +438,11 @@ export const useFormBuilderStore = defineStore('formBuilder', {
},
formConfig: (state) => ({
id: state.formId,
uuid: state.formUUID,
name: state.formName,
description: state.formDescription,
components: state.formComponents
id: state.formId,
uuid: state.formUUID,
name: state.formName,
description: state.formDescription,
components: state.formComponents
})
},
@ -912,8 +912,726 @@ const useVirtualScrolling = (components, containerHeight) => {
}
```
## JavaScript Execution Engine (FormScriptEngine)
### Architecture Overview
The FormScriptEngine component provides real-time JavaScript execution for dynamic form behavior, calculations, and conditional logic. It monitors form data changes and executes user-defined event handlers to create interactive forms.
#### Core Components
```
components/
├── FormScriptEngine.vue # Main execution engine
└── FormBuilderCanvas.vue # Integration point for script execution
pages/
└── form-builder/
└── index.vue # Form data handling and script triggering
```
### FormScriptEngine.vue Implementation
```vue
<template>
<!-- FormScriptEngine runs invisibly in the background -->
<div style="display: none;">
<!-- Debug info in development -->
<div v-if="isDev">
Script Engine Status: {{ isInitialized ? 'Active' : 'Initializing' }}
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick, onMounted } from 'vue'
// Component props
const props = defineProps({
formData: {
type: Object,
required: true
},
script: {
type: String,
default: ''
}
})
// Reactive state
const isInitialized = ref(false)
const previousFormData = ref({})
const scriptContext = ref(null)
const handlers = ref({
onLoad: null,
onFieldChange: null
})
// Development mode detection
const isDev = process.env.NODE_ENV === 'development'
// Initialize script execution context
const initializeScript = () => {
try {
if (!props.script.trim()) {
console.log('FormScriptEngine: No script provided')
return
}
// Create execution context with form functions
const context = {
setField: (fieldName, value) => {
console.log('setField called:', fieldName, '=', value)
if (props.formData[fieldName] !== value) {
// Update form data reactively
props.formData[fieldName] = value
// Emit update event for parent component
emit('fieldUpdate', { fieldName, value })
}
},
getField: (fieldName) => {
const value = props.formData[fieldName]
console.log('getField called:', fieldName, '=', value)
return value
},
console: console, // Allow console access
// Add other safe global functions as needed
parseFloat: parseFloat,
parseInt: parseInt,
Date: Date,
Math: Math
}
// Execute script in isolated context
const scriptFunction = new Function(
'setField', 'getField', 'console', 'parseFloat', 'parseInt', 'Date', 'Math',
`
${props.script}
// Return the event handlers
return {
onLoad: typeof onLoad !== 'undefined' ? onLoad : null,
onFieldChange: typeof onFieldChange !== 'undefined' ? onFieldChange : null
}
`
)
// Execute script and extract handlers
const result = scriptFunction(
context.setField,
context.getField,
context.console,
context.parseFloat,
context.parseInt,
context.Date,
context.Math
)
handlers.value = result
scriptContext.value = context
console.log('FormScriptEngine: Script initialized with handlers:', Object.keys(result))
return true
} catch (error) {
console.error('FormScriptEngine: Script initialization error:', error)
return false
}
}
// Execute onLoad handler
const executeOnLoad = async () => {
if (handlers.value.onLoad && typeof handlers.value.onLoad === 'function') {
try {
console.log('FormScriptEngine: Executing onLoad handler')
await handlers.value.onLoad()
console.log('FormScriptEngine: onLoad completed')
} catch (error) {
console.error('FormScriptEngine: onLoad execution error:', error)
}
}
}
// Execute onFieldChange handler
const executeOnFieldChange = async (fieldName, value) => {
if (handlers.value.onFieldChange && typeof handlers.value.onFieldChange === 'function') {
try {
console.log('FormScriptEngine: Executing onFieldChange for:', fieldName)
await handlers.value.onFieldChange(fieldName, value)
console.log('FormScriptEngine: onFieldChange completed for:', fieldName)
} catch (error) {
console.error('FormScriptEngine: onFieldChange execution error:', error)
}
}
}
// Detect and trigger field changes
const triggerFieldChangeDetection = () => {
const currentData = { ...props.formData }
const previousData = previousFormData.value
// Find changed fields
const changedFields = []
for (const fieldName in currentData) {
if (currentData[fieldName] !== previousData[fieldName]) {
changedFields.push({
name: fieldName,
value: currentData[fieldName],
oldValue: previousData[fieldName]
})
}
}
if (changedFields.length > 0) {
console.log('FormScriptEngine: Detected field changes:', changedFields)
// Execute handler for each changed field
changedFields.forEach(field => {
executeOnFieldChange(field.name, field.value)
})
// Update previous data after processing
previousFormData.value = { ...currentData }
} else {
console.log('FormScriptEngine: No field changes detected')
}
}
// Watch for form data changes with enhanced detection
watch(
() => props.formData,
(newData, oldData) => {
if (!isInitialized.value) return
console.log('FormScriptEngine: Form data changed, checking for field changes...')
console.log('New data:', JSON.stringify(newData))
console.log('Previous data:', JSON.stringify(previousFormData.value))
// Use nextTick to ensure DOM updates are complete
nextTick(() => {
triggerFieldChangeDetection()
})
},
{
deep: true,
flush: 'post' // Execute after DOM updates
}
)
// Initialize when component mounts
onMounted(async () => {
console.log('FormScriptEngine: Component mounted, initializing...')
// Initialize script context
const success = initializeScript()
if (success) {
// Execute onLoad handler first
await executeOnLoad()
// Set initial previousFormData AFTER onLoad execution
// This prevents initial setField calls from being detected as changes
await nextTick()
previousFormData.value = { ...props.formData }
isInitialized.value = true
console.log('FormScriptEngine: Initialization complete')
} else {
console.error('FormScriptEngine: Initialization failed')
}
})
// Public method to manually trigger change detection
const manualTrigger = () => {
console.log('FormScriptEngine: Manual trigger requested')
triggerFieldChangeDetection()
}
// Expose public methods
defineExpose({
manualTrigger,
isInitialized
})
// Emit events
const emit = defineEmits(['fieldUpdate'])
</script>
```
### Integration with Form Builder
#### Form Builder Canvas Integration
```vue
<!-- In FormBuilderCanvas.vue -->
<template>
<div class="form-canvas">
<!-- Form components render here -->
<ComponentPreview
v-for="component in formComponents"
:key="component.id"
:component="component"
:is-preview="isPreview"
/>
<!-- JavaScript execution engine -->
<FormScriptEngine
v-if="isPreview && formScript"
:form-data="previewFormData"
:script="formScript"
@field-update="handleScriptFieldUpdate"
/>
</div>
</template>
<script setup>
import FormScriptEngine from '~/components/FormScriptEngine.vue'
// Handle script-triggered field updates
const handleScriptFieldUpdate = ({ fieldName, value }) => {
console.log('Canvas: Script updated field:', fieldName, '=', value)
// Update preview form data
previewFormData.value[fieldName] = value
// Trigger UI refresh
nextTick(() => {
refreshFormComponents()
})
}
</script>
```
#### Main Form Builder Integration
```vue
<!-- In pages/form-builder/index.vue -->
<script setup>
// Enhanced FormKit input handler with script engine integration
const handleFormKitInput = (event) => {
console.log('FormKit input event:', event)
// Update preview form data
previewFormData.value = { ...event }
// Trigger script engine change detection
if (formScriptEngineRef.value) {
nextTick(() => {
formScriptEngineRef.value.manualTrigger()
})
}
}
// Template ref for script engine
const formScriptEngineRef = ref(null)
</script>
<template>
<div class="form-builder">
<!-- Canvas with integrated script engine -->
<FormBuilderCanvas
ref="formScriptEngineRef"
:form-script="formStore.currentForm?.script"
@form-kit-input="handleFormKitInput"
/>
</div>
</template>
```
### Event Handler API Reference
#### onLoad Handler
Executes once when the form loads. Use for initialization.
```javascript
onLoad: function() {
// Initialize default values
setField('status', 'active');
setField('created_date', new Date().toISOString().split('T')[0]);
// Perform initial calculations
calculateInitialValues();
}
```
#### onFieldChange Handler
Executes when any field value changes.
```javascript
onFieldChange: function(fieldName, value) {
console.log('Field changed:', fieldName, 'New value:', value);
// Handle specific field changes
switch(fieldName) {
case 'quantity':
case 'price':
updateTotal();
break;
case 'country':
updateTaxRate(value);
break;
}
}
```
### Error Handling and Debugging
#### Script Execution Errors
```javascript
// Comprehensive error handling in script execution
const executeHandler = async (handler, ...args) => {
try {
if (typeof handler === 'function') {
const result = await handler(...args)
return { success: true, result }
}
return { success: false, error: 'Handler is not a function' }
} catch (error) {
console.error('Script execution error:', error)
// Emit error event for debugging
emit('scriptError', {
error: error.message,
stack: error.stack,
handler: handler.name || 'anonymous',
args
})
return { success: false, error: error.message }
}
}
```
#### Debug Mode Features
```javascript
// Enhanced logging in development mode
const debugLog = (message, data) => {
if (isDev) {
console.log(`[FormScriptEngine Debug] ${message}`, data)
}
}
// Field change tracing
const traceFieldChange = (fieldName, oldValue, newValue) => {
if (isDev) {
console.group(`Field Change: ${fieldName}`)
console.log('Old Value:', oldValue)
console.log('New Value:', newValue)
console.log('Timestamp:', new Date().toISOString())
console.groupEnd()
}
}
```
### Performance Optimization
#### Change Detection Optimization
```javascript
// Efficient change detection using shallow comparison
const hasDataChanged = (newData, oldData) => {
const newKeys = Object.keys(newData)
const oldKeys = Object.keys(oldData)
// Quick length check
if (newKeys.length !== oldKeys.length) return true
// Value comparison
return newKeys.some(key => newData[key] !== oldData[key])
}
// Debounced change detection for rapid updates
const debouncedChangeDetection = useDebounceFn(
triggerFieldChangeDetection,
100 // 100ms debounce
)
```
#### Memory Management
```javascript
// Cleanup when component unmounts
onUnmounted(() => {
console.log('FormScriptEngine: Cleaning up...')
// Clear references
handlers.value = null
scriptContext.value = null
previousFormData.value = null
// Remove event listeners if any
cleanupEventListeners()
})
```
### Security Considerations
#### Script Sandboxing
```javascript
// Restricted execution context - only safe functions exposed
const createSecureContext = () => {
return {
// Form manipulation functions
setField,
getField,
// Safe global functions
console: {
log: console.log,
warn: console.warn,
error: console.error
},
// Math and date functions
Math,
Date,
parseFloat,
parseInt,
isNaN,
// String functions
String,
// No access to:
// - window object
// - document object
// - eval function
// - Function constructor
// - import/require
}
}
```
#### Input Validation
```javascript
// Validate script before execution
const validateScript = (script) => {
const dangerousPatterns = [
/eval\s*\(/,
/Function\s*\(/,
/window\./,
/document\./,
/import\s+/,
/require\s*\(/,
/fetch\s*\(/,
/XMLHttpRequest/
]
return !dangerousPatterns.some(pattern => pattern.test(script))
}
```
## Testing Strategy
### JavaScript Engine Tests
```javascript
// FormScriptEngine unit tests
describe('FormScriptEngine', () => {
let wrapper
let mockFormData
beforeEach(() => {
mockFormData = ref({
quantity: 0,
price: 0,
total: 0
})
wrapper = mount(FormScriptEngine, {
props: {
formData: mockFormData.value,
script: `
onLoad: function() {
setField('quantity', 1);
setField('price', 10);
}
onFieldChange: function(fieldName, value) {
if (fieldName === 'quantity' || fieldName === 'price') {
const total = getField('quantity') * getField('price');
setField('total', total);
}
}
`
}
})
})
test('should initialize script and execute onLoad', async () => {
await nextTick()
expect(mockFormData.value.quantity).toBe(1)
expect(mockFormData.value.price).toBe(10)
})
test('should execute onFieldChange when data changes', async () => {
await nextTick()
// Simulate field change
mockFormData.value.quantity = 5
await nextTick()
expect(mockFormData.value.total).toBe(50)
})
test('should handle script errors gracefully', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
const wrapper = mount(FormScriptEngine, {
props: {
formData: mockFormData.value,
script: `
onLoad: function() {
// Intentional error
undefinedFunction();
}
`
}
})
await nextTick()
expect(consoleErrorSpy).toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
test('should validate and sanitize script input', () => {
const dangerousScript = `
onLoad: function() {
eval('alert("XSS")');
window.location = 'http://malicious.com';
}
`
const isValid = wrapper.vm.validateScript(dangerousScript)
expect(isValid).toBe(false)
})
})
// Real-time calculation tests
describe('Real-time Calculations', () => {
test('should update total when quantity changes', async () => {
const formData = ref({ quantity: 2, price: 15.99, total: 0 })
const wrapper = mount(TestFormWithScript, {
props: { initialData: formData.value }
})
// Change quantity
await wrapper.find('input[name="quantity"]').setValue('3')
await nextTick()
expect(formData.value.total).toBe(47.97)
})
test('should handle edge cases in calculations', async () => {
const formData = ref({ quantity: '', price: 'invalid', total: 0 })
const wrapper = mount(TestFormWithScript, {
props: { initialData: formData.value }
})
// Should handle invalid inputs gracefully
await nextTick()
expect(formData.value.total).toBe(0)
})
})
// Performance tests
describe('FormScriptEngine Performance', () => {
test('should not trigger excessive re-renders', async () => {
const executeHandlerSpy = jest.spyOn(FormScriptEngine.methods, 'executeOnFieldChange')
const wrapper = mount(FormScriptEngine, {
props: {
formData: { field1: '', field2: '', field3: '' },
script: `
onFieldChange: function(fieldName, value) {
console.log('Changed:', fieldName);
}
`
}
})
// Make multiple rapid changes
for (let i = 0; i < 10; i++) {
wrapper.setProps({ formData: { field1: `value${i}`, field2: '', field3: '' } })
await nextTick()
}
// Should be debounced/optimized
expect(executeHandlerSpy.mock.calls.length).toBeLessThan(10)
})
})
```
### Integration Tests
```javascript
// Full form workflow tests with JavaScript
describe('Form Builder with JavaScript Integration', () => {
test('should execute calculations in preview mode', async () => {
const wrapper = mount(FormBuilder)
// Add form components
await wrapper.find('[data-component="number"]').trigger('click')
await wrapper.find('[data-component="number"]').trigger('click')
await wrapper.find('[data-component="number"]').trigger('click')
// Configure fields
const components = wrapper.vm.formStore.formComponents
components[0].props.name = 'quantity'
components[1].props.name = 'price'
components[2].props.name = 'total'
// Add JavaScript
wrapper.vm.formStore.currentForm.script = `
onFieldChange: function(fieldName, value) {
if (fieldName === 'quantity' || fieldName === 'price') {
const quantity = parseFloat(getField('quantity')) || 0;
const price = parseFloat(getField('price')) || 0;
setField('total', quantity * price);
}
}
`
// Switch to preview mode
await wrapper.find('[data-testid="preview-toggle"]').trigger('click')
// Input values
await wrapper.find('input[name="quantity"]').setValue('5')
await wrapper.find('input[name="price"]').setValue('19.99')
await nextTick()
// Check calculated total
const totalField = wrapper.find('input[name="total"]')
expect(totalField.element.value).toBe('99.95')
})
test('should handle form builder to preview data flow', async () => {
const wrapper = mount(FormBuilder)
// Build form with JavaScript
await addFormComponents(wrapper)
await addJavaScriptLogic(wrapper)
// Test preview functionality
await wrapper.find('[data-testid="preview-button"]').trigger('click')
// Verify script execution
expect(wrapper.find('.script-engine').exists()).toBe(true)
expect(wrapper.vm.isPreviewMode).toBe(true)
// Test field interactions
await simulateUserInput(wrapper)
await verifyCalculations(wrapper)
})
})
```
### Unit Tests
```javascript

View File

@ -106,7 +106,7 @@ Perfect for collecting simple data:
**Text Field**
- Single-line text input
- Smart width: Small (33%)
- Use for: Names, titles, short answers
- Use for: Names, titles, short answers
- Quick settings: Label, name, placeholder, required
**Text Area**
@ -119,7 +119,7 @@ Perfect for collecting simple data:
- Numeric input with validation
- Smart width: Narrow (25%)
- Use for: Age, quantity, prices
- Features: Min/max limits, step values
- Features: Min/max limits, step values
**Email Field**
- Email input with validation
@ -131,7 +131,7 @@ Perfect for collecting simple data:
- Secure password input
- Smart width: Medium (50%)
- Use for: Authentication forms
- Features: Password masking, strength indicators
- Features: Password masking, strength indicators
### Selection Components
For choosing from options:
@ -151,7 +151,7 @@ For choosing from options:
**Radio Group**
- Single choice from group
- Smart width: Full (100%)
- Use for: Exclusive choices, yes/no questions
- Use for: Exclusive choices, yes/no questions
- Features: Button or traditional styles
**Switch Toggle**
@ -188,7 +188,7 @@ Specialized functionality:
- Standard file input
- Smart width: Wide (75%)
- Use for: Document uploads, attachments
- Features: File type restrictions, size limits
- Features: File type restrictions, size limits
**File Drop Zone**
- Drag & drop upload area
@ -214,7 +214,7 @@ Form structure and organization:
**Heading**
- Section titles and organization
- Smart width: Full (100%)
- Use for: Form sections, categories
- Use for: Form sections, categories
- Features: Multiple heading levels (H1-H6)
**Paragraph**
@ -311,7 +311,7 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
3. **Logical Grouping**: Use similar widths for related fields
4. **Space Efficiency**: Let the system auto-optimize grid placement
### User Experience
### User Experience
1. **Clear Labels**: Use descriptive, action-oriented labels
2. **Helpful Placeholders**: Provide examples in placeholder text
3. **Progressive Disclosure**: Use sections for complex forms
@ -362,7 +362,7 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
**Performance Issues**
- Reduce number of components
- Simplify conditional logic
- Clear browser cache
- Clear browser cache
### Getting Help
- **Documentation**: Complete technical guides available
@ -370,6 +370,281 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
- **Support**: Contact support team for assistance
- **Community**: Join user community forums
## JavaScript & Dynamic Calculations
### Real-time Calculations
The Form Builder supports powerful JavaScript-based calculations that update in real-time as users interact with your forms. This enables dynamic forms with automatic calculations, conditional logic, and interactive behavior.
#### Key Features
- **Real-time Updates**: Calculations execute instantly when users change field values
- **onFieldChange Handlers**: Trigger custom logic when specific fields change
- **setField Function**: Programmatically update field values from JavaScript
- **Cross-field Dependencies**: Create complex relationships between form fields
- **Calculation Templates**: Pre-built templates for common calculation patterns
#### Basic Example
```javascript
// Form with real-time total calculation
onLoad: function() {
console.log('Form loaded, setting up calculations...');
// Initialize fields
setField('quantity', 1);
setField('price', 0);
setField('total', 0);
}
onFieldChange: function(fieldName, value) {
console.log('Field changed:', fieldName, '=', value);
if (fieldName === 'quantity' || fieldName === 'price') {
// Get current values
const quantity = getField('quantity') || 0;
const price = getField('price') || 0;
// Calculate and update total
const total = quantity * price;
setField('total', total.toFixed(2));
console.log('Updated total:', total);
}
}
```
### Available JavaScript Functions
#### setField(fieldName, value)
Updates a form field value and triggers the UI to refresh.
```javascript
// Set text field
setField('user_name', 'John Doe');
// Set number field
setField('quantity', 5);
// Set calculated field
setField('total', 99.95);
```
#### getField(fieldName)
Retrieves the current value of a form field.
```javascript
const userName = getField('user_name');
const quantity = getField('quantity') || 0; // Default to 0 if empty
```
#### onLoad Event Handler
Executes when the form first loads. Use for initialization and setting default values.
```javascript
onLoad: function() {
// Set default values
setField('country', 'USA');
setField('currency', 'USD');
// Perform initial calculations
calculateTotals();
}
```
#### onFieldChange Event Handler
Executes whenever a user changes a field value. Receives fieldName and new value as parameters.
```javascript
onFieldChange: function(fieldName, value) {
switch(fieldName) {
case 'quantity':
case 'price':
updateTotal();
break;
case 'country':
updateShippingOptions(value);
break;
}
}
```
### Real-world Examples
#### Invoice Calculator
```javascript
onLoad: function() {
// Initialize invoice fields
setField('quantity', 1);
setField('unit_price', 0);
setField('tax_rate', 8.5); // 8.5% tax
setField('subtotal', 0);
setField('tax_amount', 0);
setField('total', 0);
}
onFieldChange: function(fieldName, value) {
if (['quantity', 'unit_price', 'tax_rate'].includes(fieldName)) {
const quantity = parseFloat(getField('quantity')) || 0;
const unitPrice = parseFloat(getField('unit_price')) || 0;
const taxRate = parseFloat(getField('tax_rate')) || 0;
const subtotal = quantity * unitPrice;
const taxAmount = (subtotal * taxRate) / 100;
const total = subtotal + taxAmount;
setField('subtotal', subtotal.toFixed(2));
setField('tax_amount', taxAmount.toFixed(2));
setField('total', total.toFixed(2));
}
}
```
#### Conditional Field Logic
```javascript
onFieldChange: function(fieldName, value) {
if (fieldName === 'subscription_type') {
if (value === 'premium') {
setField('premium_features', 'Available');
setField('support_level', '24/7 Priority');
} else {
setField('premium_features', 'Not Available');
setField('support_level', 'Standard Business Hours');
}
}
}
```
#### Age Calculator
```javascript
onFieldChange: function(fieldName, value) {
if (fieldName === 'birth_date') {
const birthDate = new Date(value);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
setField('age', age);
// Set eligibility based on age
if (age >= 18) {
setField('eligible', 'Yes');
} else {
setField('eligible', 'No');
}
}
}
```
### Best Practices for JavaScript Forms
#### Performance Optimization
1. **Minimize Calculations**: Only calculate when necessary fields change
2. **Use Default Values**: Provide fallbacks for empty fields
3. **Debounce Heavy Operations**: For complex calculations, consider debouncing
4. **Cache Results**: Store calculation results when appropriate
#### Error Handling
```javascript
onFieldChange: function(fieldName, value) {
try {
if (fieldName === 'price') {
const price = parseFloat(value);
if (isNaN(price)) {
console.warn('Invalid price value:', value);
return;
}
// Perform calculation
calculateTotal(price);
}
} catch (error) {
console.error('Error in field change handler:', error);
}
}
```
#### Debugging Tips
1. **Use Console Logging**: Add console.log statements to track execution
2. **Check Field Names**: Ensure field names in code match form field names exactly
3. **Validate Data Types**: Always parse and validate input values
4. **Test Edge Cases**: Test with empty values, zero, and negative numbers
### Form Templates with JavaScript
#### CSS & JavaScript Test Form
A comprehensive template demonstrating real-time calculations:
**Field Structure:**
- `user_name` (Text): User's name
- `quantity` (Number): Quantity of items
- `price` (Number): Price per item
- `total` (Number): Calculated total (read-only)
**JavaScript Logic:**
```javascript
onLoad: function() {
console.log('Form loaded, initializing...');
setField('user_name', '');
setField('quantity', 1);
setField('price', 0);
setField('total', 0);
}
onFieldChange: function(fieldName, value) {
console.log('Field changed:', fieldName, '=', value);
if (fieldName === 'user_name') {
console.log('User name updated to:', value);
}
if (fieldName === 'quantity' || fieldName === 'price') {
const quantity = parseFloat(getField('quantity')) || 0;
const price = parseFloat(getField('price')) || 0;
const total = quantity * price;
setField('total', total.toFixed(2));
console.log('Calculated total:', total);
}
}
```
### Troubleshooting JavaScript Forms
#### Common Issues
**Calculations Not Updating**
- Verify field names match exactly (case-sensitive)
- Check browser console for JavaScript errors
- Ensure `onFieldChange` handler is properly defined
- Confirm form is in preview mode for testing
**Initial Values Not Setting**
- Make sure `onLoad` handler is defined
- Check that `setField` calls use correct field names
- Verify form data structure in browser developer tools
**Console Errors**
- **"getField/setField is not defined"**: JavaScript engine not properly initialized
- **"Cannot read property"**: Field name doesn't exist or typo in field name
- **"Unexpected token"**: Syntax error in JavaScript code
#### Debugging Workflow
1. **Open Browser Developer Tools** (F12)
2. **Check Console Tab** for error messages
3. **Add Debug Logging**:
```javascript
onFieldChange: function(fieldName, value) {
console.log('=== Field Change Debug ===');
console.log('Field:', fieldName);
console.log('New Value:', value);
console.log('Current Form Data:', Object.keys(formData));
// Your calculation logic here
}
```
4. **Test in Preview Mode** - JavaScript only executes in preview mode
5. **Verify Field Names** - Use browser inspect element to confirm field name attributes
## Advanced Features
### Custom Components

View File

@ -196,6 +196,7 @@
<!-- Preview Mode -->
<div v-if="isPreview" class="form-container">
<FormScriptEngine
ref="formScriptEngine"
:form-data="previewFormData"
:custom-script="formStore.formCustomScript"
:custom-css="formStore.formCustomCSS"
@ -205,7 +206,13 @@
@field-validate="handleScriptFieldValidate"
/>
<FormKit type="form" @submit="handlePreviewSubmit" :actions="false" v-model="previewFormData">
<FormKit
ref="previewForm"
type="form"
@submit="handlePreviewSubmit"
:actions="false"
v-model="previewFormData"
>
<div class="grid-preview-container">
<template
v-for="(component, index) in formStore.formComponents"
@ -495,26 +502,53 @@
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">Custom JavaScript</h3>
<p class="text-sm text-gray-600 mb-4">
Write custom JavaScript to add dynamic behavior to your form.
Use <code class="bg-gray-100 px-1 rounded">this.getField('fieldName')</code> to access form fields.
Write custom JavaScript to add dynamic behavior to your form. Scripts run securely in a sandboxed environment with access to form fields and utility functions.
</p>
<!-- Helper Functions Reference -->
<div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200">
<details>
<summary class="text-sm font-medium text-blue-800 cursor-pointer">📚 Available Helper Functions</summary>
<summary class="text-sm font-medium text-blue-800 cursor-pointer">📚 Core Functions</summary>
<div class="mt-2 text-xs text-blue-700 space-y-1">
<div><code>this.getField('name')</code> - Get field value</div>
<div><code>this.setField('name', value)</code> - Set field value</div>
<div><code>this.hideField('name')</code> - Hide field</div>
<div><code>this.showField('name')</code> - Show field</div>
<div><code>this.disableField('name')</code> - Disable field</div>
<div><code>this.enableField('name')</code> - Enable field</div>
<div><code>this.validateField('name')</code> - Trigger field validation</div>
<div><code>this.getAllFieldValues()</code> - Get all form values</div>
</div>
<div><code>getField('name')</code> - Get field value</div>
<div><code>setField('name', value)</code> - Set field value and trigger events</div>
<div><code>hideField('name')</code> - Hide a field</div>
<div><code>showField('name')</code> - Show a field</div>
<div><code>disableField('name')</code> - Disable a field</div>
<div><code>enableField('name')</code> - Enable a field</div>
<div><code>validateField('name')</code> - Trigger field validation</div>
<div><code>getAllFieldValues()</code> - Get all form values as object</div>
<div><code>onFieldChange(['field1', 'field2'], callback)</code> - Listen for field changes</div>
</div>
</details>
</div>
</div>
<!-- User Interface Functions -->
<div class="mb-4 p-3 bg-green-50 rounded border border-green-200">
<details>
<summary class="text-sm font-medium text-green-800 cursor-pointer">🎨 UI & Notifications</summary>
<div class="mt-2 text-xs text-green-700 space-y-1">
<div><code>showSuccess('message')</code> - Display success notification</div>
<div><code>showError('message')</code> - Display error notification</div>
<div><code>showInfo('message')</code> - Display info notification</div>
<div><code>querySelector('selector')</code> - Safe DOM querying within form</div>
</div>
</details>
</div>
<!-- Utility Functions -->
<div class="mb-4 p-3 bg-purple-50 rounded border border-purple-200">
<details>
<summary class="text-sm font-medium text-purple-800 cursor-pointer">🛠 Utilities</summary>
<div class="mt-2 text-xs text-purple-700 space-y-1">
<div><code>Math.*</code> - Mathematical functions (Math.round, Math.max, etc.)</div>
<div><code>Date</code> - Date object for date/time operations</div>
<div><code>Number()</code>, <code>String()</code>, <code>Array</code> - Type conversion</div>
<div><code>setTimeout()</code>, <code>setInterval()</code> - Timing functions</div>
<div><code>console.log()</code>, <code>console.warn()</code> - Debug logging</div>
</div>
</details>
</div>
</div>
<RsCodeMirror
@ -522,21 +556,34 @@
language="javascript"
height="300px"
placeholder="// Example: Hide/show fields based on selection
// this.onFieldChange('customer_type', (value) => {
// onFieldChange('customer_type', (value) => {
// if (value === 'business') {
// this.showField('company_name');
// this.showField('tax_id');
// showField('company_name');
// showField('tax_id');
// showInfo('Business information fields are now visible');
// } else {
// this.hideField('company_name');
// this.hideField('tax_id');
// hideField('company_name');
// hideField('tax_id');
// }
// });
// Example: Calculate total
// this.onFieldChange(['quantity', 'price'], () => {
// const qty = this.getField('quantity') || 0;
// const price = this.getField('price') || 0;
// this.setField('total', qty * price);
// Example: Real-time calculations
// onFieldChange(['quantity', 'price'], () => {
// const qty = Number(getField('quantity')) || 0;
// const price = Number(getField('price')) || 0;
// const total = Math.round((qty * price) * 100) / 100;
// setField('total', total);
//
// if (total > 1000) {
// showSuccess('Discount applied for orders over $1000!');
// }
// });
// Example: Conditional validation
// onFieldChange('email', (value) => {
// if (value && !value.includes('@')) {
// showError('Please enter a valid email address');
// }
// });"
/>
</div>
@ -598,8 +645,18 @@
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">Form Events</h3>
<p class="text-sm text-gray-600 mb-4">
Configure when your custom scripts should run.
Configure when your custom scripts should run. Field change events are detected automatically in real-time when users interact with form fields.
</p>
<!-- Important Note -->
<div class="mb-4 p-3 bg-amber-50 rounded border border-amber-200">
<div class="flex items-start">
<Icon name="material-symbols:info" class="w-4 h-4 text-amber-600 mr-2 mt-0.5" />
<div class="text-xs text-amber-700">
<strong>Note:</strong> The "On Field Change" event must be enabled for <code>onFieldChange()</code> callbacks to work in your scripts.
</div>
</div>
</div>
</div>
<div class="space-y-4">
@ -627,14 +684,20 @@
<div class="border rounded p-4">
<h4 class="font-medium mb-2">Script Execution Mode</h4>
<div class="space-y-2">
<label class="flex items-center">
<input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2">
<span class="text-sm">Safe Mode (Recommended) - Limited but secure</span>
<div class="space-y-3">
<label class="flex items-start">
<input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2 mt-0.5">
<div>
<span class="text-sm font-medium text-green-700">Safe Mode (Recommended)</span>
<p class="text-xs text-gray-600 mt-1">Scripts run in a secure sandbox with access only to form functions. Best for most use cases.</p>
</div>
</label>
<label class="flex items-center">
<input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2">
<span class="text-sm">Advanced Mode - Full JavaScript access</span>
<label class="flex items-start">
<input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2 mt-0.5">
<div>
<span class="text-sm font-medium text-orange-700">Advanced Mode</span>
<p class="text-xs text-gray-600 mt-1">Provides broader JavaScript access but with security restrictions. Use with caution.</p>
</div>
</label>
</div>
</div>
@ -796,6 +859,7 @@
<script setup>
import { useFormBuilderStore } from "~/stores/formBuilder";
import { nextTick } from 'vue';
import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
@ -838,6 +902,8 @@ const showDropdown = ref(false);
const showTemplatesModal = ref(false);
const showFieldSettings = ref(false);
const showFieldSettingsPanel = ref(false);
const previewForm = ref(null);
const formScriptEngine = ref(null);
// Settings tabs configuration
const settingsTabs = [
@ -1617,118 +1683,127 @@ const previewFormData = ref({});
// Initialize preview form data with default values
watchEffect(() => {
const newFormData = {};
console.log('[FormBuilder] watchEffect for previewFormData initialization. Current form components:', formStore.formComponents.length);
const existingFormData = { ...previewFormData.value }; // Preserve current user inputs
let newDefaults = {};
let hasNewComponents = false;
formStore.formComponents.forEach(component => {
if (component.props.name) {
// Set default values based on component type
switch (component.type) {
case 'checkbox':
newFormData[component.props.name] = [];
break;
case 'number':
case 'range':
newFormData[component.props.name] = component.props.value || 0;
break;
case 'switch':
newFormData[component.props.name] = component.props.value !== undefined ? component.props.value : false;
break;
case 'color':
newFormData[component.props.name] = component.props.value || '#3b82f6';
break;
case 'hidden':
newFormData[component.props.name] = component.props.value || '';
break;
case 'image-preview':
case 'info-display':
case 'button':
// These are display-only components and don't need form data values
break;
case 'repeating-group':
// Initialize with one empty group or use minItems to determine how many initial groups
const initialGroups = [];
const minItems = component.props.minItems || 1;
// Create the specified number of initial groups
for (let i = 0; i < minItems; i++) {
// Create an object with fields from the configuration
const group = {};
if (Array.isArray(component.props.fields)) {
component.props.fields.forEach(field => {
// Initialize each field with appropriate default value
switch (field.type) {
case 'number':
group[field.name] = 0;
break;
case 'checkbox':
group[field.name] = [];
break;
case 'select':
// If there's a default value in the options, use it
const defaultOption = Array.isArray(field.options) && field.options.length > 0
? field.options[0].value
: '';
group[field.name] = field.value || defaultOption || '';
break;
default:
group[field.name] = field.value || '';
}
});
// If field is not already in existingFormData, it's a new component or needs initialization
if (!existingFormData.hasOwnProperty(component.props.name)) {
hasNewComponents = true;
// Set default values based on component type
switch (component.type) {
case 'checkbox':
newDefaults[component.props.name] = [];
break;
case 'number':
case 'range':
newDefaults[component.props.name] = component.props.value || 0;
break;
case 'switch':
newDefaults[component.props.name] = component.props.value !== undefined ? component.props.value : false;
break;
case 'color':
newDefaults[component.props.name] = component.props.value || '#3b82f6';
break;
case 'hidden':
newDefaults[component.props.name] = component.props.value || '';
break;
case 'repeating-group':
const initialGroups = [];
const minItems = component.props.minItems || 1;
for (let i = 0; i < minItems; i++) {
const group = {};
if (Array.isArray(component.props.fields)) {
component.props.fields.forEach(field => {
switch (field.type) {
case 'number': group[field.name] = 0; break;
case 'checkbox': group[field.name] = []; break;
case 'select':
const defaultOption = Array.isArray(field.options) && field.options.length > 0 ? field.options[0].value : '';
group[field.name] = field.value || defaultOption || '';
break;
default: group[field.name] = field.value || '';
}
});
}
initialGroups.push(group);
}
initialGroups.push(group);
}
newFormData[component.props.name] = initialGroups;
break;
case 'dynamic-list':
// Initialize with default items from configuration
newFormData[component.props.name] = Array.isArray(component.props.defaultItems)
? [...component.props.defaultItems]
: [];
break;
case 'select':
case 'radio':
// For select and radio, initialize with first option value if available
if (Array.isArray(component.props.options) && component.props.options.length > 0) {
newFormData[component.props.name] = component.props.value || component.props.options[0].value;
} else {
newFormData[component.props.name] = '';
}
break;
case 'date':
case 'time':
case 'datetime-local':
// Initialize with current date/time
newFormData[component.props.name] = component.props.value || '';
break;
case 'file':
// Initialize file inputs as null
newFormData[component.props.name] = null;
break;
default:
// For text, textarea, email, password, url, tel, etc.
newFormData[component.props.name] = component.props.value || '';
newDefaults[component.props.name] = initialGroups;
break;
case 'dynamic-list':
newDefaults[component.props.name] = Array.isArray(component.props.defaultItems) ? [...component.props.defaultItems] : [];
break;
case 'select':
case 'radio':
if (Array.isArray(component.props.options) && component.props.options.length > 0) {
newDefaults[component.props.name] = component.props.value || component.props.options[0].value;
} else {
newDefaults[component.props.name] = '';
}
break;
case 'date':
case 'time':
case 'datetime-local':
case 'file': // file inputs are handled by FormKit, typically initialized to null or specific file objects
newDefaults[component.props.name] = component.props.value || null;
break;
// image-preview, info-display, button don't usually hold data in previewFormData directly
case 'image-preview':
case 'info-display':
case 'button':
break;
default:
newDefaults[component.props.name] = component.props.value || '';
}
}
}
});
previewFormData.value = newFormData;
// Only update previewFormData if it's the initial load (empty) or new components were added that need defaults
// This prevents overwriting user input when existing components change their props (which also triggers this watchEffect)
const isInitialLoad = Object.keys(previewFormData.value).length === 0 && Object.keys(newDefaults).length > 0;
if (isInitialLoad || hasNewComponents) {
console.log('[FormBuilder] Initializing/merging preview form data. Initial load:', isInitialLoad, 'Has new components:', hasNewComponents);
previewFormData.value = { ...existingFormData, ...newDefaults };
console.log('[FormBuilder] Preview form data after init/merge:', previewFormData.value);
} else {
console.log('[FormBuilder] Skipping full previewFormData re-initialization to preserve user input.');
}
});
// Handle script-driven field changes
const handleScriptFieldChange = ({ fieldName, value }) => {
previewFormData.value[fieldName] = value;
console.log('[FormBuilder] Script field change:', fieldName, '=', value);
// Update the reactive form data
const newFormData = {
...previewFormData.value,
[fieldName]: value
};
previewFormData.value = newFormData;
// Make form data accessible to component previews
formStore.updatePreviewFormData(previewFormData.value);
formStore.updatePreviewFormData(newFormData);
// Try to force FormKit form to update
nextTick(() => {
console.log('[FormBuilder] Updated form data:', newFormData);
// Try to access the FormKit form node and update it directly
if (previewForm.value && previewForm.value.node) {
try {
previewForm.value.node.input(newFormData);
console.log('[FormBuilder] Force updated FormKit form');
} catch (error) {
console.warn('[FormBuilder] Could not force update FormKit form:', error);
}
}
});
};
// Handle script-driven field validation
@ -1737,11 +1812,46 @@ const handleScriptFieldValidate = ({ fieldName }) => {
console.log(`Validating field: ${fieldName}`);
};
// Make form data accessible to component previews
watchEffect(() => {
// Handle FormKit form input events
const handleFormKitInput = (formData, node) => {
console.log('[FormBuilder] FormKit input event received!');
console.log('[FormBuilder] FormKit formData:', JSON.parse(JSON.stringify(formData)));
console.log('[FormBuilder] Current previewFormData before update:', JSON.parse(JSON.stringify(previewFormData.value)));
// Update our reactive form data - this should trigger the FormScriptEngine watcher
const oldPreviewData = { ...previewFormData.value };
previewFormData.value = { ...formData };
console.log('[FormBuilder] Updated previewFormData to:', JSON.parse(JSON.stringify(previewFormData.value)));
console.log('[FormBuilder] Did previewFormData actually change?', JSON.stringify(oldPreviewData) !== JSON.stringify(previewFormData.value));
// Make form data accessible to component previews
formStore.updatePreviewFormData(previewFormData.value);
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
};
// Make form data accessible to component previews (for UI rendering, not for triggering script engine)
watchEffect(() => {
if (formStore) { // Ensure formStore is available
formStore.updatePreviewFormData(previewFormData.value);
}
});
// Watch for changes in previewFormData to trigger FormScriptEngine
watch(() => previewFormData.value, (newData, oldData) => {
if (!isPreview.value) return; // Only in preview mode
console.log('[FormBuilder] previewFormData watcher triggered!');
console.log('[FormBuilder] New data:', JSON.parse(JSON.stringify(newData)));
console.log('[FormBuilder] Old data:', oldData ? JSON.parse(JSON.stringify(oldData)) : 'undefined');
// Update form store
formStore.updatePreviewFormData(newData);
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
}, { deep: true, immediate: false });
const navigateToManage = () => {
// If already confirmed or no unsaved changes, navigate directly
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {

View File

@ -312,6 +312,35 @@
"null"
]
},
"customScript": {
"type": [
"string",
"null"
]
},
"customCSS": {
"type": [
"string",
"null"
]
},
"formEvents": {
"type": [
"number",
"string",
"boolean",
"object",
"array",
"null"
]
},
"scriptMode": {
"type": [
"string",
"null"
],
"default": "safe"
},
"formStatus": {
"type": "string",
"default": "active"

View File

@ -823,6 +823,11 @@ export const useFormBuilderStore = defineStore('formBuilder', {
});
},
// Update preview form data
updatePreviewFormData(data) {
this.previewFormData = { ...data };
},
// Optimize the grid layout by analyzing the current components
// and adjusting their sizes to fill available spaces
optimizeGridLayout() {