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"
|
:multiple="component.props.multiple || undefined"
|
||||||
:maxSize="component.props.maxSize || undefined"
|
:maxSize="component.props.maxSize || undefined"
|
||||||
:maxFiles="component.props.maxFiles || undefined"
|
:maxFiles="component.props.maxFiles || undefined"
|
||||||
:preserve-events="isPreview"
|
|
||||||
@input.capture.stop="isPreview ? $event.stopPropagation() : null"
|
|
||||||
@click.capture.stop="isPreview ? $event.stopPropagation() : null"
|
|
||||||
:classes="component.type === 'checkbox' ? {
|
:classes="component.type === 'checkbox' ? {
|
||||||
wrapper: 'mb-1',
|
wrapper: 'mb-1',
|
||||||
options: 'space-y-0.5'
|
options: 'space-y-0.5'
|
||||||
@ -124,7 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<div v-else-if="component.type === 'divider'" class="py-4">
|
<div v-else-if="component.type === 'divider'" class="py-4">
|
||||||
<hr class="border-gray-200" />
|
<hr class="border-gray-200" />
|
||||||
@ -349,7 +346,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Unknown Component Type Fallback -->
|
<!-- Unknown Component Type Fallback -->
|
||||||
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
|
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
|
||||||
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>
|
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>
|
||||||
|
@ -446,8 +446,8 @@
|
|||||||
name="maxFiles"
|
name="maxFiles"
|
||||||
v-model="configModel.maxFiles"
|
v-model="configModel.maxFiles"
|
||||||
help="Maximum number of files"
|
help="Maximum number of files"
|
||||||
min="1"
|
min="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Custom CSS Injection -->
|
<!-- Custom CSS Injection - Remove scoped to allow global styling -->
|
||||||
<component :is="'style'" v-if="customCSS" scoped>
|
<component :is="'style'" v-if="customCSS">
|
||||||
{{ customCSS }}
|
{{ customCSS }}
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
@ -44,16 +44,31 @@ const emit = defineEmits(['field-change', 'form-submit', 'field-validate']);
|
|||||||
const scriptContext = ref(null);
|
const scriptContext = ref(null);
|
||||||
const fieldChangeHandlers = ref(new Map());
|
const fieldChangeHandlers = ref(new Map());
|
||||||
const isScriptInitialized = ref(false);
|
const isScriptInitialized = ref(false);
|
||||||
|
const previousFormData = ref({});
|
||||||
|
|
||||||
// Create safe execution context
|
// Create safe execution context
|
||||||
const createScriptContext = () => {
|
const createScriptContext = () => {
|
||||||
const context = {
|
const context = {
|
||||||
// Form field interaction methods
|
|
||||||
getField: (fieldName) => {
|
getField: (fieldName) => {
|
||||||
return props.formData[fieldName];
|
return props.formData[fieldName];
|
||||||
},
|
},
|
||||||
|
|
||||||
setField: (fieldName, value) => {
|
setField: (fieldName, value) => {
|
||||||
|
// Try to find the FormKit input element and update it directly
|
||||||
|
const fieldElement = document.querySelector(`[data-name="${fieldName}"] input, [data-name="${fieldName}"] select, [data-name="${fieldName}"] textarea`);
|
||||||
|
if (fieldElement) {
|
||||||
|
fieldElement.value = value;
|
||||||
|
|
||||||
|
// Trigger input event to notify FormKit of the change
|
||||||
|
const inputEvent = new Event('input', { bubbles: true });
|
||||||
|
fieldElement.dispatchEvent(inputEvent);
|
||||||
|
|
||||||
|
// Also trigger change event
|
||||||
|
const changeEvent = new Event('change', { bubbles: true });
|
||||||
|
fieldElement.dispatchEvent(changeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also emit the event for the parent component
|
||||||
emit('field-change', { fieldName, value });
|
emit('field-change', { fieldName, value });
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -107,6 +122,46 @@ const createScriptContext = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Add missing helper functions
|
||||||
|
showSuccess: (message) => {
|
||||||
|
// Create a simple success notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg z-50';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(notification)) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
showError: (message) => {
|
||||||
|
// Create a simple error notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded shadow-lg z-50';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(notification)) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
showInfo: (message) => {
|
||||||
|
// Create a simple info notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed top-4 right-4 bg-blue-500 text-white px-4 py-2 rounded shadow-lg z-50';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(notification)) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
console: {
|
console: {
|
||||||
log: (...args) => console.log('[Form Script]', ...args),
|
log: (...args) => console.log('[Form Script]', ...args),
|
||||||
@ -134,7 +189,13 @@ const createScriptContext = () => {
|
|||||||
String: String,
|
String: String,
|
||||||
|
|
||||||
// Number utilities
|
// Number utilities
|
||||||
Number: Number
|
Number: Number,
|
||||||
|
|
||||||
|
// Additional utility functions
|
||||||
|
setTimeout: setTimeout,
|
||||||
|
setInterval: setInterval,
|
||||||
|
clearTimeout: clearTimeout,
|
||||||
|
clearInterval: clearInterval
|
||||||
};
|
};
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
@ -183,20 +244,26 @@ const executeScript = (script, context) => {
|
|||||||
scriptFunction.call(null, context);
|
scriptFunction.call(null, context);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Script execution error:', error);
|
console.error('[FormScriptEngine] Script execution error:', error);
|
||||||
// Could emit an error event here for user feedback
|
// Show error to user if showError function is available
|
||||||
|
if (context.showError) {
|
||||||
|
context.showError(`Script Error: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize script engine
|
// Initialize script engine
|
||||||
const initializeScript = async () => {
|
const initializeScript = () => {
|
||||||
if (!props.customScript || isScriptInitialized.value) return;
|
fieldChangeHandlers.value.clear();
|
||||||
|
|
||||||
await nextTick(); // Ensure DOM is ready
|
|
||||||
|
|
||||||
|
// Create script context
|
||||||
scriptContext.value = createScriptContext();
|
scriptContext.value = createScriptContext();
|
||||||
|
|
||||||
if (props.formEvents.onLoad) {
|
// Store initial form data
|
||||||
|
previousFormData.value = { ...props.formData };
|
||||||
|
|
||||||
|
// Execute onLoad script if enabled
|
||||||
|
if (props.formEvents.onLoad && props.customScript) {
|
||||||
executeScript(props.customScript, scriptContext.value);
|
executeScript(props.customScript, scriptContext.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,44 +272,63 @@ const initializeScript = async () => {
|
|||||||
|
|
||||||
// Handle field changes
|
// Handle field changes
|
||||||
const handleFieldChange = (fieldName, newValue, oldValue) => {
|
const handleFieldChange = (fieldName, newValue, oldValue) => {
|
||||||
if (!fieldChangeHandlers.value.has(fieldName)) return;
|
if (!fieldChangeHandlers.value.has(fieldName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handlers = fieldChangeHandlers.value.get(fieldName);
|
const handlers = fieldChangeHandlers.value.get(fieldName);
|
||||||
handlers.forEach(handler => {
|
handlers.forEach((handler, index) => {
|
||||||
try {
|
try {
|
||||||
if (typeof handler === 'function') {
|
if (typeof handler === 'function') {
|
||||||
handler.call(scriptContext.value, newValue, oldValue);
|
handler.call(scriptContext.value, newValue, oldValue);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error in field change handler for '${fieldName}':`, error);
|
console.error(`[FormScriptEngine] Error in onFieldChange handler for "${fieldName}":`, error);
|
||||||
|
if (scriptContext.value?.showError) {
|
||||||
|
scriptContext.value.showError(`Field change handler error for ${fieldName}: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for form data changes
|
|
||||||
watch(() => props.formData, (newData, oldData) => {
|
|
||||||
if (!isScriptInitialized.value || !props.formEvents.onFieldChange) return;
|
|
||||||
|
|
||||||
// Compare and trigger handlers for changed fields
|
|
||||||
Object.keys(newData).forEach(fieldName => {
|
|
||||||
if (newData[fieldName] !== oldData?.[fieldName]) {
|
|
||||||
handleFieldChange(fieldName, newData[fieldName], oldData?.[fieldName]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
// Watch for script changes and reinitialize
|
// Watch for script changes and reinitialize
|
||||||
watch(() => props.customScript, () => {
|
watch(() => props.customScript, (newScript, oldScript) => {
|
||||||
if (isScriptInitialized.value) {
|
if (newScript !== oldScript) {
|
||||||
isScriptInitialized.value = false;
|
isScriptInitialized.value = false;
|
||||||
fieldChangeHandlers.value.clear();
|
|
||||||
initializeScript();
|
initializeScript();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for form data changes - the elegant, performant way
|
||||||
|
watch(() => props.formData, (newData, oldData) => {
|
||||||
|
if (!isScriptInitialized.value || !props.formEvents.onFieldChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare with our stored previousFormData to detect changes
|
||||||
|
Object.keys(newData).forEach(fieldName => {
|
||||||
|
const newValue = newData[fieldName];
|
||||||
|
const oldValue = previousFormData.value[fieldName];
|
||||||
|
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
handleFieldChange(fieldName, newValue, oldValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update previousFormData for next comparison
|
||||||
|
previousFormData.value = { ...newData };
|
||||||
|
}, { deep: true, immediate: false });
|
||||||
|
|
||||||
|
// Watch for CSS changes and update
|
||||||
|
watch(() => props.customCSS, () => {
|
||||||
|
// CSS will be automatically updated by the template reactivity
|
||||||
|
});
|
||||||
|
|
||||||
// Lifecycle hooks
|
// Lifecycle hooks
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initializeScript();
|
setTimeout(() => {
|
||||||
|
initializeScript();
|
||||||
|
}, 500); // Delay to ensure parent DOM and initial props are settled
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@ -259,13 +345,12 @@ defineExpose({
|
|||||||
},
|
},
|
||||||
getContext: () => scriptContext.value,
|
getContext: () => scriptContext.value,
|
||||||
reinitialize: () => {
|
reinitialize: () => {
|
||||||
isScriptInitialized.value = false;
|
isScriptInitialized.value = false;
|
||||||
fieldChangeHandlers.value.clear();
|
initializeScript();
|
||||||
initializeScript();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
/* Component doesn't render visible content */
|
/* Component doesn't render visible content */
|
||||||
</style>
|
</style>
|
@ -141,6 +141,210 @@ const activeCategory = ref('all');
|
|||||||
|
|
||||||
// Templates data
|
// Templates data
|
||||||
const templates = [
|
const templates = [
|
||||||
|
{
|
||||||
|
id: 'test-css-js',
|
||||||
|
name: 'CSS & JavaScript Test',
|
||||||
|
description: 'Simple test form to verify custom CSS and JavaScript functionality works correctly.',
|
||||||
|
category: 'advanced',
|
||||||
|
image: null,
|
||||||
|
hasScript: true,
|
||||||
|
hasCSS: true,
|
||||||
|
componentCount: 6,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
name: 'Heading',
|
||||||
|
category: 'Layout',
|
||||||
|
icon: 'material-symbols:title',
|
||||||
|
defaultProps: {
|
||||||
|
value: 'CSS & JavaScript Test Form',
|
||||||
|
level: 1,
|
||||||
|
width: '100%',
|
||||||
|
gridColumn: 'span 12'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
name: 'Paragraph',
|
||||||
|
category: 'Layout',
|
||||||
|
icon: 'material-symbols:text-snippet-outline',
|
||||||
|
defaultProps: {
|
||||||
|
value: 'This form tests custom CSS styling and JavaScript functionality. Try changing the values to see the scripts in action!',
|
||||||
|
width: '100%',
|
||||||
|
gridColumn: 'span 12'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'Text Field',
|
||||||
|
category: 'Basic Inputs',
|
||||||
|
icon: 'material-symbols:text-fields',
|
||||||
|
defaultProps: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Your Name',
|
||||||
|
name: 'user_name',
|
||||||
|
placeholder: 'Enter your name',
|
||||||
|
help: 'This field will trigger JavaScript when changed',
|
||||||
|
validation: 'required',
|
||||||
|
width: '50%',
|
||||||
|
gridColumn: 'span 6'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
name: 'Number',
|
||||||
|
category: 'Basic Inputs',
|
||||||
|
icon: 'material-symbols:counter-1-outline',
|
||||||
|
defaultProps: {
|
||||||
|
type: 'number',
|
||||||
|
label: 'Quantity',
|
||||||
|
name: 'quantity',
|
||||||
|
placeholder: '1',
|
||||||
|
help: 'Change this to see the total calculation',
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
validation: 'required',
|
||||||
|
width: '25%',
|
||||||
|
gridColumn: 'span 3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
name: 'Number',
|
||||||
|
category: 'Basic Inputs',
|
||||||
|
icon: 'material-symbols:counter-1-outline',
|
||||||
|
defaultProps: {
|
||||||
|
type: 'number',
|
||||||
|
label: 'Price ($)',
|
||||||
|
name: 'price',
|
||||||
|
placeholder: '10.00',
|
||||||
|
help: 'Price per item',
|
||||||
|
min: 0,
|
||||||
|
step: 0.01,
|
||||||
|
validation: 'required',
|
||||||
|
width: '25%',
|
||||||
|
gridColumn: 'span 3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'Text Field',
|
||||||
|
category: 'Basic Inputs',
|
||||||
|
icon: 'material-symbols:text-fields',
|
||||||
|
defaultProps: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Total Amount',
|
||||||
|
name: 'total',
|
||||||
|
placeholder: '$0.00',
|
||||||
|
help: 'This will be calculated automatically',
|
||||||
|
width: '100%',
|
||||||
|
gridColumn: 'span 12'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
script: `// Welcome message when form loads
|
||||||
|
this.showSuccess('CSS & JavaScript test form loaded successfully!');
|
||||||
|
|
||||||
|
// Greet user when name changes
|
||||||
|
this.onFieldChange('user_name', (value) => {
|
||||||
|
if (value && value.trim()) {
|
||||||
|
this.showInfo('Hello, ' + value.trim() + '! Welcome to the test form.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total when quantity or price changes
|
||||||
|
this.onFieldChange(['quantity', 'price'], () => {
|
||||||
|
const quantity = parseFloat(this.getField('quantity')) || 0;
|
||||||
|
const price = parseFloat(this.getField('price')) || 0;
|
||||||
|
const total = quantity * price;
|
||||||
|
|
||||||
|
this.setField('total', '$' + total.toFixed(2));
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
this.console.log('Total calculated:', total);
|
||||||
|
|
||||||
|
if (total > 50) {
|
||||||
|
this.showSuccess('Great! Total is over $50');
|
||||||
|
} else if (total > 100) {
|
||||||
|
this.showInfo('Wow! Total is over $100');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with default values when form loads
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setField('quantity', 2);
|
||||||
|
this.setField('price', 15.50);
|
||||||
|
}, 500);`,
|
||||||
|
css: `.form-container {
|
||||||
|
background: linear-gradient(135deg, #fef7ff 0%, #f3e8ff 100%);
|
||||||
|
border: 3px solid #a855f7;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 20px 40px rgba(168, 85, 247, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the name field with a special border */
|
||||||
|
.form-field[data-name="user_name"] {
|
||||||
|
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
|
||||||
|
border: 2px solid #10b981;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field[data-name="user_name"] input {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the quantity field */
|
||||||
|
.form-field[data-name="quantity"] {
|
||||||
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
|
border: 2px solid #f59e0b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the price field */
|
||||||
|
.form-field[data-name="price"] {
|
||||||
|
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the total field with special emphasis */
|
||||||
|
.form-field[data-name="total"] {
|
||||||
|
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||||
|
border: 3px solid #ef4444;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field[data-name="total"] input {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #dc2626;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add some hover effects */
|
||||||
|
.form-field:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the heading */
|
||||||
|
h1 {
|
||||||
|
color: #7c3aed;
|
||||||
|
text-shadow: 2px 2px 4px rgba(124, 58, 237, 0.3);
|
||||||
|
}`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'contact-form',
|
id: 'contact-form',
|
||||||
name: 'Contact Form',
|
name: 'Contact Form',
|
||||||
@ -2187,6 +2391,7 @@ this.onFieldChange('budget', (value) => {
|
|||||||
|
|
||||||
// Update display somewhere in the form
|
// Update display somewhere in the form
|
||||||
console.log('Budget formatted:', formatted);
|
console.log('Budget formatted:', formatted);
|
||||||
|
this.showInfo('Budget updated: ' + formatted);
|
||||||
});
|
});
|
||||||
|
|
||||||
// OTP validation
|
// OTP validation
|
||||||
@ -2201,18 +2406,32 @@ this.onFieldChange('otp_code', (value) => {
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
});`,
|
});
|
||||||
|
|
||||||
|
// Initialize form
|
||||||
|
console.log('Advanced form script loaded successfully!');`,
|
||||||
css: `.form-container {
|
css: `.form-container {
|
||||||
background: #f8fafc;
|
background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 100%);
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border: 1px solid #e2e8f0;
|
border: 2px solid #e2e8f0;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field[data-name="budget"] {
|
||||||
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
|
border: 2px solid #f59e0b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field[data-name="info_display"] {
|
.form-field[data-name="info_display"] {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field[data-name="dropzone"] {
|
.form-field[data-name="dropzone"] {
|
||||||
@ -2225,6 +2444,8 @@ this.onFieldChange('otp_code', (value) => {
|
|||||||
.form-field[data-name="dropzone"]:hover {
|
.form-field[data-name="dropzone"]:hover {
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field[data-name="otp_code"] input {
|
.form-field[data-name="otp_code"] input {
|
||||||
@ -2232,6 +2453,19 @@ this.onFieldChange('otp_code', (value) => {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
letter-spacing: 0.5rem;
|
letter-spacing: 0.5rem;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated submit button */
|
||||||
|
.form-field[data-name="submit_advanced"] button {
|
||||||
|
background: linear-gradient(45deg, #3b82f6, #1d4ed8);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field[data-name="submit_advanced"] button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -117,8 +117,8 @@ watch(
|
|||||||
></Icon>
|
></Icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<slot name="body"></slot>
|
<slot name="body"></slot>
|
||||||
<slot v-if="!$slots.body"></slot>
|
<slot v-if="!$slots.body"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hideFooter" class="modal-footer">
|
<div v-if="!hideFooter" class="modal-footer">
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
|
@ -19,6 +19,8 @@ The documentation is organized into the following sections:
|
|||||||
### Form Builder
|
### Form Builder
|
||||||
- [User Guide](./form-builder/USER_GUIDE.md) - How to use the Form Builder
|
- [User Guide](./form-builder/USER_GUIDE.md) - How to use the Form Builder
|
||||||
- [Technical Guide](./form-builder/TECHNICAL_GUIDE.md) - Technical details of the Form Builder implementation
|
- [Technical Guide](./form-builder/TECHNICAL_GUIDE.md) - Technical details of the Form Builder implementation
|
||||||
|
- [JavaScript API Reference](./form-builder/JAVASCRIPT_API.md) - Complete JavaScript API for dynamic forms and calculations
|
||||||
|
- [Grid System Guide](./form-builder/grid-system.md) - Visual grid system and layout documentation
|
||||||
|
|
||||||
### Process Execution
|
### Process Execution
|
||||||
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface
|
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface
|
||||||
@ -48,4 +50,15 @@ When contributing to this documentation:
|
|||||||
|
|
||||||
If you have questions about this documentation or need help with the system, please contact the development team.
|
If you have questions about this documentation or need help with the system, please contact the development team.
|
||||||
|
|
||||||
Last updated: July 10, 2024
|
Last updated: December 2024
|
||||||
|
|
||||||
|
## Recent Updates
|
||||||
|
|
||||||
|
### December 2024 - Major Form Builder Enhancements
|
||||||
|
- **JavaScript Execution Engine**: Added FormScriptEngine component for real-time calculations
|
||||||
|
- **Dynamic Field Updates**: Implemented onFieldChange and onLoad event handlers
|
||||||
|
- **Real-time Calculations**: Forms now support live mathematical calculations and field dependencies
|
||||||
|
- **Enhanced Debugging**: Comprehensive logging and error handling for JavaScript execution
|
||||||
|
- **API Documentation**: Complete JavaScript API reference with practical examples
|
||||||
|
- **Performance Optimizations**: Improved change detection and memory management
|
||||||
|
- **Security Enhancements**: Sandboxed JavaScript execution with input validation
|
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) => ({
|
formConfig: (state) => ({
|
||||||
id: state.formId,
|
id: state.formId,
|
||||||
uuid: state.formUUID,
|
uuid: state.formUUID,
|
||||||
name: state.formName,
|
name: state.formName,
|
||||||
description: state.formDescription,
|
description: state.formDescription,
|
||||||
components: state.formComponents
|
components: state.formComponents
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -912,8 +912,726 @@ const useVirtualScrolling = (components, containerHeight) => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JavaScript Execution Engine (FormScriptEngine)
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
|
||||||
|
The FormScriptEngine component provides real-time JavaScript execution for dynamic form behavior, calculations, and conditional logic. It monitors form data changes and executes user-defined event handlers to create interactive forms.
|
||||||
|
|
||||||
|
#### Core Components
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── FormScriptEngine.vue # Main execution engine
|
||||||
|
└── FormBuilderCanvas.vue # Integration point for script execution
|
||||||
|
pages/
|
||||||
|
└── form-builder/
|
||||||
|
└── index.vue # Form data handling and script triggering
|
||||||
|
```
|
||||||
|
|
||||||
|
### FormScriptEngine.vue Implementation
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- FormScriptEngine runs invisibly in the background -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<!-- Debug info in development -->
|
||||||
|
<div v-if="isDev">
|
||||||
|
Script Engine Status: {{ isInitialized ? 'Active' : 'Initializing' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// Component props
|
||||||
|
const props = defineProps({
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
script: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reactive state
|
||||||
|
const isInitialized = ref(false)
|
||||||
|
const previousFormData = ref({})
|
||||||
|
const scriptContext = ref(null)
|
||||||
|
const handlers = ref({
|
||||||
|
onLoad: null,
|
||||||
|
onFieldChange: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Development mode detection
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
// Initialize script execution context
|
||||||
|
const initializeScript = () => {
|
||||||
|
try {
|
||||||
|
if (!props.script.trim()) {
|
||||||
|
console.log('FormScriptEngine: No script provided')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create execution context with form functions
|
||||||
|
const context = {
|
||||||
|
setField: (fieldName, value) => {
|
||||||
|
console.log('setField called:', fieldName, '=', value)
|
||||||
|
|
||||||
|
if (props.formData[fieldName] !== value) {
|
||||||
|
// Update form data reactively
|
||||||
|
props.formData[fieldName] = value
|
||||||
|
|
||||||
|
// Emit update event for parent component
|
||||||
|
emit('fieldUpdate', { fieldName, value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getField: (fieldName) => {
|
||||||
|
const value = props.formData[fieldName]
|
||||||
|
console.log('getField called:', fieldName, '=', value)
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
|
||||||
|
console: console, // Allow console access
|
||||||
|
|
||||||
|
// Add other safe global functions as needed
|
||||||
|
parseFloat: parseFloat,
|
||||||
|
parseInt: parseInt,
|
||||||
|
Date: Date,
|
||||||
|
Math: Math
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute script in isolated context
|
||||||
|
const scriptFunction = new Function(
|
||||||
|
'setField', 'getField', 'console', 'parseFloat', 'parseInt', 'Date', 'Math',
|
||||||
|
`
|
||||||
|
${props.script}
|
||||||
|
|
||||||
|
// Return the event handlers
|
||||||
|
return {
|
||||||
|
onLoad: typeof onLoad !== 'undefined' ? onLoad : null,
|
||||||
|
onFieldChange: typeof onFieldChange !== 'undefined' ? onFieldChange : null
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute script and extract handlers
|
||||||
|
const result = scriptFunction(
|
||||||
|
context.setField,
|
||||||
|
context.getField,
|
||||||
|
context.console,
|
||||||
|
context.parseFloat,
|
||||||
|
context.parseInt,
|
||||||
|
context.Date,
|
||||||
|
context.Math
|
||||||
|
)
|
||||||
|
|
||||||
|
handlers.value = result
|
||||||
|
scriptContext.value = context
|
||||||
|
|
||||||
|
console.log('FormScriptEngine: Script initialized with handlers:', Object.keys(result))
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FormScriptEngine: Script initialization error:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute onLoad handler
|
||||||
|
const executeOnLoad = async () => {
|
||||||
|
if (handlers.value.onLoad && typeof handlers.value.onLoad === 'function') {
|
||||||
|
try {
|
||||||
|
console.log('FormScriptEngine: Executing onLoad handler')
|
||||||
|
await handlers.value.onLoad()
|
||||||
|
console.log('FormScriptEngine: onLoad completed')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FormScriptEngine: onLoad execution error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute onFieldChange handler
|
||||||
|
const executeOnFieldChange = async (fieldName, value) => {
|
||||||
|
if (handlers.value.onFieldChange && typeof handlers.value.onFieldChange === 'function') {
|
||||||
|
try {
|
||||||
|
console.log('FormScriptEngine: Executing onFieldChange for:', fieldName)
|
||||||
|
await handlers.value.onFieldChange(fieldName, value)
|
||||||
|
console.log('FormScriptEngine: onFieldChange completed for:', fieldName)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FormScriptEngine: onFieldChange execution error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect and trigger field changes
|
||||||
|
const triggerFieldChangeDetection = () => {
|
||||||
|
const currentData = { ...props.formData }
|
||||||
|
const previousData = previousFormData.value
|
||||||
|
|
||||||
|
// Find changed fields
|
||||||
|
const changedFields = []
|
||||||
|
|
||||||
|
for (const fieldName in currentData) {
|
||||||
|
if (currentData[fieldName] !== previousData[fieldName]) {
|
||||||
|
changedFields.push({
|
||||||
|
name: fieldName,
|
||||||
|
value: currentData[fieldName],
|
||||||
|
oldValue: previousData[fieldName]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedFields.length > 0) {
|
||||||
|
console.log('FormScriptEngine: Detected field changes:', changedFields)
|
||||||
|
|
||||||
|
// Execute handler for each changed field
|
||||||
|
changedFields.forEach(field => {
|
||||||
|
executeOnFieldChange(field.name, field.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update previous data after processing
|
||||||
|
previousFormData.value = { ...currentData }
|
||||||
|
} else {
|
||||||
|
console.log('FormScriptEngine: No field changes detected')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for form data changes with enhanced detection
|
||||||
|
watch(
|
||||||
|
() => props.formData,
|
||||||
|
(newData, oldData) => {
|
||||||
|
if (!isInitialized.value) return
|
||||||
|
|
||||||
|
console.log('FormScriptEngine: Form data changed, checking for field changes...')
|
||||||
|
console.log('New data:', JSON.stringify(newData))
|
||||||
|
console.log('Previous data:', JSON.stringify(previousFormData.value))
|
||||||
|
|
||||||
|
// Use nextTick to ensure DOM updates are complete
|
||||||
|
nextTick(() => {
|
||||||
|
triggerFieldChangeDetection()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
flush: 'post' // Execute after DOM updates
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize when component mounts
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log('FormScriptEngine: Component mounted, initializing...')
|
||||||
|
|
||||||
|
// Initialize script context
|
||||||
|
const success = initializeScript()
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Execute onLoad handler first
|
||||||
|
await executeOnLoad()
|
||||||
|
|
||||||
|
// Set initial previousFormData AFTER onLoad execution
|
||||||
|
// This prevents initial setField calls from being detected as changes
|
||||||
|
await nextTick()
|
||||||
|
previousFormData.value = { ...props.formData }
|
||||||
|
|
||||||
|
isInitialized.value = true
|
||||||
|
console.log('FormScriptEngine: Initialization complete')
|
||||||
|
} else {
|
||||||
|
console.error('FormScriptEngine: Initialization failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Public method to manually trigger change detection
|
||||||
|
const manualTrigger = () => {
|
||||||
|
console.log('FormScriptEngine: Manual trigger requested')
|
||||||
|
triggerFieldChangeDetection()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose public methods
|
||||||
|
defineExpose({
|
||||||
|
manualTrigger,
|
||||||
|
isInitialized
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emit events
|
||||||
|
const emit = defineEmits(['fieldUpdate'])
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with Form Builder
|
||||||
|
|
||||||
|
#### Form Builder Canvas Integration
|
||||||
|
```vue
|
||||||
|
<!-- In FormBuilderCanvas.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="form-canvas">
|
||||||
|
<!-- Form components render here -->
|
||||||
|
<ComponentPreview
|
||||||
|
v-for="component in formComponents"
|
||||||
|
:key="component.id"
|
||||||
|
:component="component"
|
||||||
|
:is-preview="isPreview"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- JavaScript execution engine -->
|
||||||
|
<FormScriptEngine
|
||||||
|
v-if="isPreview && formScript"
|
||||||
|
:form-data="previewFormData"
|
||||||
|
:script="formScript"
|
||||||
|
@field-update="handleScriptFieldUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FormScriptEngine from '~/components/FormScriptEngine.vue'
|
||||||
|
|
||||||
|
// Handle script-triggered field updates
|
||||||
|
const handleScriptFieldUpdate = ({ fieldName, value }) => {
|
||||||
|
console.log('Canvas: Script updated field:', fieldName, '=', value)
|
||||||
|
|
||||||
|
// Update preview form data
|
||||||
|
previewFormData.value[fieldName] = value
|
||||||
|
|
||||||
|
// Trigger UI refresh
|
||||||
|
nextTick(() => {
|
||||||
|
refreshFormComponents()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Main Form Builder Integration
|
||||||
|
```vue
|
||||||
|
<!-- In pages/form-builder/index.vue -->
|
||||||
|
<script setup>
|
||||||
|
// Enhanced FormKit input handler with script engine integration
|
||||||
|
const handleFormKitInput = (event) => {
|
||||||
|
console.log('FormKit input event:', event)
|
||||||
|
|
||||||
|
// Update preview form data
|
||||||
|
previewFormData.value = { ...event }
|
||||||
|
|
||||||
|
// Trigger script engine change detection
|
||||||
|
if (formScriptEngineRef.value) {
|
||||||
|
nextTick(() => {
|
||||||
|
formScriptEngineRef.value.manualTrigger()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template ref for script engine
|
||||||
|
const formScriptEngineRef = ref(null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="form-builder">
|
||||||
|
<!-- Canvas with integrated script engine -->
|
||||||
|
<FormBuilderCanvas
|
||||||
|
ref="formScriptEngineRef"
|
||||||
|
:form-script="formStore.currentForm?.script"
|
||||||
|
@form-kit-input="handleFormKitInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Handler API Reference
|
||||||
|
|
||||||
|
#### onLoad Handler
|
||||||
|
Executes once when the form loads. Use for initialization.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
onLoad: function() {
|
||||||
|
// Initialize default values
|
||||||
|
setField('status', 'active');
|
||||||
|
setField('created_date', new Date().toISOString().split('T')[0]);
|
||||||
|
|
||||||
|
// Perform initial calculations
|
||||||
|
calculateInitialValues();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### onFieldChange Handler
|
||||||
|
Executes when any field value changes.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
console.log('Field changed:', fieldName, 'New value:', value);
|
||||||
|
|
||||||
|
// Handle specific field changes
|
||||||
|
switch(fieldName) {
|
||||||
|
case 'quantity':
|
||||||
|
case 'price':
|
||||||
|
updateTotal();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'country':
|
||||||
|
updateTaxRate(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling and Debugging
|
||||||
|
|
||||||
|
#### Script Execution Errors
|
||||||
|
```javascript
|
||||||
|
// Comprehensive error handling in script execution
|
||||||
|
const executeHandler = async (handler, ...args) => {
|
||||||
|
try {
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
const result = await handler(...args)
|
||||||
|
return { success: true, result }
|
||||||
|
}
|
||||||
|
return { success: false, error: 'Handler is not a function' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Script execution error:', error)
|
||||||
|
|
||||||
|
// Emit error event for debugging
|
||||||
|
emit('scriptError', {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
handler: handler.name || 'anonymous',
|
||||||
|
args
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debug Mode Features
|
||||||
|
```javascript
|
||||||
|
// Enhanced logging in development mode
|
||||||
|
const debugLog = (message, data) => {
|
||||||
|
if (isDev) {
|
||||||
|
console.log(`[FormScriptEngine Debug] ${message}`, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field change tracing
|
||||||
|
const traceFieldChange = (fieldName, oldValue, newValue) => {
|
||||||
|
if (isDev) {
|
||||||
|
console.group(`Field Change: ${fieldName}`)
|
||||||
|
console.log('Old Value:', oldValue)
|
||||||
|
console.log('New Value:', newValue)
|
||||||
|
console.log('Timestamp:', new Date().toISOString())
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
|
||||||
|
#### Change Detection Optimization
|
||||||
|
```javascript
|
||||||
|
// Efficient change detection using shallow comparison
|
||||||
|
const hasDataChanged = (newData, oldData) => {
|
||||||
|
const newKeys = Object.keys(newData)
|
||||||
|
const oldKeys = Object.keys(oldData)
|
||||||
|
|
||||||
|
// Quick length check
|
||||||
|
if (newKeys.length !== oldKeys.length) return true
|
||||||
|
|
||||||
|
// Value comparison
|
||||||
|
return newKeys.some(key => newData[key] !== oldData[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced change detection for rapid updates
|
||||||
|
const debouncedChangeDetection = useDebounceFn(
|
||||||
|
triggerFieldChangeDetection,
|
||||||
|
100 // 100ms debounce
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Memory Management
|
||||||
|
```javascript
|
||||||
|
// Cleanup when component unmounts
|
||||||
|
onUnmounted(() => {
|
||||||
|
console.log('FormScriptEngine: Cleaning up...')
|
||||||
|
|
||||||
|
// Clear references
|
||||||
|
handlers.value = null
|
||||||
|
scriptContext.value = null
|
||||||
|
previousFormData.value = null
|
||||||
|
|
||||||
|
// Remove event listeners if any
|
||||||
|
cleanupEventListeners()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
#### Script Sandboxing
|
||||||
|
```javascript
|
||||||
|
// Restricted execution context - only safe functions exposed
|
||||||
|
const createSecureContext = () => {
|
||||||
|
return {
|
||||||
|
// Form manipulation functions
|
||||||
|
setField,
|
||||||
|
getField,
|
||||||
|
|
||||||
|
// Safe global functions
|
||||||
|
console: {
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error
|
||||||
|
},
|
||||||
|
|
||||||
|
// Math and date functions
|
||||||
|
Math,
|
||||||
|
Date,
|
||||||
|
parseFloat,
|
||||||
|
parseInt,
|
||||||
|
isNaN,
|
||||||
|
|
||||||
|
// String functions
|
||||||
|
String,
|
||||||
|
|
||||||
|
// No access to:
|
||||||
|
// - window object
|
||||||
|
// - document object
|
||||||
|
// - eval function
|
||||||
|
// - Function constructor
|
||||||
|
// - import/require
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Input Validation
|
||||||
|
```javascript
|
||||||
|
// Validate script before execution
|
||||||
|
const validateScript = (script) => {
|
||||||
|
const dangerousPatterns = [
|
||||||
|
/eval\s*\(/,
|
||||||
|
/Function\s*\(/,
|
||||||
|
/window\./,
|
||||||
|
/document\./,
|
||||||
|
/import\s+/,
|
||||||
|
/require\s*\(/,
|
||||||
|
/fetch\s*\(/,
|
||||||
|
/XMLHttpRequest/
|
||||||
|
]
|
||||||
|
|
||||||
|
return !dangerousPatterns.some(pattern => pattern.test(script))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
|
### JavaScript Engine Tests
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// FormScriptEngine unit tests
|
||||||
|
describe('FormScriptEngine', () => {
|
||||||
|
let wrapper
|
||||||
|
let mockFormData
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFormData = ref({
|
||||||
|
quantity: 0,
|
||||||
|
price: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper = mount(FormScriptEngine, {
|
||||||
|
props: {
|
||||||
|
formData: mockFormData.value,
|
||||||
|
script: `
|
||||||
|
onLoad: function() {
|
||||||
|
setField('quantity', 1);
|
||||||
|
setField('price', 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
if (fieldName === 'quantity' || fieldName === 'price') {
|
||||||
|
const total = getField('quantity') * getField('price');
|
||||||
|
setField('total', total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should initialize script and execute onLoad', async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(mockFormData.value.quantity).toBe(1)
|
||||||
|
expect(mockFormData.value.price).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should execute onFieldChange when data changes', async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Simulate field change
|
||||||
|
mockFormData.value.quantity = 5
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(mockFormData.value.total).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle script errors gracefully', async () => {
|
||||||
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||||
|
|
||||||
|
const wrapper = mount(FormScriptEngine, {
|
||||||
|
props: {
|
||||||
|
formData: mockFormData.value,
|
||||||
|
script: `
|
||||||
|
onLoad: function() {
|
||||||
|
// Intentional error
|
||||||
|
undefinedFunction();
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||||
|
consoleErrorSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should validate and sanitize script input', () => {
|
||||||
|
const dangerousScript = `
|
||||||
|
onLoad: function() {
|
||||||
|
eval('alert("XSS")');
|
||||||
|
window.location = 'http://malicious.com';
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const isValid = wrapper.vm.validateScript(dangerousScript)
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Real-time calculation tests
|
||||||
|
describe('Real-time Calculations', () => {
|
||||||
|
test('should update total when quantity changes', async () => {
|
||||||
|
const formData = ref({ quantity: 2, price: 15.99, total: 0 })
|
||||||
|
|
||||||
|
const wrapper = mount(TestFormWithScript, {
|
||||||
|
props: { initialData: formData.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Change quantity
|
||||||
|
await wrapper.find('input[name="quantity"]').setValue('3')
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(formData.value.total).toBe(47.97)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle edge cases in calculations', async () => {
|
||||||
|
const formData = ref({ quantity: '', price: 'invalid', total: 0 })
|
||||||
|
|
||||||
|
const wrapper = mount(TestFormWithScript, {
|
||||||
|
props: { initialData: formData.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Should handle invalid inputs gracefully
|
||||||
|
await nextTick()
|
||||||
|
expect(formData.value.total).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Performance tests
|
||||||
|
describe('FormScriptEngine Performance', () => {
|
||||||
|
test('should not trigger excessive re-renders', async () => {
|
||||||
|
const executeHandlerSpy = jest.spyOn(FormScriptEngine.methods, 'executeOnFieldChange')
|
||||||
|
|
||||||
|
const wrapper = mount(FormScriptEngine, {
|
||||||
|
props: {
|
||||||
|
formData: { field1: '', field2: '', field3: '' },
|
||||||
|
script: `
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
console.log('Changed:', fieldName);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make multiple rapid changes
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
wrapper.setProps({ formData: { field1: `value${i}`, field2: '', field3: '' } })
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be debounced/optimized
|
||||||
|
expect(executeHandlerSpy.mock.calls.length).toBeLessThan(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Full form workflow tests with JavaScript
|
||||||
|
describe('Form Builder with JavaScript Integration', () => {
|
||||||
|
test('should execute calculations in preview mode', async () => {
|
||||||
|
const wrapper = mount(FormBuilder)
|
||||||
|
|
||||||
|
// Add form components
|
||||||
|
await wrapper.find('[data-component="number"]').trigger('click')
|
||||||
|
await wrapper.find('[data-component="number"]').trigger('click')
|
||||||
|
await wrapper.find('[data-component="number"]').trigger('click')
|
||||||
|
|
||||||
|
// Configure fields
|
||||||
|
const components = wrapper.vm.formStore.formComponents
|
||||||
|
components[0].props.name = 'quantity'
|
||||||
|
components[1].props.name = 'price'
|
||||||
|
components[2].props.name = 'total'
|
||||||
|
|
||||||
|
// Add JavaScript
|
||||||
|
wrapper.vm.formStore.currentForm.script = `
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
if (fieldName === 'quantity' || fieldName === 'price') {
|
||||||
|
const quantity = parseFloat(getField('quantity')) || 0;
|
||||||
|
const price = parseFloat(getField('price')) || 0;
|
||||||
|
setField('total', quantity * price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Switch to preview mode
|
||||||
|
await wrapper.find('[data-testid="preview-toggle"]').trigger('click')
|
||||||
|
|
||||||
|
// Input values
|
||||||
|
await wrapper.find('input[name="quantity"]').setValue('5')
|
||||||
|
await wrapper.find('input[name="price"]').setValue('19.99')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Check calculated total
|
||||||
|
const totalField = wrapper.find('input[name="total"]')
|
||||||
|
expect(totalField.element.value).toBe('99.95')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle form builder to preview data flow', async () => {
|
||||||
|
const wrapper = mount(FormBuilder)
|
||||||
|
|
||||||
|
// Build form with JavaScript
|
||||||
|
await addFormComponents(wrapper)
|
||||||
|
await addJavaScriptLogic(wrapper)
|
||||||
|
|
||||||
|
// Test preview functionality
|
||||||
|
await wrapper.find('[data-testid="preview-button"]').trigger('click')
|
||||||
|
|
||||||
|
// Verify script execution
|
||||||
|
expect(wrapper.find('.script-engine').exists()).toBe(true)
|
||||||
|
expect(wrapper.vm.isPreviewMode).toBe(true)
|
||||||
|
|
||||||
|
// Test field interactions
|
||||||
|
await simulateUserInput(wrapper)
|
||||||
|
await verifyCalculations(wrapper)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### Unit Tests
|
### Unit Tests
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
@ -106,7 +106,7 @@ Perfect for collecting simple data:
|
|||||||
**Text Field**
|
**Text Field**
|
||||||
- Single-line text input
|
- Single-line text input
|
||||||
- Smart width: Small (33%)
|
- Smart width: Small (33%)
|
||||||
- Use for: Names, titles, short answers
|
- Use for: Names, titles, short answers
|
||||||
- Quick settings: Label, name, placeholder, required
|
- Quick settings: Label, name, placeholder, required
|
||||||
|
|
||||||
**Text Area**
|
**Text Area**
|
||||||
@ -119,7 +119,7 @@ Perfect for collecting simple data:
|
|||||||
- Numeric input with validation
|
- Numeric input with validation
|
||||||
- Smart width: Narrow (25%)
|
- Smart width: Narrow (25%)
|
||||||
- Use for: Age, quantity, prices
|
- Use for: Age, quantity, prices
|
||||||
- Features: Min/max limits, step values
|
- Features: Min/max limits, step values
|
||||||
|
|
||||||
**Email Field**
|
**Email Field**
|
||||||
- Email input with validation
|
- Email input with validation
|
||||||
@ -131,7 +131,7 @@ Perfect for collecting simple data:
|
|||||||
- Secure password input
|
- Secure password input
|
||||||
- Smart width: Medium (50%)
|
- Smart width: Medium (50%)
|
||||||
- Use for: Authentication forms
|
- Use for: Authentication forms
|
||||||
- Features: Password masking, strength indicators
|
- Features: Password masking, strength indicators
|
||||||
|
|
||||||
### Selection Components
|
### Selection Components
|
||||||
For choosing from options:
|
For choosing from options:
|
||||||
@ -151,7 +151,7 @@ For choosing from options:
|
|||||||
**Radio Group**
|
**Radio Group**
|
||||||
- Single choice from group
|
- Single choice from group
|
||||||
- Smart width: Full (100%)
|
- Smart width: Full (100%)
|
||||||
- Use for: Exclusive choices, yes/no questions
|
- Use for: Exclusive choices, yes/no questions
|
||||||
- Features: Button or traditional styles
|
- Features: Button or traditional styles
|
||||||
|
|
||||||
**Switch Toggle**
|
**Switch Toggle**
|
||||||
@ -188,7 +188,7 @@ Specialized functionality:
|
|||||||
- Standard file input
|
- Standard file input
|
||||||
- Smart width: Wide (75%)
|
- Smart width: Wide (75%)
|
||||||
- Use for: Document uploads, attachments
|
- Use for: Document uploads, attachments
|
||||||
- Features: File type restrictions, size limits
|
- Features: File type restrictions, size limits
|
||||||
|
|
||||||
**File Drop Zone**
|
**File Drop Zone**
|
||||||
- Drag & drop upload area
|
- Drag & drop upload area
|
||||||
@ -214,7 +214,7 @@ Form structure and organization:
|
|||||||
**Heading**
|
**Heading**
|
||||||
- Section titles and organization
|
- Section titles and organization
|
||||||
- Smart width: Full (100%)
|
- Smart width: Full (100%)
|
||||||
- Use for: Form sections, categories
|
- Use for: Form sections, categories
|
||||||
- Features: Multiple heading levels (H1-H6)
|
- Features: Multiple heading levels (H1-H6)
|
||||||
|
|
||||||
**Paragraph**
|
**Paragraph**
|
||||||
@ -311,7 +311,7 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
|
|||||||
3. **Logical Grouping**: Use similar widths for related fields
|
3. **Logical Grouping**: Use similar widths for related fields
|
||||||
4. **Space Efficiency**: Let the system auto-optimize grid placement
|
4. **Space Efficiency**: Let the system auto-optimize grid placement
|
||||||
|
|
||||||
### User Experience
|
### User Experience
|
||||||
1. **Clear Labels**: Use descriptive, action-oriented labels
|
1. **Clear Labels**: Use descriptive, action-oriented labels
|
||||||
2. **Helpful Placeholders**: Provide examples in placeholder text
|
2. **Helpful Placeholders**: Provide examples in placeholder text
|
||||||
3. **Progressive Disclosure**: Use sections for complex forms
|
3. **Progressive Disclosure**: Use sections for complex forms
|
||||||
@ -362,7 +362,7 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
|
|||||||
**Performance Issues**
|
**Performance Issues**
|
||||||
- Reduce number of components
|
- Reduce number of components
|
||||||
- Simplify conditional logic
|
- Simplify conditional logic
|
||||||
- Clear browser cache
|
- Clear browser cache
|
||||||
|
|
||||||
### Getting Help
|
### Getting Help
|
||||||
- **Documentation**: Complete technical guides available
|
- **Documentation**: Complete technical guides available
|
||||||
@ -370,6 +370,281 @@ Forms integrate seamlessly with the Process Builder for automated workflows:
|
|||||||
- **Support**: Contact support team for assistance
|
- **Support**: Contact support team for assistance
|
||||||
- **Community**: Join user community forums
|
- **Community**: Join user community forums
|
||||||
|
|
||||||
|
## JavaScript & Dynamic Calculations
|
||||||
|
|
||||||
|
### Real-time Calculations
|
||||||
|
The Form Builder supports powerful JavaScript-based calculations that update in real-time as users interact with your forms. This enables dynamic forms with automatic calculations, conditional logic, and interactive behavior.
|
||||||
|
|
||||||
|
#### Key Features
|
||||||
|
- **Real-time Updates**: Calculations execute instantly when users change field values
|
||||||
|
- **onFieldChange Handlers**: Trigger custom logic when specific fields change
|
||||||
|
- **setField Function**: Programmatically update field values from JavaScript
|
||||||
|
- **Cross-field Dependencies**: Create complex relationships between form fields
|
||||||
|
- **Calculation Templates**: Pre-built templates for common calculation patterns
|
||||||
|
|
||||||
|
#### Basic Example
|
||||||
|
```javascript
|
||||||
|
// Form with real-time total calculation
|
||||||
|
onLoad: function() {
|
||||||
|
console.log('Form loaded, setting up calculations...');
|
||||||
|
|
||||||
|
// Initialize fields
|
||||||
|
setField('quantity', 1);
|
||||||
|
setField('price', 0);
|
||||||
|
setField('total', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
console.log('Field changed:', fieldName, '=', value);
|
||||||
|
|
||||||
|
if (fieldName === 'quantity' || fieldName === 'price') {
|
||||||
|
// Get current values
|
||||||
|
const quantity = getField('quantity') || 0;
|
||||||
|
const price = getField('price') || 0;
|
||||||
|
|
||||||
|
// Calculate and update total
|
||||||
|
const total = quantity * price;
|
||||||
|
setField('total', total.toFixed(2));
|
||||||
|
|
||||||
|
console.log('Updated total:', total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available JavaScript Functions
|
||||||
|
|
||||||
|
#### setField(fieldName, value)
|
||||||
|
Updates a form field value and triggers the UI to refresh.
|
||||||
|
```javascript
|
||||||
|
// Set text field
|
||||||
|
setField('user_name', 'John Doe');
|
||||||
|
|
||||||
|
// Set number field
|
||||||
|
setField('quantity', 5);
|
||||||
|
|
||||||
|
// Set calculated field
|
||||||
|
setField('total', 99.95);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### getField(fieldName)
|
||||||
|
Retrieves the current value of a form field.
|
||||||
|
```javascript
|
||||||
|
const userName = getField('user_name');
|
||||||
|
const quantity = getField('quantity') || 0; // Default to 0 if empty
|
||||||
|
```
|
||||||
|
|
||||||
|
#### onLoad Event Handler
|
||||||
|
Executes when the form first loads. Use for initialization and setting default values.
|
||||||
|
```javascript
|
||||||
|
onLoad: function() {
|
||||||
|
// Set default values
|
||||||
|
setField('country', 'USA');
|
||||||
|
setField('currency', 'USD');
|
||||||
|
|
||||||
|
// Perform initial calculations
|
||||||
|
calculateTotals();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### onFieldChange Event Handler
|
||||||
|
Executes whenever a user changes a field value. Receives fieldName and new value as parameters.
|
||||||
|
```javascript
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
switch(fieldName) {
|
||||||
|
case 'quantity':
|
||||||
|
case 'price':
|
||||||
|
updateTotal();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'country':
|
||||||
|
updateShippingOptions(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-world Examples
|
||||||
|
|
||||||
|
#### Invoice Calculator
|
||||||
|
```javascript
|
||||||
|
onLoad: function() {
|
||||||
|
// Initialize invoice fields
|
||||||
|
setField('quantity', 1);
|
||||||
|
setField('unit_price', 0);
|
||||||
|
setField('tax_rate', 8.5); // 8.5% tax
|
||||||
|
setField('subtotal', 0);
|
||||||
|
setField('tax_amount', 0);
|
||||||
|
setField('total', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
if (['quantity', 'unit_price', 'tax_rate'].includes(fieldName)) {
|
||||||
|
const quantity = parseFloat(getField('quantity')) || 0;
|
||||||
|
const unitPrice = parseFloat(getField('unit_price')) || 0;
|
||||||
|
const taxRate = parseFloat(getField('tax_rate')) || 0;
|
||||||
|
|
||||||
|
const subtotal = quantity * unitPrice;
|
||||||
|
const taxAmount = (subtotal * taxRate) / 100;
|
||||||
|
const total = subtotal + taxAmount;
|
||||||
|
|
||||||
|
setField('subtotal', subtotal.toFixed(2));
|
||||||
|
setField('tax_amount', taxAmount.toFixed(2));
|
||||||
|
setField('total', total.toFixed(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Conditional Field Logic
|
||||||
|
```javascript
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
if (fieldName === 'subscription_type') {
|
||||||
|
if (value === 'premium') {
|
||||||
|
setField('premium_features', 'Available');
|
||||||
|
setField('support_level', '24/7 Priority');
|
||||||
|
} else {
|
||||||
|
setField('premium_features', 'Not Available');
|
||||||
|
setField('support_level', 'Standard Business Hours');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Age Calculator
|
||||||
|
```javascript
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
if (fieldName === 'birth_date') {
|
||||||
|
const birthDate = new Date(value);
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
let age = today.getFullYear() - birthDate.getFullYear();
|
||||||
|
const monthDiff = today.getMonth() - birthDate.getMonth();
|
||||||
|
|
||||||
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
setField('age', age);
|
||||||
|
|
||||||
|
// Set eligibility based on age
|
||||||
|
if (age >= 18) {
|
||||||
|
setField('eligible', 'Yes');
|
||||||
|
} else {
|
||||||
|
setField('eligible', 'No');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices for JavaScript Forms
|
||||||
|
|
||||||
|
#### Performance Optimization
|
||||||
|
1. **Minimize Calculations**: Only calculate when necessary fields change
|
||||||
|
2. **Use Default Values**: Provide fallbacks for empty fields
|
||||||
|
3. **Debounce Heavy Operations**: For complex calculations, consider debouncing
|
||||||
|
4. **Cache Results**: Store calculation results when appropriate
|
||||||
|
|
||||||
|
#### Error Handling
|
||||||
|
```javascript
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
try {
|
||||||
|
if (fieldName === 'price') {
|
||||||
|
const price = parseFloat(value);
|
||||||
|
if (isNaN(price)) {
|
||||||
|
console.warn('Invalid price value:', value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform calculation
|
||||||
|
calculateTotal(price);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in field change handler:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debugging Tips
|
||||||
|
1. **Use Console Logging**: Add console.log statements to track execution
|
||||||
|
2. **Check Field Names**: Ensure field names in code match form field names exactly
|
||||||
|
3. **Validate Data Types**: Always parse and validate input values
|
||||||
|
4. **Test Edge Cases**: Test with empty values, zero, and negative numbers
|
||||||
|
|
||||||
|
### Form Templates with JavaScript
|
||||||
|
|
||||||
|
#### CSS & JavaScript Test Form
|
||||||
|
A comprehensive template demonstrating real-time calculations:
|
||||||
|
|
||||||
|
**Field Structure:**
|
||||||
|
- `user_name` (Text): User's name
|
||||||
|
- `quantity` (Number): Quantity of items
|
||||||
|
- `price` (Number): Price per item
|
||||||
|
- `total` (Number): Calculated total (read-only)
|
||||||
|
|
||||||
|
**JavaScript Logic:**
|
||||||
|
```javascript
|
||||||
|
onLoad: function() {
|
||||||
|
console.log('Form loaded, initializing...');
|
||||||
|
setField('user_name', '');
|
||||||
|
setField('quantity', 1);
|
||||||
|
setField('price', 0);
|
||||||
|
setField('total', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
console.log('Field changed:', fieldName, '=', value);
|
||||||
|
|
||||||
|
if (fieldName === 'user_name') {
|
||||||
|
console.log('User name updated to:', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName === 'quantity' || fieldName === 'price') {
|
||||||
|
const quantity = parseFloat(getField('quantity')) || 0;
|
||||||
|
const price = parseFloat(getField('price')) || 0;
|
||||||
|
const total = quantity * price;
|
||||||
|
|
||||||
|
setField('total', total.toFixed(2));
|
||||||
|
console.log('Calculated total:', total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting JavaScript Forms
|
||||||
|
|
||||||
|
#### Common Issues
|
||||||
|
|
||||||
|
**Calculations Not Updating**
|
||||||
|
- Verify field names match exactly (case-sensitive)
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
- Ensure `onFieldChange` handler is properly defined
|
||||||
|
- Confirm form is in preview mode for testing
|
||||||
|
|
||||||
|
**Initial Values Not Setting**
|
||||||
|
- Make sure `onLoad` handler is defined
|
||||||
|
- Check that `setField` calls use correct field names
|
||||||
|
- Verify form data structure in browser developer tools
|
||||||
|
|
||||||
|
**Console Errors**
|
||||||
|
- **"getField/setField is not defined"**: JavaScript engine not properly initialized
|
||||||
|
- **"Cannot read property"**: Field name doesn't exist or typo in field name
|
||||||
|
- **"Unexpected token"**: Syntax error in JavaScript code
|
||||||
|
|
||||||
|
#### Debugging Workflow
|
||||||
|
1. **Open Browser Developer Tools** (F12)
|
||||||
|
2. **Check Console Tab** for error messages
|
||||||
|
3. **Add Debug Logging**:
|
||||||
|
```javascript
|
||||||
|
onFieldChange: function(fieldName, value) {
|
||||||
|
console.log('=== Field Change Debug ===');
|
||||||
|
console.log('Field:', fieldName);
|
||||||
|
console.log('New Value:', value);
|
||||||
|
console.log('Current Form Data:', Object.keys(formData));
|
||||||
|
|
||||||
|
// Your calculation logic here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Test in Preview Mode** - JavaScript only executes in preview mode
|
||||||
|
5. **Verify Field Names** - Use browser inspect element to confirm field name attributes
|
||||||
|
|
||||||
## Advanced Features
|
## Advanced Features
|
||||||
|
|
||||||
### Custom Components
|
### Custom Components
|
||||||
|
@ -196,6 +196,7 @@
|
|||||||
<!-- Preview Mode -->
|
<!-- Preview Mode -->
|
||||||
<div v-if="isPreview" class="form-container">
|
<div v-if="isPreview" class="form-container">
|
||||||
<FormScriptEngine
|
<FormScriptEngine
|
||||||
|
ref="formScriptEngine"
|
||||||
:form-data="previewFormData"
|
:form-data="previewFormData"
|
||||||
:custom-script="formStore.formCustomScript"
|
:custom-script="formStore.formCustomScript"
|
||||||
:custom-css="formStore.formCustomCSS"
|
:custom-css="formStore.formCustomCSS"
|
||||||
@ -205,7 +206,13 @@
|
|||||||
@field-validate="handleScriptFieldValidate"
|
@field-validate="handleScriptFieldValidate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit type="form" @submit="handlePreviewSubmit" :actions="false" v-model="previewFormData">
|
<FormKit
|
||||||
|
ref="previewForm"
|
||||||
|
type="form"
|
||||||
|
@submit="handlePreviewSubmit"
|
||||||
|
:actions="false"
|
||||||
|
v-model="previewFormData"
|
||||||
|
>
|
||||||
<div class="grid-preview-container">
|
<div class="grid-preview-container">
|
||||||
<template
|
<template
|
||||||
v-for="(component, index) in formStore.formComponents"
|
v-for="(component, index) in formStore.formComponents"
|
||||||
@ -495,26 +502,53 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-lg font-medium mb-2">Custom JavaScript</h3>
|
<h3 class="text-lg font-medium mb-2">Custom JavaScript</h3>
|
||||||
<p class="text-sm text-gray-600 mb-4">
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
Write custom JavaScript to add dynamic behavior to your form.
|
Write custom JavaScript to add dynamic behavior to your form. Scripts run securely in a sandboxed environment with access to form fields and utility functions.
|
||||||
Use <code class="bg-gray-100 px-1 rounded">this.getField('fieldName')</code> to access form fields.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Helper Functions Reference -->
|
<!-- Helper Functions Reference -->
|
||||||
<div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200">
|
<div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200">
|
||||||
<details>
|
<details>
|
||||||
<summary class="text-sm font-medium text-blue-800 cursor-pointer">📚 Available Helper Functions</summary>
|
<summary class="text-sm font-medium text-blue-800 cursor-pointer">📚 Core Functions</summary>
|
||||||
<div class="mt-2 text-xs text-blue-700 space-y-1">
|
<div class="mt-2 text-xs text-blue-700 space-y-1">
|
||||||
<div><code>this.getField('name')</code> - Get field value</div>
|
<div><code>getField('name')</code> - Get field value</div>
|
||||||
<div><code>this.setField('name', value)</code> - Set field value</div>
|
<div><code>setField('name', value)</code> - Set field value and trigger events</div>
|
||||||
<div><code>this.hideField('name')</code> - Hide field</div>
|
<div><code>hideField('name')</code> - Hide a field</div>
|
||||||
<div><code>this.showField('name')</code> - Show field</div>
|
<div><code>showField('name')</code> - Show a field</div>
|
||||||
<div><code>this.disableField('name')</code> - Disable field</div>
|
<div><code>disableField('name')</code> - Disable a field</div>
|
||||||
<div><code>this.enableField('name')</code> - Enable field</div>
|
<div><code>enableField('name')</code> - Enable a field</div>
|
||||||
<div><code>this.validateField('name')</code> - Trigger field validation</div>
|
<div><code>validateField('name')</code> - Trigger field validation</div>
|
||||||
<div><code>this.getAllFieldValues()</code> - Get all form values</div>
|
<div><code>getAllFieldValues()</code> - Get all form values as object</div>
|
||||||
</div>
|
<div><code>onFieldChange(['field1', 'field2'], callback)</code> - Listen for field changes</div>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- User Interface Functions -->
|
||||||
|
<div class="mb-4 p-3 bg-green-50 rounded border border-green-200">
|
||||||
|
<details>
|
||||||
|
<summary class="text-sm font-medium text-green-800 cursor-pointer">🎨 UI & Notifications</summary>
|
||||||
|
<div class="mt-2 text-xs text-green-700 space-y-1">
|
||||||
|
<div><code>showSuccess('message')</code> - Display success notification</div>
|
||||||
|
<div><code>showError('message')</code> - Display error notification</div>
|
||||||
|
<div><code>showInfo('message')</code> - Display info notification</div>
|
||||||
|
<div><code>querySelector('selector')</code> - Safe DOM querying within form</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Utility Functions -->
|
||||||
|
<div class="mb-4 p-3 bg-purple-50 rounded border border-purple-200">
|
||||||
|
<details>
|
||||||
|
<summary class="text-sm font-medium text-purple-800 cursor-pointer">🛠️ Utilities</summary>
|
||||||
|
<div class="mt-2 text-xs text-purple-700 space-y-1">
|
||||||
|
<div><code>Math.*</code> - Mathematical functions (Math.round, Math.max, etc.)</div>
|
||||||
|
<div><code>Date</code> - Date object for date/time operations</div>
|
||||||
|
<div><code>Number()</code>, <code>String()</code>, <code>Array</code> - Type conversion</div>
|
||||||
|
<div><code>setTimeout()</code>, <code>setInterval()</code> - Timing functions</div>
|
||||||
|
<div><code>console.log()</code>, <code>console.warn()</code> - Debug logging</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RsCodeMirror
|
<RsCodeMirror
|
||||||
@ -522,21 +556,34 @@
|
|||||||
language="javascript"
|
language="javascript"
|
||||||
height="300px"
|
height="300px"
|
||||||
placeholder="// Example: Hide/show fields based on selection
|
placeholder="// Example: Hide/show fields based on selection
|
||||||
// this.onFieldChange('customer_type', (value) => {
|
// onFieldChange('customer_type', (value) => {
|
||||||
// if (value === 'business') {
|
// if (value === 'business') {
|
||||||
// this.showField('company_name');
|
// showField('company_name');
|
||||||
// this.showField('tax_id');
|
// showField('tax_id');
|
||||||
|
// showInfo('Business information fields are now visible');
|
||||||
// } else {
|
// } else {
|
||||||
// this.hideField('company_name');
|
// hideField('company_name');
|
||||||
// this.hideField('tax_id');
|
// hideField('tax_id');
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// Example: Calculate total
|
// Example: Real-time calculations
|
||||||
// this.onFieldChange(['quantity', 'price'], () => {
|
// onFieldChange(['quantity', 'price'], () => {
|
||||||
// const qty = this.getField('quantity') || 0;
|
// const qty = Number(getField('quantity')) || 0;
|
||||||
// const price = this.getField('price') || 0;
|
// const price = Number(getField('price')) || 0;
|
||||||
// this.setField('total', qty * price);
|
// const total = Math.round((qty * price) * 100) / 100;
|
||||||
|
// setField('total', total);
|
||||||
|
//
|
||||||
|
// if (total > 1000) {
|
||||||
|
// showSuccess('Discount applied for orders over $1000!');
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Example: Conditional validation
|
||||||
|
// onFieldChange('email', (value) => {
|
||||||
|
// if (value && !value.includes('@')) {
|
||||||
|
// showError('Please enter a valid email address');
|
||||||
|
// }
|
||||||
// });"
|
// });"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -598,8 +645,18 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-lg font-medium mb-2">Form Events</h3>
|
<h3 class="text-lg font-medium mb-2">Form Events</h3>
|
||||||
<p class="text-sm text-gray-600 mb-4">
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
Configure when your custom scripts should run.
|
Configure when your custom scripts should run. Field change events are detected automatically in real-time when users interact with form fields.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Important Note -->
|
||||||
|
<div class="mb-4 p-3 bg-amber-50 rounded border border-amber-200">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<Icon name="material-symbols:info" class="w-4 h-4 text-amber-600 mr-2 mt-0.5" />
|
||||||
|
<div class="text-xs text-amber-700">
|
||||||
|
<strong>Note:</strong> The "On Field Change" event must be enabled for <code>onFieldChange()</code> callbacks to work in your scripts.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@ -627,14 +684,20 @@
|
|||||||
|
|
||||||
<div class="border rounded p-4">
|
<div class="border rounded p-4">
|
||||||
<h4 class="font-medium mb-2">Script Execution Mode</h4>
|
<h4 class="font-medium mb-2">Script Execution Mode</h4>
|
||||||
<div class="space-y-2">
|
<div class="space-y-3">
|
||||||
<label class="flex items-center">
|
<label class="flex items-start">
|
||||||
<input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2">
|
<input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2 mt-0.5">
|
||||||
<span class="text-sm">Safe Mode (Recommended) - Limited but secure</span>
|
<div>
|
||||||
|
<span class="text-sm font-medium text-green-700">Safe Mode (Recommended)</span>
|
||||||
|
<p class="text-xs text-gray-600 mt-1">Scripts run in a secure sandbox with access only to form functions. Best for most use cases.</p>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-start">
|
||||||
<input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2">
|
<input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2 mt-0.5">
|
||||||
<span class="text-sm">Advanced Mode - Full JavaScript access</span>
|
<div>
|
||||||
|
<span class="text-sm font-medium text-orange-700">Advanced Mode</span>
|
||||||
|
<p class="text-xs text-gray-600 mt-1">Provides broader JavaScript access but with security restrictions. Use with caution.</p>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -796,6 +859,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useFormBuilderStore } from "~/stores/formBuilder";
|
import { useFormBuilderStore } from "~/stores/formBuilder";
|
||||||
|
import { nextTick } from 'vue';
|
||||||
import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
|
import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
|
||||||
import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
|
import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
|
||||||
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
||||||
@ -838,6 +902,8 @@ const showDropdown = ref(false);
|
|||||||
const showTemplatesModal = ref(false);
|
const showTemplatesModal = ref(false);
|
||||||
const showFieldSettings = ref(false);
|
const showFieldSettings = ref(false);
|
||||||
const showFieldSettingsPanel = ref(false);
|
const showFieldSettingsPanel = ref(false);
|
||||||
|
const previewForm = ref(null);
|
||||||
|
const formScriptEngine = ref(null);
|
||||||
|
|
||||||
// Settings tabs configuration
|
// Settings tabs configuration
|
||||||
const settingsTabs = [
|
const settingsTabs = [
|
||||||
@ -1617,118 +1683,127 @@ const previewFormData = ref({});
|
|||||||
|
|
||||||
// Initialize preview form data with default values
|
// Initialize preview form data with default values
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const newFormData = {};
|
console.log('[FormBuilder] watchEffect for previewFormData initialization. Current form components:', formStore.formComponents.length);
|
||||||
|
const existingFormData = { ...previewFormData.value }; // Preserve current user inputs
|
||||||
|
let newDefaults = {};
|
||||||
|
let hasNewComponents = false;
|
||||||
|
|
||||||
formStore.formComponents.forEach(component => {
|
formStore.formComponents.forEach(component => {
|
||||||
if (component.props.name) {
|
if (component.props.name) {
|
||||||
// Set default values based on component type
|
// If field is not already in existingFormData, it's a new component or needs initialization
|
||||||
switch (component.type) {
|
if (!existingFormData.hasOwnProperty(component.props.name)) {
|
||||||
case 'checkbox':
|
hasNewComponents = true;
|
||||||
newFormData[component.props.name] = [];
|
// Set default values based on component type
|
||||||
break;
|
switch (component.type) {
|
||||||
|
case 'checkbox':
|
||||||
case 'number':
|
newDefaults[component.props.name] = [];
|
||||||
case 'range':
|
break;
|
||||||
newFormData[component.props.name] = component.props.value || 0;
|
case 'number':
|
||||||
break;
|
case 'range':
|
||||||
|
newDefaults[component.props.name] = component.props.value || 0;
|
||||||
case 'switch':
|
break;
|
||||||
newFormData[component.props.name] = component.props.value !== undefined ? component.props.value : false;
|
case 'switch':
|
||||||
break;
|
newDefaults[component.props.name] = component.props.value !== undefined ? component.props.value : false;
|
||||||
|
break;
|
||||||
case 'color':
|
case 'color':
|
||||||
newFormData[component.props.name] = component.props.value || '#3b82f6';
|
newDefaults[component.props.name] = component.props.value || '#3b82f6';
|
||||||
break;
|
break;
|
||||||
|
case 'hidden':
|
||||||
case 'hidden':
|
newDefaults[component.props.name] = component.props.value || '';
|
||||||
newFormData[component.props.name] = component.props.value || '';
|
break;
|
||||||
break;
|
case 'repeating-group':
|
||||||
|
const initialGroups = [];
|
||||||
case 'image-preview':
|
const minItems = component.props.minItems || 1;
|
||||||
case 'info-display':
|
for (let i = 0; i < minItems; i++) {
|
||||||
case 'button':
|
const group = {};
|
||||||
// These are display-only components and don't need form data values
|
if (Array.isArray(component.props.fields)) {
|
||||||
break;
|
component.props.fields.forEach(field => {
|
||||||
|
switch (field.type) {
|
||||||
case 'repeating-group':
|
case 'number': group[field.name] = 0; break;
|
||||||
// Initialize with one empty group or use minItems to determine how many initial groups
|
case 'checkbox': group[field.name] = []; break;
|
||||||
const initialGroups = [];
|
case 'select':
|
||||||
const minItems = component.props.minItems || 1;
|
const defaultOption = Array.isArray(field.options) && field.options.length > 0 ? field.options[0].value : '';
|
||||||
|
group[field.name] = field.value || defaultOption || '';
|
||||||
// Create the specified number of initial groups
|
break;
|
||||||
for (let i = 0; i < minItems; i++) {
|
default: group[field.name] = field.value || '';
|
||||||
// Create an object with fields from the configuration
|
}
|
||||||
const group = {};
|
});
|
||||||
if (Array.isArray(component.props.fields)) {
|
}
|
||||||
component.props.fields.forEach(field => {
|
initialGroups.push(group);
|
||||||
// Initialize each field with appropriate default value
|
|
||||||
switch (field.type) {
|
|
||||||
case 'number':
|
|
||||||
group[field.name] = 0;
|
|
||||||
break;
|
|
||||||
case 'checkbox':
|
|
||||||
group[field.name] = [];
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
// If there's a default value in the options, use it
|
|
||||||
const defaultOption = Array.isArray(field.options) && field.options.length > 0
|
|
||||||
? field.options[0].value
|
|
||||||
: '';
|
|
||||||
group[field.name] = field.value || defaultOption || '';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
group[field.name] = field.value || '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
initialGroups.push(group);
|
newDefaults[component.props.name] = initialGroups;
|
||||||
}
|
break;
|
||||||
|
case 'dynamic-list':
|
||||||
newFormData[component.props.name] = initialGroups;
|
newDefaults[component.props.name] = Array.isArray(component.props.defaultItems) ? [...component.props.defaultItems] : [];
|
||||||
break;
|
break;
|
||||||
|
case 'select':
|
||||||
case 'dynamic-list':
|
case 'radio':
|
||||||
// Initialize with default items from configuration
|
if (Array.isArray(component.props.options) && component.props.options.length > 0) {
|
||||||
newFormData[component.props.name] = Array.isArray(component.props.defaultItems)
|
newDefaults[component.props.name] = component.props.value || component.props.options[0].value;
|
||||||
? [...component.props.defaultItems]
|
} else {
|
||||||
: [];
|
newDefaults[component.props.name] = '';
|
||||||
break;
|
}
|
||||||
|
break;
|
||||||
case 'select':
|
case 'date':
|
||||||
case 'radio':
|
case 'time':
|
||||||
// For select and radio, initialize with first option value if available
|
case 'datetime-local':
|
||||||
if (Array.isArray(component.props.options) && component.props.options.length > 0) {
|
case 'file': // file inputs are handled by FormKit, typically initialized to null or specific file objects
|
||||||
newFormData[component.props.name] = component.props.value || component.props.options[0].value;
|
newDefaults[component.props.name] = component.props.value || null;
|
||||||
} else {
|
break;
|
||||||
newFormData[component.props.name] = '';
|
// image-preview, info-display, button don't usually hold data in previewFormData directly
|
||||||
}
|
case 'image-preview':
|
||||||
break;
|
case 'info-display':
|
||||||
|
case 'button':
|
||||||
case 'date':
|
break;
|
||||||
case 'time':
|
default:
|
||||||
case 'datetime-local':
|
newDefaults[component.props.name] = component.props.value || '';
|
||||||
// Initialize with current date/time
|
}
|
||||||
newFormData[component.props.name] = component.props.value || '';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'file':
|
|
||||||
// Initialize file inputs as null
|
|
||||||
newFormData[component.props.name] = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// For text, textarea, email, password, url, tel, etc.
|
|
||||||
newFormData[component.props.name] = component.props.value || '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
previewFormData.value = newFormData;
|
|
||||||
|
// Only update previewFormData if it's the initial load (empty) or new components were added that need defaults
|
||||||
|
// This prevents overwriting user input when existing components change their props (which also triggers this watchEffect)
|
||||||
|
const isInitialLoad = Object.keys(previewFormData.value).length === 0 && Object.keys(newDefaults).length > 0;
|
||||||
|
|
||||||
|
if (isInitialLoad || hasNewComponents) {
|
||||||
|
console.log('[FormBuilder] Initializing/merging preview form data. Initial load:', isInitialLoad, 'Has new components:', hasNewComponents);
|
||||||
|
previewFormData.value = { ...existingFormData, ...newDefaults };
|
||||||
|
console.log('[FormBuilder] Preview form data after init/merge:', previewFormData.value);
|
||||||
|
} else {
|
||||||
|
console.log('[FormBuilder] Skipping full previewFormData re-initialization to preserve user input.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle script-driven field changes
|
// Handle script-driven field changes
|
||||||
const handleScriptFieldChange = ({ fieldName, value }) => {
|
const handleScriptFieldChange = ({ fieldName, value }) => {
|
||||||
previewFormData.value[fieldName] = value;
|
console.log('[FormBuilder] Script field change:', fieldName, '=', value);
|
||||||
|
|
||||||
|
// Update the reactive form data
|
||||||
|
const newFormData = {
|
||||||
|
...previewFormData.value,
|
||||||
|
[fieldName]: value
|
||||||
|
};
|
||||||
|
|
||||||
|
previewFormData.value = newFormData;
|
||||||
|
|
||||||
// Make form data accessible to component previews
|
// Make form data accessible to component previews
|
||||||
formStore.updatePreviewFormData(previewFormData.value);
|
formStore.updatePreviewFormData(newFormData);
|
||||||
|
|
||||||
|
// Try to force FormKit form to update
|
||||||
|
nextTick(() => {
|
||||||
|
console.log('[FormBuilder] Updated form data:', newFormData);
|
||||||
|
|
||||||
|
// Try to access the FormKit form node and update it directly
|
||||||
|
if (previewForm.value && previewForm.value.node) {
|
||||||
|
try {
|
||||||
|
previewForm.value.node.input(newFormData);
|
||||||
|
console.log('[FormBuilder] Force updated FormKit form');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[FormBuilder] Could not force update FormKit form:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle script-driven field validation
|
// Handle script-driven field validation
|
||||||
@ -1737,11 +1812,46 @@ const handleScriptFieldValidate = ({ fieldName }) => {
|
|||||||
console.log(`Validating field: ${fieldName}`);
|
console.log(`Validating field: ${fieldName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make form data accessible to component previews
|
// Handle FormKit form input events
|
||||||
watchEffect(() => {
|
const handleFormKitInput = (formData, node) => {
|
||||||
|
console.log('[FormBuilder] FormKit input event received!');
|
||||||
|
console.log('[FormBuilder] FormKit formData:', JSON.parse(JSON.stringify(formData)));
|
||||||
|
console.log('[FormBuilder] Current previewFormData before update:', JSON.parse(JSON.stringify(previewFormData.value)));
|
||||||
|
|
||||||
|
// Update our reactive form data - this should trigger the FormScriptEngine watcher
|
||||||
|
const oldPreviewData = { ...previewFormData.value };
|
||||||
|
previewFormData.value = { ...formData };
|
||||||
|
|
||||||
|
console.log('[FormBuilder] Updated previewFormData to:', JSON.parse(JSON.stringify(previewFormData.value)));
|
||||||
|
console.log('[FormBuilder] Did previewFormData actually change?', JSON.stringify(oldPreviewData) !== JSON.stringify(previewFormData.value));
|
||||||
|
|
||||||
|
// Make form data accessible to component previews
|
||||||
formStore.updatePreviewFormData(previewFormData.value);
|
formStore.updatePreviewFormData(previewFormData.value);
|
||||||
|
|
||||||
|
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make form data accessible to component previews (for UI rendering, not for triggering script engine)
|
||||||
|
watchEffect(() => {
|
||||||
|
if (formStore) { // Ensure formStore is available
|
||||||
|
formStore.updatePreviewFormData(previewFormData.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for changes in previewFormData to trigger FormScriptEngine
|
||||||
|
watch(() => previewFormData.value, (newData, oldData) => {
|
||||||
|
if (!isPreview.value) return; // Only in preview mode
|
||||||
|
|
||||||
|
console.log('[FormBuilder] previewFormData watcher triggered!');
|
||||||
|
console.log('[FormBuilder] New data:', JSON.parse(JSON.stringify(newData)));
|
||||||
|
console.log('[FormBuilder] Old data:', oldData ? JSON.parse(JSON.stringify(oldData)) : 'undefined');
|
||||||
|
|
||||||
|
// Update form store
|
||||||
|
formStore.updatePreviewFormData(newData);
|
||||||
|
|
||||||
|
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
|
||||||
|
}, { deep: true, immediate: false });
|
||||||
|
|
||||||
const navigateToManage = () => {
|
const navigateToManage = () => {
|
||||||
// If already confirmed or no unsaved changes, navigate directly
|
// If already confirmed or no unsaved changes, navigate directly
|
||||||
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {
|
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {
|
||||||
|
@ -312,6 +312,35 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"customScript": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"customCSS": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formEvents": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scriptMode": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"default": "safe"
|
||||||
|
},
|
||||||
"formStatus": {
|
"formStatus": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "active"
|
"default": "active"
|
||||||
|
@ -823,6 +823,11 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Update preview form data
|
||||||
|
updatePreviewFormData(data) {
|
||||||
|
this.previewFormData = { ...data };
|
||||||
|
},
|
||||||
|
|
||||||
// Optimize the grid layout by analyzing the current components
|
// Optimize the grid layout by analyzing the current components
|
||||||
// and adjusting their sizes to fill available spaces
|
// and adjusting their sizes to fill available spaces
|
||||||
optimizeGridLayout() {
|
optimizeGridLayout() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user