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:
parent
33982fb093
commit
72c61184ae
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
@ -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);
|
||||
}`
|
||||
}
|
||||
];
|
||||
|
@ -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>
|
||||
|
@ -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
|
767
doc/form-builder/JAVASCRIPT_API.md
Normal file
767
doc/form-builder/JAVASCRIPT_API.md
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user