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" :multiple="component.props.multiple || undefined"
:maxSize="component.props.maxSize || undefined" :maxSize="component.props.maxSize || undefined"
:maxFiles="component.props.maxFiles || 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' ? { :classes="component.type === 'checkbox' ? {
wrapper: 'mb-1', wrapper: 'mb-1',
options: 'space-y-0.5' options: 'space-y-0.5'
@ -124,7 +121,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Divider --> <!-- Divider -->
<div v-else-if="component.type === 'divider'" class="py-4"> <div v-else-if="component.type === 'divider'" class="py-4">
<hr class="border-gray-200" /> <hr class="border-gray-200" />
@ -349,7 +346,7 @@
</div> </div>
<!-- Unknown Component Type Fallback --> <!-- Unknown Component Type Fallback -->
<div v-else class="p-4 border border-dashed border-gray-300 rounded"> <div v-else class="p-4 border border-dashed border-gray-300 rounded">
<div class="text-gray-500">Unknown component type: {{ component.type }}</div> <div class="text-gray-500">Unknown component type: {{ component.type }}</div>

View File

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

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<!-- Custom CSS Injection --> <!-- Custom CSS Injection - Remove scoped to allow global styling -->
<component :is="'style'" v-if="customCSS" scoped> <component :is="'style'" v-if="customCSS">
{{ customCSS }} {{ customCSS }}
</component> </component>
</div> </div>
@ -44,16 +44,31 @@ const emit = defineEmits(['field-change', 'form-submit', 'field-validate']);
const scriptContext = ref(null); const scriptContext = ref(null);
const fieldChangeHandlers = ref(new Map()); const fieldChangeHandlers = ref(new Map());
const isScriptInitialized = ref(false); const isScriptInitialized = ref(false);
const previousFormData = ref({});
// Create safe execution context // Create safe execution context
const createScriptContext = () => { const createScriptContext = () => {
const context = { const context = {
// Form field interaction methods
getField: (fieldName) => { getField: (fieldName) => {
return props.formData[fieldName]; return props.formData[fieldName];
}, },
setField: (fieldName, value) => { 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 }); 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 // Utility functions
console: { console: {
log: (...args) => console.log('[Form Script]', ...args), log: (...args) => console.log('[Form Script]', ...args),
@ -134,7 +189,13 @@ const createScriptContext = () => {
String: String, String: String,
// Number utilities // Number utilities
Number: Number Number: Number,
// Additional utility functions
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearInterval: clearInterval
}; };
return context; return context;
@ -183,20 +244,26 @@ const executeScript = (script, context) => {
scriptFunction.call(null, context); scriptFunction.call(null, context);
} }
} catch (error) { } catch (error) {
console.error('Script execution error:', error); console.error('[FormScriptEngine] Script execution error:', error);
// Could emit an error event here for user feedback // Show error to user if showError function is available
if (context.showError) {
context.showError(`Script Error: ${error.message}`);
}
} }
}; };
// Initialize script engine // Initialize script engine
const initializeScript = async () => { const initializeScript = () => {
if (!props.customScript || isScriptInitialized.value) return; fieldChangeHandlers.value.clear();
await nextTick(); // Ensure DOM is ready
// Create script context
scriptContext.value = createScriptContext(); 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); executeScript(props.customScript, scriptContext.value);
} }
@ -205,44 +272,63 @@ const initializeScript = async () => {
// Handle field changes // Handle field changes
const handleFieldChange = (fieldName, newValue, oldValue) => { const handleFieldChange = (fieldName, newValue, oldValue) => {
if (!fieldChangeHandlers.value.has(fieldName)) return; if (!fieldChangeHandlers.value.has(fieldName)) {
return;
}
const handlers = fieldChangeHandlers.value.get(fieldName); const handlers = fieldChangeHandlers.value.get(fieldName);
handlers.forEach(handler => { handlers.forEach((handler, index) => {
try { try {
if (typeof handler === 'function') { if (typeof handler === 'function') {
handler.call(scriptContext.value, newValue, oldValue); handler.call(scriptContext.value, newValue, oldValue);
} }
} catch (error) { } 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 for script changes and reinitialize
watch(() => props.customScript, () => { watch(() => props.customScript, (newScript, oldScript) => {
if (isScriptInitialized.value) { if (newScript !== oldScript) {
isScriptInitialized.value = false; isScriptInitialized.value = false;
fieldChangeHandlers.value.clear();
initializeScript(); 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 // Lifecycle hooks
onMounted(() => { onMounted(() => {
initializeScript(); setTimeout(() => {
initializeScript();
}, 500); // Delay to ensure parent DOM and initial props are settled
}); });
onUnmounted(() => { onUnmounted(() => {
@ -259,13 +345,12 @@ defineExpose({
}, },
getContext: () => scriptContext.value, getContext: () => scriptContext.value,
reinitialize: () => { reinitialize: () => {
isScriptInitialized.value = false; isScriptInitialized.value = false;
fieldChangeHandlers.value.clear(); initializeScript();
initializeScript();
} }
}); });
</script> </script>
<style scoped> <style>
/* Component doesn't render visible content */ /* Component doesn't render visible content */
</style> </style>

View File

@ -141,6 +141,210 @@ const activeCategory = ref('all');
// Templates data // Templates data
const templates = [ 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', id: 'contact-form',
name: 'Contact Form', name: 'Contact Form',
@ -2187,6 +2391,7 @@ this.onFieldChange('budget', (value) => {
// Update display somewhere in the form // Update display somewhere in the form
console.log('Budget formatted:', formatted); console.log('Budget formatted:', formatted);
this.showInfo('Budget updated: ' + formatted);
}); });
// OTP validation // OTP validation
@ -2201,18 +2406,32 @@ this.onFieldChange('otp_code', (value) => {
} }
}, 1000); }, 1000);
} }
});`, });
// Initialize form
console.log('Advanced form script loaded successfully!');`,
css: `.form-container { css: `.form-container {
background: #f8fafc; background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 100%);
border-radius: 12px; border-radius: 16px;
padding: 2rem; 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"] { .form-field[data-name="info_display"] {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
} }
.form-field[data-name="dropzone"] { .form-field[data-name="dropzone"] {
@ -2225,6 +2444,8 @@ this.onFieldChange('otp_code', (value) => {
.form-field[data-name="dropzone"]:hover { .form-field[data-name="dropzone"]:hover {
border-color: #3b82f6; border-color: #3b82f6;
background: #eff6ff; background: #eff6ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
} }
.form-field[data-name="otp_code"] input { .form-field[data-name="otp_code"] input {
@ -2232,6 +2453,19 @@ this.onFieldChange('otp_code', (value) => {
font-family: monospace; font-family: monospace;
font-size: 1.2rem; font-size: 1.2rem;
letter-spacing: 0.5rem; 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> ></Icon>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot name="body"></slot> <slot name="body"></slot>
<slot v-if="!$slots.body"></slot> <slot v-if="!$slots.body"></slot>
</div> </div>
<div v-if="!hideFooter" class="modal-footer"> <div v-if="!hideFooter" class="modal-footer">
<slot name="footer"></slot> <slot name="footer"></slot>

View File

@ -19,6 +19,8 @@ The documentation is organized into the following sections:
### Form Builder ### Form Builder
- [User Guide](./form-builder/USER_GUIDE.md) - How to use the 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 - [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 ### Process Execution
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface - [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. 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) => ({ formConfig: (state) => ({
id: state.formId, id: state.formId,
uuid: state.formUUID, uuid: state.formUUID,
name: state.formName, name: state.formName,
description: state.formDescription, description: state.formDescription,
components: state.formComponents 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 ## 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 ### Unit Tests
```javascript ```javascript

View File

@ -106,7 +106,7 @@ Perfect for collecting simple data:
**Text Field** **Text Field**
- Single-line text input - Single-line text input
- Smart width: Small (33%) - Smart width: Small (33%)
- Use for: Names, titles, short answers - Use for: Names, titles, short answers
- Quick settings: Label, name, placeholder, required - Quick settings: Label, name, placeholder, required
**Text Area** **Text Area**
@ -119,7 +119,7 @@ Perfect for collecting simple data:
- Numeric input with validation - Numeric input with validation
- Smart width: Narrow (25%) - Smart width: Narrow (25%)
- Use for: Age, quantity, prices - Use for: Age, quantity, prices
- Features: Min/max limits, step values - Features: Min/max limits, step values
**Email Field** **Email Field**
- Email input with validation - Email input with validation
@ -131,7 +131,7 @@ Perfect for collecting simple data:
- Secure password input - Secure password input
- Smart width: Medium (50%) - Smart width: Medium (50%)
- Use for: Authentication forms - Use for: Authentication forms
- Features: Password masking, strength indicators - Features: Password masking, strength indicators
### Selection Components ### Selection Components
For choosing from options: For choosing from options:
@ -151,7 +151,7 @@ For choosing from options:
**Radio Group** **Radio Group**
- Single choice from group - Single choice from group
- Smart width: Full (100%) - Smart width: Full (100%)
- Use for: Exclusive choices, yes/no questions - Use for: Exclusive choices, yes/no questions
- Features: Button or traditional styles - Features: Button or traditional styles
**Switch Toggle** **Switch Toggle**
@ -188,7 +188,7 @@ Specialized functionality:
- Standard file input - Standard file input
- Smart width: Wide (75%) - Smart width: Wide (75%)
- Use for: Document uploads, attachments - Use for: Document uploads, attachments
- Features: File type restrictions, size limits - Features: File type restrictions, size limits
**File Drop Zone** **File Drop Zone**
- Drag & drop upload area - Drag & drop upload area
@ -214,7 +214,7 @@ Form structure and organization:
**Heading** **Heading**
- Section titles and organization - Section titles and organization
- Smart width: Full (100%) - Smart width: Full (100%)
- Use for: Form sections, categories - Use for: Form sections, categories
- Features: Multiple heading levels (H1-H6) - Features: Multiple heading levels (H1-H6)
**Paragraph** **Paragraph**
@ -311,7 +311,7 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
3. **Logical Grouping**: Use similar widths for related fields 3. **Logical Grouping**: Use similar widths for related fields
4. **Space Efficiency**: Let the system auto-optimize grid placement 4. **Space Efficiency**: Let the system auto-optimize grid placement
### User Experience ### User Experience
1. **Clear Labels**: Use descriptive, action-oriented labels 1. **Clear Labels**: Use descriptive, action-oriented labels
2. **Helpful Placeholders**: Provide examples in placeholder text 2. **Helpful Placeholders**: Provide examples in placeholder text
3. **Progressive Disclosure**: Use sections for complex forms 3. **Progressive Disclosure**: Use sections for complex forms
@ -362,7 +362,7 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
**Performance Issues** **Performance Issues**
- Reduce number of components - Reduce number of components
- Simplify conditional logic - Simplify conditional logic
- Clear browser cache - Clear browser cache
### Getting Help ### Getting Help
- **Documentation**: Complete technical guides available - **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 - **Support**: Contact support team for assistance
- **Community**: Join user community forums - **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 ## Advanced Features
### Custom Components ### Custom Components

View File

@ -196,6 +196,7 @@
<!-- Preview Mode --> <!-- Preview Mode -->
<div v-if="isPreview" class="form-container"> <div v-if="isPreview" class="form-container">
<FormScriptEngine <FormScriptEngine
ref="formScriptEngine"
:form-data="previewFormData" :form-data="previewFormData"
:custom-script="formStore.formCustomScript" :custom-script="formStore.formCustomScript"
:custom-css="formStore.formCustomCSS" :custom-css="formStore.formCustomCSS"
@ -205,7 +206,13 @@
@field-validate="handleScriptFieldValidate" @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"> <div class="grid-preview-container">
<template <template
v-for="(component, index) in formStore.formComponents" v-for="(component, index) in formStore.formComponents"
@ -495,26 +502,53 @@
<div class="mb-4"> <div class="mb-4">
<h3 class="text-lg font-medium mb-2">Custom JavaScript</h3> <h3 class="text-lg font-medium mb-2">Custom JavaScript</h3>
<p class="text-sm text-gray-600 mb-4"> <p class="text-sm text-gray-600 mb-4">
Write custom JavaScript to add dynamic behavior to your form. 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.
Use <code class="bg-gray-100 px-1 rounded">this.getField('fieldName')</code> to access form fields.
</p> </p>
<!-- Helper Functions Reference --> <!-- Helper Functions Reference -->
<div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200"> <div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200">
<details> <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 class="mt-2 text-xs text-blue-700 space-y-1">
<div><code>this.getField('name')</code> - Get field value</div> <div><code>getField('name')</code> - Get field value</div>
<div><code>this.setField('name', value)</code> - Set field value</div> <div><code>setField('name', value)</code> - Set field value and trigger events</div>
<div><code>this.hideField('name')</code> - Hide field</div> <div><code>hideField('name')</code> - Hide a field</div>
<div><code>this.showField('name')</code> - Show field</div> <div><code>showField('name')</code> - Show a field</div>
<div><code>this.disableField('name')</code> - Disable field</div> <div><code>disableField('name')</code> - Disable a field</div>
<div><code>this.enableField('name')</code> - Enable field</div> <div><code>enableField('name')</code> - Enable a field</div>
<div><code>this.validateField('name')</code> - Trigger field validation</div> <div><code>validateField('name')</code> - Trigger field validation</div>
<div><code>this.getAllFieldValues()</code> - Get all form values</div> <div><code>getAllFieldValues()</code> - Get all form values as object</div>
</div> <div><code>onFieldChange(['field1', 'field2'], callback)</code> - Listen for field changes</div>
</div>
</details> </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> </div>
<RsCodeMirror <RsCodeMirror
@ -522,21 +556,34 @@
language="javascript" language="javascript"
height="300px" height="300px"
placeholder="// Example: Hide/show fields based on selection placeholder="// Example: Hide/show fields based on selection
// this.onFieldChange('customer_type', (value) => { // onFieldChange('customer_type', (value) => {
// if (value === 'business') { // if (value === 'business') {
// this.showField('company_name'); // showField('company_name');
// this.showField('tax_id'); // showField('tax_id');
// showInfo('Business information fields are now visible');
// } else { // } else {
// this.hideField('company_name'); // hideField('company_name');
// this.hideField('tax_id'); // hideField('tax_id');
// } // }
// }); // });
// Example: Calculate total // Example: Real-time calculations
// this.onFieldChange(['quantity', 'price'], () => { // onFieldChange(['quantity', 'price'], () => {
// const qty = this.getField('quantity') || 0; // const qty = Number(getField('quantity')) || 0;
// const price = this.getField('price') || 0; // const price = Number(getField('price')) || 0;
// this.setField('total', qty * price); // 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> </div>
@ -598,8 +645,18 @@
<div class="mb-4"> <div class="mb-4">
<h3 class="text-lg font-medium mb-2">Form Events</h3> <h3 class="text-lg font-medium mb-2">Form Events</h3>
<p class="text-sm text-gray-600 mb-4"> <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> </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>
<div class="space-y-4"> <div class="space-y-4">
@ -627,14 +684,20 @@
<div class="border rounded p-4"> <div class="border rounded p-4">
<h4 class="font-medium mb-2">Script Execution Mode</h4> <h4 class="font-medium mb-2">Script Execution Mode</h4>
<div class="space-y-2"> <div class="space-y-3">
<label class="flex items-center"> <label class="flex items-start">
<input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2"> <input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2 mt-0.5">
<span class="text-sm">Safe Mode (Recommended) - Limited but secure</span> <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>
<label class="flex items-center"> <label class="flex items-start">
<input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2"> <input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2 mt-0.5">
<span class="text-sm">Advanced Mode - Full JavaScript access</span> <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> </label>
</div> </div>
</div> </div>
@ -796,6 +859,7 @@
<script setup> <script setup>
import { useFormBuilderStore } from "~/stores/formBuilder"; import { useFormBuilderStore } from "~/stores/formBuilder";
import { nextTick } from 'vue';
import FormBuilderHistory from "~/components/FormBuilderHistory.vue"; import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
import FormTemplatesModal from '~/components/FormTemplatesModal.vue'; import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue'; import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
@ -838,6 +902,8 @@ const showDropdown = ref(false);
const showTemplatesModal = ref(false); const showTemplatesModal = ref(false);
const showFieldSettings = ref(false); const showFieldSettings = ref(false);
const showFieldSettingsPanel = ref(false); const showFieldSettingsPanel = ref(false);
const previewForm = ref(null);
const formScriptEngine = ref(null);
// Settings tabs configuration // Settings tabs configuration
const settingsTabs = [ const settingsTabs = [
@ -1617,118 +1683,127 @@ const previewFormData = ref({});
// Initialize preview form data with default values // Initialize preview form data with default values
watchEffect(() => { 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 => { formStore.formComponents.forEach(component => {
if (component.props.name) { if (component.props.name) {
// Set default values based on component type // If field is not already in existingFormData, it's a new component or needs initialization
switch (component.type) { if (!existingFormData.hasOwnProperty(component.props.name)) {
case 'checkbox': hasNewComponents = true;
newFormData[component.props.name] = []; // Set default values based on component type
break; switch (component.type) {
case 'checkbox':
case 'number': newDefaults[component.props.name] = [];
case 'range': break;
newFormData[component.props.name] = component.props.value || 0; case 'number':
break; case 'range':
newDefaults[component.props.name] = component.props.value || 0;
case 'switch': break;
newFormData[component.props.name] = component.props.value !== undefined ? component.props.value : false; case 'switch':
break; newDefaults[component.props.name] = component.props.value !== undefined ? component.props.value : false;
break;
case 'color': case 'color':
newFormData[component.props.name] = component.props.value || '#3b82f6'; newDefaults[component.props.name] = component.props.value || '#3b82f6';
break; break;
case 'hidden':
case 'hidden': newDefaults[component.props.name] = component.props.value || '';
newFormData[component.props.name] = component.props.value || ''; break;
break; case 'repeating-group':
const initialGroups = [];
case 'image-preview': const minItems = component.props.minItems || 1;
case 'info-display': for (let i = 0; i < minItems; i++) {
case 'button': const group = {};
// These are display-only components and don't need form data values if (Array.isArray(component.props.fields)) {
break; component.props.fields.forEach(field => {
switch (field.type) {
case 'repeating-group': case 'number': group[field.name] = 0; break;
// Initialize with one empty group or use minItems to determine how many initial groups case 'checkbox': group[field.name] = []; break;
const initialGroups = []; case 'select':
const minItems = component.props.minItems || 1; const defaultOption = Array.isArray(field.options) && field.options.length > 0 ? field.options[0].value : '';
group[field.name] = field.value || defaultOption || '';
// Create the specified number of initial groups break;
for (let i = 0; i < minItems; i++) { default: group[field.name] = field.value || '';
// Create an object with fields from the configuration }
const group = {}; });
if (Array.isArray(component.props.fields)) { }
component.props.fields.forEach(field => { initialGroups.push(group);
// 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 || '';
}
});
} }
initialGroups.push(group); newDefaults[component.props.name] = initialGroups;
} break;
case 'dynamic-list':
newFormData[component.props.name] = initialGroups; newDefaults[component.props.name] = Array.isArray(component.props.defaultItems) ? [...component.props.defaultItems] : [];
break; break;
case 'select':
case 'dynamic-list': case 'radio':
// Initialize with default items from configuration if (Array.isArray(component.props.options) && component.props.options.length > 0) {
newFormData[component.props.name] = Array.isArray(component.props.defaultItems) newDefaults[component.props.name] = component.props.value || component.props.options[0].value;
? [...component.props.defaultItems] } else {
: []; newDefaults[component.props.name] = '';
break; }
break;
case 'select': case 'date':
case 'radio': case 'time':
// For select and radio, initialize with first option value if available case 'datetime-local':
if (Array.isArray(component.props.options) && component.props.options.length > 0) { case 'file': // file inputs are handled by FormKit, typically initialized to null or specific file objects
newFormData[component.props.name] = component.props.value || component.props.options[0].value; newDefaults[component.props.name] = component.props.value || null;
} else { break;
newFormData[component.props.name] = ''; // image-preview, info-display, button don't usually hold data in previewFormData directly
} case 'image-preview':
break; case 'info-display':
case 'button':
case 'date': break;
case 'time': default:
case 'datetime-local': newDefaults[component.props.name] = component.props.value || '';
// 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 || '';
} }
} }
}); });
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 // Handle script-driven field changes
const handleScriptFieldChange = ({ fieldName, value }) => { 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 // 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 // Handle script-driven field validation
@ -1737,11 +1812,46 @@ const handleScriptFieldValidate = ({ fieldName }) => {
console.log(`Validating field: ${fieldName}`); console.log(`Validating field: ${fieldName}`);
}; };
// Make form data accessible to component previews // Handle FormKit form input events
watchEffect(() => { 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); 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 = () => { const navigateToManage = () => {
// If already confirmed or no unsaved changes, navigate directly // If already confirmed or no unsaved changes, navigate directly
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) { if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {

View File

@ -312,6 +312,35 @@
"null" "null"
] ]
}, },
"customScript": {
"type": [
"string",
"null"
]
},
"customCSS": {
"type": [
"string",
"null"
]
},
"formEvents": {
"type": [
"number",
"string",
"boolean",
"object",
"array",
"null"
]
},
"scriptMode": {
"type": [
"string",
"null"
],
"default": "safe"
},
"formStatus": { "formStatus": {
"type": "string", "type": "string",
"default": "active" "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 // Optimize the grid layout by analyzing the current components
// and adjusting their sizes to fill available spaces // and adjusting their sizes to fill available spaces
optimizeGridLayout() { optimizeGridLayout() {