Enhance Form Builder with Conditional Logic and Dynamic List Features

- Introduced a new ConditionalLogicEngine component to manage and execute conditional logic for form fields, allowing for dynamic visibility and behavior based on user input.
- Added conditional logic properties to various form components, enabling users to define conditions for showing, hiding, enabling, or disabling fields.
- Enhanced the dynamic list component with new settings for item validation, uniqueness, and import/export functionality, improving data management capabilities.
- Updated FormBuilderFieldSettingsModal to include a visual condition builder interface for easier configuration of conditional logic.
- Improved documentation to reflect the new features and provide guidance on using conditional logic within forms.
This commit is contained in:
Afiq 2025-05-30 12:29:11 +08:00
parent fa90c234c5
commit db3b00ce11
11 changed files with 1897 additions and 258 deletions

View File

@ -15,6 +15,6 @@ export default {
props: ["accept", "multiple", "maxSize", "minSize", "maxFiles", "disabled"],
}),
switch: createInput(Switch, {
props: ["value"],
props: ["value", "disabled", "name", "id"],
}),
};

View File

@ -1,10 +1,6 @@
<template>
<div
class="component-preview form-field"
:style="componentStyle"
:data-name="component.props.name"
:data-type="component.type"
>
<div class="component-preview form-field" :style="componentStyle" :data-name="component.props.name"
:data-type="component.type">
<!-- Hidden Field Special Display -->
<div v-if="component.type === 'hidden' && isPreview" class="py-2">
<div class="hidden-field-preview border-2 border-dashed border-gray-300 bg-gray-50 rounded-lg p-3">
@ -27,46 +23,27 @@
</div>
<!-- Basic Input Types (including radio and checkbox) -->
<FormKit
v-else-if="isInputType"
:id="`preview-${component.id}`"
:type="component.type"
:name="component.props.name"
:label="component.props.label"
:help="component.props.help"
:placeholder="component.props.placeholder"
:validation="component.props.validation"
:validation-visibility="isPreview ? 'live' : 'blur'"
:readonly="isPreview"
:options="component.props.options || undefined"
:value="component.props.value || undefined"
:accept="component.props.accept || undefined"
:max="component.props.max || undefined"
:mask="component.props.mask || undefined"
:digits="component.props.digits || undefined"
:multiple="component.props.multiple || undefined"
:maxSize="component.props.maxSize || undefined"
:maxFiles="component.props.maxFiles || undefined"
:classes="component.type === 'checkbox' ? {
<FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type" :name="component.props.name"
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
:readonly="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
:maxFiles="component.props.maxFiles || undefined" :classes="component.type === 'checkbox' ? {
wrapper: 'mb-1',
options: 'space-y-0.5'
} : {}"
:class="{ 'canvas-component': isPreview }"
/>
} : {}" :class="{ 'canvas-component': isPreview }" />
<!-- Heading -->
<div v-else-if="component.type === 'heading'" class="py-2">
<component
:is="`h${component.props.level || 2}`"
class="font-semibold"
:class="{
'text-2xl': component.props.level === 2,
'text-xl': component.props.level === 3,
'text-lg': component.props.level === 4
}"
>
<component :is="`h${component.props.level || 2}`" class="font-semibold" :class="{
'text-2xl': component.props.level === 2,
'text-xl': component.props.level === 3,
'text-lg': component.props.level === 4
}">
{{ component.props.value || 'Heading Text' }}
</component>
</div>
@ -78,13 +55,10 @@
<!-- Information Display -->
<div v-else-if="component.type === 'info-display'" class="py-2">
<div
class="info-display-container rounded"
:style="{
backgroundColor: component.props.backgroundColor || '#f9fafb',
border: component.props.showBorder ? '1px solid #e5e7eb' : 'none'
}"
>
<div class="info-display-container rounded" :style="{
backgroundColor: component.props.backgroundColor || '#f9fafb',
border: component.props.showBorder ? '1px solid #e5e7eb' : 'none'
}">
<!-- Title -->
<div v-if="component.props.title" class="p-3 border-b border-gray-200 bg-white">
<h4 class="text-base font-medium text-gray-800">{{ component.props.title }}</h4>
@ -92,21 +66,16 @@
<!-- Fields Display -->
<div class="p-4">
<div
v-if="component.props.layout === 'grid'"
class="grid grid-cols-2 gap-4"
>
<div v-if="component.props.layout === 'grid'" class="grid grid-cols-2 gap-4">
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
</div>
</div>
<div
v-else-if="component.props.layout === 'horizontal'"
class="space-y-2"
>
<div v-for="(field, index) in component.props.fields" :key="index" class="flex justify-between items-center">
<div v-else-if="component.props.layout === 'horizontal'" class="space-y-2">
<div v-for="(field, index) in component.props.fields" :key="index"
class="flex justify-between items-center">
<dt class="text-sm font-medium text-gray-600">{{ field.label }}:</dt>
<dd class="text-sm text-gray-900">{{ field.value }}</dd>
</div>
@ -129,31 +98,27 @@
<!-- Image Preview Component -->
<div v-else-if="component.type === 'image-preview'" class="py-2">
<div class="image-preview-container" :class="{ 'has-caption': component.props.showCaption && component.props.caption }">
<div class="image-preview-container"
:class="{ 'has-caption': component.props.showCaption && component.props.caption }">
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
{{ component.props.label }}
</label>
<div
class="image-container relative"
:class="{ 'cursor-zoom-in': component.props.showZoom }"
@click="handleImageClick"
>
<img
:src="component.props.imageUrl"
:alt="component.props.altText || 'Image preview'"
class="max-w-full rounded"
:style="{
<div class="image-container relative" :class="{ 'cursor-zoom-in': component.props.showZoom }"
@click="handleImageClick">
<img :src="component.props.imageUrl" :alt="component.props.altText || 'Image preview'"
class="max-w-full rounded" :style="{
maxWidth: component.props.maxWidth || '100%',
height: component.props.height || 'auto',
}"
/>
<div v-if="component.props.showZoom" class="absolute bottom-2 right-2 bg-black bg-opacity-60 text-white p-1 rounded">
}" />
<div v-if="component.props.showZoom"
class="absolute bottom-2 right-2 bg-black bg-opacity-60 text-white p-1 rounded">
<Icon name="material-symbols:zoom-in" class="w-4 h-4" />
</div>
</div>
<div v-if="component.props.showCaption && component.props.caption" class="mt-1 text-sm text-gray-500 text-center">
<div v-if="component.props.showCaption && component.props.caption"
class="mt-1 text-sm text-gray-500 text-center">
{{ component.props.caption }}
</div>
@ -184,22 +149,14 @@
<div class="space-y-3">
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
<FormKit
:type="field.type"
:label="field.label"
:placeholder="field.placeholder"
:name="`${field.name}_1`"
:options="field.options"
disabled
/>
<FormKit :type="field.type" :label="field.label" :placeholder="field.placeholder"
:name="`${field.name}_1`" :options="field.options" disabled />
</template>
</div>
</div>
<button
type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
>
<button type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
{{ component.props.buttonText || 'Add Item' }}
</button>
@ -207,43 +164,29 @@
<!-- Functional groups (in form view) -->
<div v-else class="repeating-groups space-y-4">
<div
v-for="(group, groupIndex) in (previewFormData[component.props.name] || [])"
:key="groupIndex"
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50"
>
<div v-for="(group, groupIndex) in (previewFormData[component.props.name] || [])" :key="groupIndex"
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
<div class="flex justify-between items-center mb-3">
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
<button
v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 1)"
type="button"
class="text-red-500 hover:text-red-700 text-sm"
@click="removeGroupItem(groupIndex)"
>
<button v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 1)"
type="button" class="text-red-500 hover:text-red-700 text-sm" @click="removeGroupItem(groupIndex)">
{{ component.props.removeText || 'Remove' }}
</button>
</div>
<div class="space-y-3">
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
<FormKit
:type="field.type"
:label="field.label"
:placeholder="field.placeholder"
:name="`${component.props.name}.${groupIndex}.${field.name}`"
:options="field.options"
v-model="group[field.name]"
/>
<FormKit :type="field.type" :label="field.label" :placeholder="field.placeholder"
:name="`${component.props.name}.${groupIndex}.${field.name}`" :options="field.options"
v-model="group[field.name]" />
</template>
</div>
</div>
<button
v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 10)"
<button v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 10)"
type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
@click="addGroupItem"
>
@click="addGroupItem">
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
{{ component.props.buttonText || 'Add Item' }}
</button>
@ -256,68 +199,116 @@
<div class="dynamic-list-container">
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
{{ component.props.label }}
<span v-if="component.props.showItemCounter" class="text-xs text-gray-500 ml-2">
({{ (previewFormData[component.props.name] || []).length }}/{{ component.props.maxItems || 20 }})
</span>
</label>
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
{{ component.props.help }}
</div>
<!-- Search Box (if enabled) -->
<div v-if="component.props.enableSearch" class="mb-3">
<div class="relative">
<input type="text" v-model="searchQuery[component.props.name]" placeholder="Search items..."
class="w-full px-3 py-2 pl-9 border border-gray-300 rounded-md text-sm" />
<Icon name="material-symbols:search"
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
<!-- Bulk Operations Toolbar (if enabled) -->
<div v-if="component.props.bulkOperations && (previewFormData[component.props.name] || []).length > 0"
class="mb-3 flex items-center justify-between bg-gray-50 p-2 rounded">
<div class="flex items-center space-x-2">
<input type="checkbox" :checked="isAllSelected(component.props.name)"
@change="toggleSelectAll(component.props.name)" class="h-4 w-4 rounded border-gray-300" />
<span class="text-xs text-gray-600">
{{ selectedItems[component.props.name]?.length || 0 }} selected
</span>
</div>
<div class="flex space-x-2">
<button v-if="(selectedItems[component.props.name]?.length || 0) > 0"
@click="deleteSelectedItems(component.props.name)"
class="text-xs text-red-600 hover:text-red-800 flex items-center">
<Icon name="material-symbols:delete-outline" class="w-4 h-4 mr-1" />
Delete Selected
</button>
<button v-if="component.props.importEnabled" @click="showImportModal(component.props.name)"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center">
<Icon name="material-symbols:upload-file-outline" class="w-4 h-4 mr-1" />
Import
</button>
</div>
</div>
<div class="space-y-2">
<div v-if="isPreview" class="list-items space-y-2">
<div v-for="(item, index) in component.props.defaultItems" :key="index" class="flex items-center">
<input
type="text"
:value="item"
:placeholder="component.props.placeholder"
<input :type="component.props.itemType || 'text'" :value="item" :placeholder="component.props.placeholder"
disabled
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700 disabled:bg-gray-50"
/>
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700 disabled:bg-gray-50" />
<div v-if="component.props.enableSorting" class="ml-2 cursor-move text-gray-400">
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
</div>
<button type="button" class="ml-2 text-red-500 hover:text-red-700">
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
</button>
</div>
<!-- Add button for preview -->
<button
type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
>
<button type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
{{ component.props.buttonText || 'Add Item' }}
</button>
</div>
<div v-else class="list-items space-y-2">
<div
v-for="(item, index) in (previewFormData[component.props.name] || [])"
:key="index"
class="flex items-center"
>
<input
type="text"
v-model="previewFormData[component.props.name][index]"
<div v-for="(item, index) in getFilteredItems(component.props.name)" :key="index"
class="flex items-center group">
<input v-if="component.props.bulkOperations" type="checkbox"
:checked="isItemSelected(component.props.name, index)"
@change="toggleItemSelection(component.props.name, index)"
class="mr-2 h-4 w-4 rounded border-gray-300" />
<input :type="component.props.itemType || 'text'" v-model="previewFormData[component.props.name][index]"
:placeholder="component.props.placeholder"
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700"
/>
<button
v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 0)"
type="button"
class="ml-2 text-red-500 hover:text-red-700"
@click="removeListItem(index)"
>
:class="getItemInputClasses(component.props.name, index, item)"
@blur="validateItem(component.props.name, index, item)"
@input="handleItemInput(component.props.name, index, $event.target.value)" />
<div v-if="component.props.enableSorting"
class="ml-2 cursor-move text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity">
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
</div>
<button v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 0)"
type="button" class="ml-2 text-red-500 hover:text-red-700" @click="removeListItem(index)">
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
</button>
</div>
<button
v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 20)"
<!-- Validation Messages -->
<div v-if="validationErrors[component.props.name]" class="text-xs text-red-600 mt-1">
{{ validationErrors[component.props.name] }}
</div>
<button v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 20)"
type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
@click="addListItem"
>
@click="addListItem">
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
{{ component.props.buttonText || 'Add Item' }}
</button>
<!-- Export Button (if enabled) -->
<div v-if="component.props.exportFormat && (previewFormData[component.props.name] || []).length > 0"
class="mt-2">
<button @click="exportItems(component.props.name, component.props.exportFormat)"
class="text-xs text-green-600 hover:text-green-800 flex items-center">
<Icon name="material-symbols:download" class="w-4 h-4 mr-1" />
Export as {{ component.props.exportFormat.toUpperCase() }}
</button>
</div>
</div>
</div>
</div>
@ -329,14 +320,9 @@
{{ component.props.label }}
</label>
<RsButton
:type="component.props.buttonType || 'button'"
:variant="component.props.variant || 'primary'"
:size="component.props.size || 'md'"
:disabled="component.props.disabled || false"
@click="handleButtonClick"
class="button-component"
>
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
class="button-component">
{{ component.props.label || 'Button' }}
</RsButton>
@ -373,6 +359,47 @@ const props = defineProps({
const formStore = useFormBuilderStore();
const previewFormData = computed(() => formStore.previewFormData || {});
// Enhanced dynamic list functionality
const searchQuery = ref({});
const selectedItems = ref({});
const validationErrors = ref({});
// Initialize data for components
onMounted(() => {
// Initialize search and selection data for dynamic lists
if (props.component.type === 'dynamic-list') {
const listName = props.component.props.name;
if (listName) {
searchQuery.value[listName] = '';
selectedItems.value[listName] = [];
validationErrors.value[listName] = '';
// Initialize form data with default items if they exist and form data is empty
const currentFormData = formStore.previewFormData[listName];
const defaultItems = props.component.props.defaultItems;
if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) {
const updatedData = { ...formStore.previewFormData, [listName]: [...defaultItems] };
formStore.updatePreviewFormData(updatedData);
}
}
}
});
// Watch for changes to component props, especially defaultItems
watch(() => props.component.props.defaultItems, (newDefaultItems, oldDefaultItems) => {
if (props.component.type === 'dynamic-list') {
const listName = props.component.props.name;
if (!listName) return;
// Always update when defaultItems change, regardless of current form data
const items = newDefaultItems || [];
const updatedData = { ...formStore.previewFormData, [listName]: [...items] };
formStore.updatePreviewFormData(updatedData);
}
}, { deep: true, immediate: true });
// Repeating group and dynamic list functionality
const addGroupItem = () => {
if (props.isPreview) return;
@ -452,6 +479,256 @@ const removeListItem = (index) => {
formStore.updatePreviewFormData(updatedData);
};
// Enhanced Dynamic List Methods
// Validate individual item based on component settings
const validateItem = (listName, index, value) => {
const component = props.component;
if (!component.props.itemValidation) return true;
// Simple validation for now - can be enhanced with more complex rules
const rules = component.props.itemValidation.split('|');
for (const rule of rules) {
if (rule === 'required' && (!value || value.trim() === '')) {
validationErrors.value[listName] = `Item ${index + 1} is required`;
return false;
}
if (rule.startsWith('min:')) {
const minLength = parseInt(rule.split(':')[1]);
if (value.length < minLength) {
validationErrors.value[listName] = `Item ${index + 1} must be at least ${minLength} characters`;
return false;
}
}
if (rule.startsWith('max:')) {
const maxLength = parseInt(rule.split(':')[1]);
if (value.length > maxLength) {
validationErrors.value[listName] = `Item ${index + 1} cannot exceed ${maxLength} characters`;
return false;
}
}
if (rule === 'email' && component.props.itemType === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
validationErrors.value[listName] = `Item ${index + 1} must be a valid email address`;
return false;
}
}
if (rule === 'url' && component.props.itemType === 'url') {
try {
new URL(value);
} catch {
validationErrors.value[listName] = `Item ${index + 1} must be a valid URL`;
return false;
}
}
}
// Clear validation error if validation passes
validationErrors.value[listName] = '';
return true;
};
// Check for duplicate items
const checkDuplicates = (listName, newValue, currentIndex) => {
if (props.component.props.allowDuplicates) return true;
const currentItems = formStore.previewFormData[listName] || [];
const duplicateIndex = currentItems.findIndex((item, index) =>
index !== currentIndex && item.toLowerCase() === newValue.toLowerCase()
);
if (duplicateIndex !== -1) {
validationErrors.value[listName] = `Duplicate item detected. Item "${newValue}" already exists.`;
return false;
}
validationErrors.value[listName] = '';
return true;
};
// Handle item input with validation
const handleItemInput = (listName, index, value) => {
// Check for duplicates if not allowed
if (!checkDuplicates(listName, value, index)) {
return;
}
// Validate the item
validateItem(listName, index, value);
};
// Get CSS classes for item input based on validation state
const getItemInputClasses = (listName, index, value) => {
const baseClasses = "block w-full border rounded-md shadow-sm py-2 px-3 text-gray-700";
const hasError = validationErrors.value[listName];
if (hasError) {
return `${baseClasses} border-red-300 focus:border-red-500`;
}
return `${baseClasses} border-gray-300 focus:border-blue-500 focus:ring-blue-500`;
};
// Filter items based on search query
const getFilteredItems = (listName) => {
const items = formStore.previewFormData[listName] || [];
const query = searchQuery.value[listName];
if (!query || !props.component.props.enableSearch) {
return items;
}
return items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
};
// Bulk operations
const isItemSelected = (listName, index) => {
return selectedItems.value[listName]?.includes(index) || false;
};
const toggleItemSelection = (listName, index) => {
if (!selectedItems.value[listName]) {
selectedItems.value[listName] = [];
}
const selectedList = selectedItems.value[listName];
const selectedIndex = selectedList.indexOf(index);
if (selectedIndex > -1) {
selectedList.splice(selectedIndex, 1);
} else {
selectedList.push(index);
}
};
const isAllSelected = (listName) => {
const items = formStore.previewFormData[listName] || [];
const selected = selectedItems.value[listName] || [];
return items.length > 0 && selected.length === items.length;
};
const toggleSelectAll = (listName) => {
const items = formStore.previewFormData[listName] || [];
if (isAllSelected(listName)) {
selectedItems.value[listName] = [];
} else {
selectedItems.value[listName] = items.map((_, index) => index);
}
};
const deleteSelectedItems = (listName) => {
if (!props.component.props.confirmDelete || confirm('Are you sure you want to delete the selected items?')) {
const currentItems = [...(formStore.previewFormData[listName] || [])];
const selected = selectedItems.value[listName] || [];
// Sort indices in descending order to avoid index shifting issues
selected.sort((a, b) => b - a);
// Remove selected items
selected.forEach(index => {
currentItems.splice(index, 1);
});
// Update form data
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
formStore.updatePreviewFormData(updatedData);
// Clear selection
selectedItems.value[listName] = [];
}
};
// Import/Export functionality
const exportItems = (listName, format) => {
const items = formStore.previewFormData[listName] || [];
let content = '';
let filename = `${listName}_items`;
let mimeType = 'text/plain';
switch (format) {
case 'json':
content = JSON.stringify(items, null, 2);
filename += '.json';
mimeType = 'application/json';
break;
case 'csv':
content = items.map(item => `"${item}"`).join('\n');
filename += '.csv';
mimeType = 'text/csv';
break;
default: // txt
content = items.join('\n');
filename += '.txt';
break;
}
// Create and trigger download
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
};
const showImportModal = (listName) => {
// This would open a modal for importing items
// For now, let's use a simple file input
const input = document.createElement('input');
input.type = 'file';
input.accept = '.txt,.csv,.json';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
let importedItems = [];
try {
if (file.name.endsWith('.json')) {
importedItems = JSON.parse(content);
} else if (file.name.endsWith('.csv')) {
importedItems = content.split('\n').map(line => line.replace(/"/g, '').trim()).filter(Boolean);
} else {
importedItems = content.split('\n').filter(Boolean);
}
// Add imported items to current list
const currentItems = [...(formStore.previewFormData[listName] || [])];
const newItems = [...currentItems, ...importedItems];
// Respect max items limit
const maxItems = props.component.props.maxItems || 20;
if (newItems.length > maxItems) {
newItems.splice(maxItems);
alert(`Import limited to ${maxItems} items total.`);
}
// Update form data
const updatedData = { ...formStore.previewFormData, [listName]: newItems };
formStore.updatePreviewFormData(updatedData);
} catch (error) {
alert('Error importing file. Please check the file format.');
}
};
reader.readAsText(file);
}
};
input.click();
};
// Handle button click
const handleButtonClick = () => {
if (props.isPreview) return;
@ -475,11 +752,8 @@ const handleButtonClick = () => {
}
// Default behavior - log the button click
console.log(`Button clicked: ${props.component.props.label || 'Unnamed Button'}`);
};
// Check if the component is a standard FormKit input type (excluding specialized components)
const isStandardInputType = computed(() => {
const standardInputTypes = [

View File

@ -0,0 +1,150 @@
<template>
<div class="conditional-logic-engine">
<!-- This component handles conditional logic execution -->
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue'
const props = defineProps({
formComponents: {
type: Array,
required: true
},
formData: {
type: Object,
required: true
}
})
const emit = defineEmits(['script-generated'])
// Internal state
const generatedScript = ref('')
const isInitialized = ref(false)
// Generate conditional logic script from form components
const generateConditionalLogicScript = () => {
const scriptsArray = []
// Process each component that has conditional logic enabled
props.formComponents.forEach(component => {
const conditionalLogic = component.props.conditionalLogic
if (!conditionalLogic || !conditionalLogic.enabled || !conditionalLogic.conditions.length) {
return
}
const { conditions, action, operator } = conditionalLogic
const fieldName = component.props.name
if (!fieldName) return
// Generate condition checks
const conditionChecks = conditions.map(condition => {
const { field, operator: condOp, value } = condition
switch (condOp) {
case 'equals':
return `getField('${field}') === '${value}'`
case 'not_equals':
return `getField('${field}') !== '${value}'`
case 'contains':
return `String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())`
case 'not_contains':
return `!String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())`
case 'is_empty':
return `!getField('${field}') || getField('${field}') === ''`
case 'is_not_empty':
return `getField('${field}') && getField('${field}') !== ''`
case 'greater_than':
return `Number(getField('${field}')) > ${Number(value) || 0}`
case 'less_than':
return `Number(getField('${field}')) < ${Number(value) || 0}`
default:
return `getField('${field}') === '${value}'`
}
}).join(` ${operator} `)
// Generate action functions
const actionCode = action === 'show' ? `showField('${fieldName}')` :
action === 'hide' ? `hideField('${fieldName}')` :
action === 'enable' ? `enableField('${fieldName}')` :
action === 'disable' ? `disableField('${fieldName}')` :
`showField('${fieldName}')`
const inverseActionCode = action === 'show' ? `hideField('${fieldName}')` :
action === 'hide' ? `showField('${fieldName}')` :
action === 'enable' ? `disableField('${fieldName}')` :
action === 'disable' ? `enableField('${fieldName}')` :
`hideField('${fieldName}')`
// Generate field change listeners
const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))]
watchedFields.forEach(watchField => {
const script = `
// Conditional logic for field: ${fieldName}
onFieldChange('${watchField}', function() {
if (${conditionChecks}) {
${actionCode};
} else {
${inverseActionCode};
}
});`
scriptsArray.push(script)
})
// Also add initial evaluation
const initialScript = `
// Initial evaluation for field: ${fieldName}
(function() {
if (${conditionChecks}) {
${actionCode};
} else {
${inverseActionCode};
}
})();`
scriptsArray.push(initialScript)
})
return scriptsArray.join('\n\n')
}
// Update generated script when components change
watch(() => props.formComponents, () => {
if (isInitialized.value) {
updateConditionalLogic()
}
}, { deep: true })
// Update conditional logic
const updateConditionalLogic = () => {
const newScript = generateConditionalLogicScript()
generatedScript.value = newScript
// Emit the generated script to parent component
emit('script-generated', newScript)
}
// Initialize on mount
onMounted(() => {
nextTick(() => {
updateConditionalLogic()
isInitialized.value = true
})
})
// Expose the generated script for debugging
defineExpose({
generatedScript,
updateConditionalLogic
})
</script>
<style scoped>
.conditional-logic-engine {
display: none; /* This component is purely functional */
}
</style>

View File

@ -81,7 +81,7 @@
<div class="p-3">
<component-preview
:component="element"
:is-preview="true"
:is-preview="false"
/>
</div>

View File

@ -129,13 +129,20 @@ const availableComponents = [
type: 'text',
name: 'Text Field',
category: 'Basic Inputs',
icon: 'material-symbols:text-fields',
icon: 'heroicons:document-text',
description: 'Single line text input',
defaultProps: {
type: 'text',
placeholder: 'Enter text...',
help: '',
validation: ''
validation: '',
// Conditional Logic Properties
conditionalLogic: {
enabled: false,
conditions: [], // Array of condition objects
action: 'show', // 'show', 'hide', 'enable', 'disable'
operator: 'and' // 'and', 'or'
}
}
},
{
@ -148,7 +155,14 @@ const availableComponents = [
type: 'textarea',
placeholder: 'Enter text...',
help: '',
validation: ''
validation: '',
// Conditional Logic Properties
conditionalLogic: {
enabled: false,
conditions: [],
action: 'show',
operator: 'and'
}
}
},
{
@ -161,7 +175,14 @@ const availableComponents = [
type: 'number',
placeholder: '0',
help: '',
validation: ''
validation: '',
// Conditional Logic Properties
conditionalLogic: {
enabled: false,
conditions: [],
action: 'show',
operator: 'and'
}
}
},
{
@ -174,7 +195,14 @@ const availableComponents = [
type: 'email',
placeholder: 'email@example.com',
help: '',
validation: 'email'
validation: 'email',
// Conditional Logic Properties
conditionalLogic: {
enabled: false,
conditions: [],
action: 'show',
operator: 'and'
}
}
},
{
@ -187,7 +215,14 @@ const availableComponents = [
type: 'password',
placeholder: 'Enter password...',
help: '',
validation: ''
validation: '',
// Conditional Logic Properties
conditionalLogic: {
enabled: false,
conditions: [],
action: 'show',
operator: 'and'
}
}
},
{
@ -259,7 +294,14 @@ const availableComponents = [
{ label: 'Option 2', value: 'option_2' },
{ label: 'Option 3', value: 'option_3' }
],
validation: ''
validation: '',
// Conditional Logic Properties
conditionalLogic: {
enabled: false,
conditions: [],
action: 'show',
operator: 'and'
}
}
},
{
@ -449,7 +491,17 @@ const availableComponents = [
buttonText: 'Add Item',
minItems: 0,
maxItems: 20,
defaultItems: ['Item 1', 'Item 2']
defaultItems: ['Item 1', 'Item 2'],
itemValidation: '',
allowDuplicates: true,
enableSorting: false,
enableSearch: false,
itemType: 'text',
showItemCounter: true,
confirmDelete: false,
bulkOperations: false,
exportFormat: 'json',
importEnabled: false
}
},
{

View File

@ -211,6 +211,131 @@
/>
</div>
<!-- New Enhanced Settings -->
<div class="space-y-4 border-t pt-4 mt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Item Settings</h4>
<!-- Item Type -->
<FormKit
v-if="showField('itemType')"
type="select"
label="Item Type"
name="itemType"
v-model="configModel.itemType"
:options="[
{ label: 'Text', value: 'text' },
{ label: 'Number', value: 'number' },
{ label: 'Email', value: 'email' },
{ label: 'URL', value: 'url' }
]"
help="Type of data for list items"
/>
<!-- Item Validation -->
<FormKit
v-if="showField('itemValidation')"
type="text"
label="Item Validation Rules"
name="itemValidation"
v-model="configModel.itemValidation"
help="Validation rules for individual items (e.g., 'required|min:3|max:50')"
placeholder="required|min:3"
/>
<!-- Behavior Settings -->
<div class="grid grid-cols-2 gap-3">
<div v-if="showField('allowDuplicates')" class="space-y-2">
<FormKit
type="checkbox"
label="Allow Duplicates"
name="allowDuplicates"
v-model="configModel.allowDuplicates"
help="Allow duplicate items in the list"
/>
</div>
<div v-if="showField('enableSorting')" class="space-y-2">
<FormKit
type="checkbox"
label="Enable Sorting"
name="enableSorting"
v-model="configModel.enableSorting"
help="Allow drag & drop reordering of items"
/>
</div>
<div v-if="showField('enableSearch')" class="space-y-2">
<FormKit
type="checkbox"
label="Enable Search"
name="enableSearch"
v-model="configModel.enableSearch"
help="Add search/filter functionality"
/>
</div>
<div v-if="showField('showItemCounter')" class="space-y-2">
<FormKit
type="checkbox"
label="Show Item Counter"
name="showItemCounter"
v-model="configModel.showItemCounter"
help="Display current item count"
/>
</div>
<div v-if="showField('confirmDelete')" class="space-y-2">
<FormKit
type="checkbox"
label="Confirm Delete"
name="confirmDelete"
v-model="configModel.confirmDelete"
help="Require confirmation before deleting items"
/>
</div>
<div v-if="showField('bulkOperations')" class="space-y-2">
<FormKit
type="checkbox"
label="Bulk Operations"
name="bulkOperations"
v-model="configModel.bulkOperations"
help="Enable bulk select/delete operations"
/>
</div>
</div>
<!-- Import/Export Settings -->
<div class="space-y-3 border-t pt-3">
<h5 class="text-xs font-medium text-gray-600 uppercase tracking-wider">Import/Export</h5>
<div class="grid grid-cols-2 gap-3">
<FormKit
v-if="showField('importEnabled')"
type="checkbox"
label="Enable Import"
name="importEnabled"
v-model="configModel.importEnabled"
help="Allow importing items from file"
/>
<FormKit
v-if="showField('exportFormat')"
type="select"
label="Export Format"
name="exportFormat"
v-model="configModel.exportFormat"
:options="[
{ label: 'JSON', value: 'json' },
{ label: 'CSV', value: 'csv' },
{ label: 'Plain Text', value: 'txt' }
]"
help="Default export format for items"
/>
</div>
</div>
</div>
<div v-if="showField('defaultItems')">
<label class="block text-sm font-medium text-gray-700 mb-1">Default Items</label>
<div class="border rounded-md p-3 bg-gray-50 space-y-2">
@ -919,6 +1044,17 @@ const showField = (fieldName) => {
return componentType === 'repeating-group' || componentType === 'dynamic-list';
case 'defaultItems':
return componentType === 'dynamic-list';
case 'itemValidation':
case 'allowDuplicates':
case 'enableSorting':
case 'enableSearch':
case 'itemType':
case 'showItemCounter':
case 'confirmDelete':
case 'bulkOperations':
case 'exportFormat':
case 'importEnabled':
return componentType === 'dynamic-list';
case 'fields':
return componentType === 'info-display' || componentType === 'repeating-group';
case 'layout':
@ -1024,7 +1160,7 @@ const fieldsToShow = {
button: ['label', 'name', 'help', 'buttonType', 'variant', 'size', 'disabled', 'onClick', 'width'],
'image-preview': ['label', 'name', 'help', 'imageUrl', 'altText', 'caption', 'showZoom', 'showCaption', 'maxWidth', 'height', 'width'],
'repeating-group': ['label', 'name', 'help', 'minItems', 'maxItems', 'buttonText', 'removeText', 'fields', 'width'],
'dynamic-list': ['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'width'],
'dynamic-list': ['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'itemValidation', 'allowDuplicates', 'enableSorting', 'enableSearch', 'itemType', 'showItemCounter', 'confirmDelete', 'bulkOperations', 'exportFormat', 'importEnabled', 'width'],
// Layout elements
heading: ['value', 'level', 'width'],

View File

@ -305,14 +305,14 @@
placeholder="5"
/>
<div class="field-wrapper">
<label class="block text-sm font-medium text-gray-700 mb-2">Allow Multiple Files</label>
<label class="toggle-switch">
<input type="checkbox" v-model="configModel.multiple" />
<span class="toggle-slider"></span>
<span class="toggle-label">{{ configModel.multiple ? 'Yes' : 'No' }}</span>
</label>
</div>
<FormKit
type="switch"
label="Allow Multiple Files"
name="multiple"
v-model="configModel.multiple"
help="Enable uploading multiple files at once"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</template>
@ -417,6 +417,197 @@
/>
</template>
<!-- Dynamic List Configuration -->
<template v-if="component.type === 'dynamic-list'">
<div class="space-y-6">
<!-- Basic List Settings -->
<div class="grid grid-cols-2 gap-4">
<FormKit
type="text"
label="Add Button Text"
name="buttonText"
v-model="configModel.buttonText"
help="Text for the add item button"
:classes="{ outer: 'field-wrapper' }"
placeholder="Add Item"
/>
<FormKit
type="select"
label="Item Type"
name="itemType"
v-model="configModel.itemType"
:options="[
{ label: 'Text', value: 'text' },
{ label: 'Number', value: 'number' },
{ label: 'Email', value: 'email' },
{ label: 'URL', value: 'url' }
]"
help="Type of data for list items"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
<!-- Item Limits -->
<div class="grid grid-cols-2 gap-4">
<FormKit
type="number"
label="Minimum Items"
name="minItems"
v-model="configModel.minItems"
help="Minimum number of items required"
:classes="{ outer: 'field-wrapper' }"
:min="0"
placeholder="0"
/>
<FormKit
type="number"
label="Maximum Items"
name="maxItems"
v-model="configModel.maxItems"
help="Maximum number of items allowed"
:classes="{ outer: 'field-wrapper' }"
:min="1"
placeholder="10"
/>
</div>
<!-- Item Validation -->
<FormKit
type="text"
label="Item Validation Rules"
name="itemValidation"
v-model="configModel.itemValidation"
help="Validation rules for individual items (e.g., 'required|min:3|max:50')"
:classes="{ outer: 'field-wrapper' }"
placeholder="required|min:3"
/>
<!-- Behavior Settings -->
<div class="space-y-4">
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Behavior Settings</h5>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="switch"
label="Allow Duplicates"
name="allowDuplicates"
v-model="configModel.allowDuplicates"
help="Allow duplicate items in the list"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="switch"
label="Enable Sorting"
name="enableSorting"
v-model="configModel.enableSorting"
help="Allow drag & drop reordering"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="switch"
label="Enable Search"
name="enableSearch"
v-model="configModel.enableSearch"
help="Add search/filter functionality"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="switch"
label="Show Item Counter"
name="showItemCounter"
v-model="configModel.showItemCounter"
help="Display current item count"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="switch"
label="Confirm Delete"
name="confirmDelete"
v-model="configModel.confirmDelete"
help="Require confirmation before deleting"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="switch"
label="Bulk Operations"
name="bulkOperations"
v-model="configModel.bulkOperations"
help="Enable bulk select/delete operations"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</div>
<!-- Import/Export Settings -->
<div class="space-y-4">
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Import/Export Settings</h5>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="switch"
label="Enable Import"
name="importEnabled"
v-model="configModel.importEnabled"
help="Allow importing items from files"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="select"
label="Export Format"
name="exportFormat"
v-model="configModel.exportFormat"
:options="[
{ label: 'JSON', value: 'json' },
{ label: 'CSV', value: 'csv' },
{ label: 'Text', value: 'txt' }
]"
help="Default format for exporting items"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</div>
<!-- Default Items -->
<div class="space-y-4">
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Default Items</h5>
<div class="border rounded-md p-3 bg-gray-50 space-y-2">
<div v-for="(item, index) in (configModel.defaultItems || [])" :key="index" class="flex items-center">
<input
type="text"
v-model="configModel.defaultItems[index]"
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
placeholder="Enter default item"
/>
<button
@click="removeDefaultItem(index)"
class="ml-2 text-red-500 hover:text-red-700"
type="button"
>
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<button
@click="addDefaultItem"
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
type="button"
>
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
Add Default Item
</button>
</div>
</div>
</div>
</template>
<!-- Content for Text Components -->
<template v-if="showField('value')">
<FormKit
@ -516,7 +707,7 @@
<div class="validation-options">
<!-- Required Field -->
<div class="validation-item">
<label class="validation-toggle">
<label class="validation-toggle" @click.stop>
<input type="checkbox" v-model="isRequired" />
<span class="toggle-slider"></span>
<div class="validation-info">
@ -528,7 +719,7 @@
<!-- Email Validation -->
<div v-if="component.type === 'email' || component.type === 'text'" class="validation-item">
<label class="validation-toggle">
<label class="validation-toggle" @click.stop>
<input type="checkbox" v-model="isEmailValidation" />
<span class="toggle-slider"></span>
<div class="validation-info">
@ -540,7 +731,7 @@
<!-- URL Validation -->
<div v-if="component.type === 'url' || component.type === 'text'" class="validation-item">
<label class="validation-toggle">
<label class="validation-toggle" @click.stop>
<input type="checkbox" v-model="isUrlValidation" />
<span class="toggle-slider"></span>
<div class="validation-info">
@ -552,7 +743,7 @@
<!-- Length Validation -->
<div v-if="isTextBasedField" class="validation-item">
<label class="validation-toggle">
<label class="validation-toggle" @click.stop>
<input type="checkbox" v-model="hasLengthValidation" />
<span class="toggle-slider"></span>
<div class="validation-info">
@ -583,7 +774,7 @@
<!-- Number Range Validation -->
<div v-if="component.type === 'number'" class="validation-item">
<label class="validation-toggle">
<label class="validation-toggle" @click.stop>
<input type="checkbox" v-model="hasNumberValidation" />
<span class="toggle-slider"></span>
<div class="validation-info">
@ -618,10 +809,286 @@
label="Custom Validation Rules"
name="validation"
v-model="configModel.validation"
help="Advanced: Enter custom validation rules (e.g., 'required|email|length:5,50')"
help="Advanced: Enter custom validation rules using the format below"
:classes="{ outer: 'field-wrapper' }"
placeholder="Leave empty for basic validation"
placeholder="e.g., required|email|length:3,50"
/>
<!-- Validation Rules Guide -->
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<h5 class="text-sm font-medium text-blue-800 mb-3 flex items-center">
<Icon name="heroicons:information-circle" class="w-4 h-4 mr-2" />
Validation Rules Guide
</h5>
<div class="space-y-4 text-sm">
<!-- Basic Rules -->
<div>
<h6 class="font-medium text-blue-700 mb-2">Basic Rules</h6>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="validation-rule">
<code class="rule-code">required</code>
<span class="rule-desc">Field must be filled</span>
</div>
<div class="validation-rule">
<code class="rule-code">email</code>
<span class="rule-desc">Must be valid email</span>
</div>
<div class="validation-rule">
<code class="rule-code">url</code>
<span class="rule-desc">Must be valid URL</span>
</div>
<div class="validation-rule">
<code class="rule-code">number</code>
<span class="rule-desc">Must be numeric</span>
</div>
</div>
</div>
<!-- Length Rules -->
<div>
<h6 class="font-medium text-blue-700 mb-2">Length Rules</h6>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="validation-rule">
<code class="rule-code">length:5</code>
<span class="rule-desc">Exactly 5 characters</span>
</div>
<div class="validation-rule">
<code class="rule-code">length:3,20</code>
<span class="rule-desc">Between 3-20 characters</span>
</div>
<div class="validation-rule">
<code class="rule-code">min:3</code>
<span class="rule-desc">Minimum 3 characters</span>
</div>
<div class="validation-rule">
<code class="rule-code">max:50</code>
<span class="rule-desc">Maximum 50 characters</span>
</div>
</div>
</div>
<!-- Number Rules -->
<div>
<h6 class="font-medium text-blue-700 mb-2">Number Rules</h6>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="validation-rule">
<code class="rule-code">between:1,100</code>
<span class="rule-desc">Between 1 and 100</span>
</div>
<div class="validation-rule">
<code class="rule-code">min_value:0</code>
<span class="rule-desc">Minimum value 0</span>
</div>
<div class="validation-rule">
<code class="rule-code">max_value:999</code>
<span class="rule-desc">Maximum value 999</span>
</div>
<div class="validation-rule">
<code class="rule-code">integer</code>
<span class="rule-desc">Must be whole number</span>
</div>
</div>
</div>
<!-- Pattern Rules -->
<div>
<h6 class="font-medium text-blue-700 mb-2">Pattern Rules</h6>
<div class="grid grid-cols-1 gap-2 text-xs">
<div class="validation-rule">
<code class="rule-code">alpha</code>
<span class="rule-desc">Only letters (A-Z, a-z)</span>
</div>
<div class="validation-rule">
<code class="rule-code">alpha_numeric</code>
<span class="rule-desc">Letters and numbers only</span>
</div>
<div class="validation-rule">
<code class="rule-code">alpha_spaces</code>
<span class="rule-desc">Letters and spaces only</span>
</div>
</div>
</div>
</div>
<!-- Examples Section -->
<div class="mt-4 pt-4 border-t border-blue-200">
<h6 class="font-medium text-blue-700 mb-2">Examples</h6>
<div class="space-y-2 text-xs">
<div class="example-item">
<code class="example-code">required|email</code>
<span class="example-desc">Required email field</span>
</div>
<div class="example-item">
<code class="example-code">required|length:3,50</code>
<span class="example-desc">Required text, 3-50 characters</span>
</div>
<div class="example-item">
<code class="example-code">number|between:1,100</code>
<span class="example-desc">Number between 1-100</span>
</div>
<div class="example-item">
<code class="example-code">required|alpha_numeric|min:5</code>
<span class="example-desc">Required alphanumeric, min 5 chars</span>
</div>
</div>
</div>
<!-- Quick Tips -->
<div class="mt-3 pt-3 border-t border-blue-200">
<p class="text-xs text-blue-600">
<strong>💡 Tips:</strong> Separate rules with | (pipe) Order doesn't matter Leave empty for no validation
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Conditional Logic Section -->
<div v-if="showField('conditionalLogic')" class="section-container mt-6">
<div class="section">
<div class="section-wrapper">
<div class="section-header">
<h4 class="section-title">
<Icon name="heroicons:adjustments-horizontal" class="w-5 h-5 mr-2" />
Conditional Logic
</h4>
<p class="section-description">Show, hide, or disable this field based on other field values</p>
</div>
<div class="section-content">
<!-- Enable Conditional Logic -->
<FormKit
type="switch"
label="Enable Conditional Logic"
name="conditionalLogicEnabled"
v-model="configModel.conditionalLogic.enabled"
help="Show or hide this field based on other fields"
:classes="{ outer: 'field-wrapper' }"
/>
<!-- Conditional Logic Configuration -->
<div v-if="configModel.conditionalLogic.enabled" class="mt-4 space-y-4">
<!-- Action Selection -->
<FormKit
type="select"
label="Action"
name="conditionalAction"
v-model="configModel.conditionalLogic.action"
:options="[
{ label: 'Show this field', value: 'show' },
{ label: 'Hide this field', value: 'hide' },
{ label: 'Enable this field', value: 'enable' },
{ label: 'Disable this field', value: 'disable' }
]"
help="What should happen when conditions are met"
:classes="{ outer: 'field-wrapper' }"
/>
<!-- Operator Selection (when multiple conditions) -->
<FormKit
v-if="configModel.conditionalLogic.conditions.length > 1"
type="select"
label="Logic Operator"
name="conditionalOperator"
v-model="configModel.conditionalLogic.operator"
:options="[
{ label: 'All conditions must be true (AND)', value: 'and' },
{ label: 'Any condition can be true (OR)', value: 'or' }
]"
help="How multiple conditions should be evaluated"
:classes="{ outer: 'field-wrapper' }"
/>
<!-- Conditions -->
<div class="conditions-section">
<h5 class="text-sm font-medium text-gray-700 mb-3">Conditions</h5>
<div v-if="configModel.conditionalLogic.conditions.length === 0" class="text-center py-4 text-gray-500 border-2 border-dashed border-gray-300 rounded-lg">
<Icon name="heroicons:plus-circle" class="w-8 h-8 mx-auto mb-2 text-gray-400" />
<p class="text-sm">No conditions set. Add a condition to get started.</p>
</div>
<div v-else class="space-y-3">
<div
v-for="(condition, index) in configModel.conditionalLogic.conditions"
:key="index"
class="condition-item bg-gray-50 p-4 rounded-lg border"
>
<div class="grid grid-cols-3 gap-3">
<FormKit
type="text"
label="Field Name"
:name="`condition_field_${index}`"
v-model="condition.field"
placeholder="field_name"
help="Name of the field to check"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="select"
label="Operator"
:name="`condition_operator_${index}`"
v-model="condition.operator"
:options="[
{ label: 'Equals', value: 'equals' },
{ label: 'Not equals', value: 'not_equals' },
{ label: 'Contains', value: 'contains' },
{ label: 'Does not contain', value: 'not_contains' },
{ label: 'Is empty', value: 'is_empty' },
{ label: 'Is not empty', value: 'is_not_empty' },
{ label: 'Greater than', value: 'greater_than' },
{ label: 'Less than', value: 'less_than' }
]"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="text"
label="Value"
:name="`condition_value_${index}`"
v-model="condition.value"
placeholder="Value to compare"
help="Leave empty for 'is empty' conditions"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
<div class="flex justify-end mt-3">
<button
@click="removeCondition(index)"
type="button"
class="text-red-600 hover:text-red-800 text-sm flex items-center"
>
<Icon name="heroicons:trash" class="w-4 h-4 mr-1" />
Remove Condition
</button>
</div>
</div>
</div>
<button
@click="addCondition"
type="button"
class="mt-3 inline-flex items-center px-3 py-2 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
>
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
Add Condition
</button>
</div>
<!-- Preview JavaScript Code -->
<div v-if="configModel.conditionalLogic.conditions.length > 0" class="mt-4">
<h5 class="text-sm font-medium text-gray-700 mb-2">Generated Code Preview</h5>
<div class="bg-gray-900 text-green-400 p-3 rounded text-xs font-mono">
{{ generateConditionalLogicCode() }}
</div>
<p class="text-xs text-gray-500 mt-1">This JavaScript will be automatically executed to handle the conditional logic</p>
</div>
</div>
</div>
</div>
@ -713,7 +1180,8 @@ const getComponentIcon = (type) => {
heading: 'heroicons:h1',
paragraph: 'heroicons:document-text',
divider: 'heroicons:minus',
'info-display': 'heroicons:information-circle'
'info-display': 'heroicons:information-circle',
'dynamic-list': 'heroicons:list-bullet'
}
return icons[type] || 'heroicons:square-3-stack-3d'
}
@ -745,7 +1213,8 @@ const getComponentTypeName = (type) => {
heading: 'Heading Text',
paragraph: 'Paragraph Text',
divider: 'Divider Line',
'info-display': 'Information Display'
'info-display': 'Information Display',
'dynamic-list': 'Dynamic List'
}
return names[type] || 'Form Field'
}
@ -777,7 +1246,8 @@ const getComponentDescription = (type) => {
heading: 'Section heading to organize form content',
paragraph: 'Text content for instructions and descriptions',
divider: 'Visual separator to organize form sections',
'info-display': 'Read-only information display in organized format'
'info-display': 'Read-only information display in organized format',
'dynamic-list': 'Dynamic list for displaying and managing items'
}
return descriptions[type] || 'Configure this form field'
}
@ -802,13 +1272,14 @@ const showField = (fieldName) => {
if (!props.component) return false
const fieldConfig = {
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select'],
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'dynamic-list'],
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'dynamic-list'],
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'dynamic-list'],
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'dynamic-list'],
value: ['heading', 'paragraph', 'hidden'],
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'info-display'],
options: ['select', 'checkbox', 'radio']
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'info-display', 'dynamic-list'],
options: ['select', 'checkbox', 'radio'],
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'dynamic-list']
}
return fieldConfig[fieldName]?.includes(props.component.type) || false
@ -817,7 +1288,7 @@ const showField = (fieldName) => {
const hasOptions = computed(() => showField('options'))
const hasSpecificSettings = computed(() => {
if (!props.component) return false
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'text', 'textarea', 'paragraph', 'button']
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'paragraph', 'button', 'dynamic-list']
return specificTypes.includes(props.component.type)
})
@ -936,7 +1407,8 @@ const getRecommendedWidth = (fieldType) => {
'range': 'full',
'switch': 'full',
'button': 'full',
'info-display': 'full'
'info-display': 'full',
'dynamic-list': 'full'
}
return recommendations[fieldType] || 'full'
@ -1011,6 +1483,108 @@ const removeOption = (index) => {
configModel.value.options.splice(index, 1)
}
// Default items management for dynamic-list
const addDefaultItem = () => {
if (!configModel.value.defaultItems) {
configModel.value.defaultItems = []
}
configModel.value.defaultItems.push('')
}
const removeDefaultItem = (index) => {
if (configModel.value.defaultItems) {
configModel.value.defaultItems.splice(index, 1)
}
}
// Conditional logic management
const addCondition = () => {
if (!configModel.value.conditionalLogic) {
configModel.value.conditionalLogic = {
enabled: true,
conditions: [],
action: 'show',
operator: 'and'
}
}
if (!configModel.value.conditionalLogic.conditions) {
configModel.value.conditionalLogic.conditions = []
}
configModel.value.conditionalLogic.conditions.push({
field: '',
operator: 'equals',
value: ''
})
}
const removeCondition = (index) => {
if (configModel.value.conditionalLogic?.conditions) {
configModel.value.conditionalLogic.conditions.splice(index, 1)
}
}
const generateConditionalLogicCode = () => {
if (!configModel.value.conditionalLogic || !configModel.value.conditionalLogic.conditions.length) {
return '// No conditions defined'
}
const { conditions, action, operator } = configModel.value.conditionalLogic
const fieldName = configModel.value.name || 'this_field'
// Generate condition checks
const conditionChecks = conditions.map(condition => {
const { field, operator: condOp, value } = condition
switch (condOp) {
case 'equals':
return `getField('${field}') === '${value}'`
case 'not_equals':
return `getField('${field}') !== '${value}'`
case 'contains':
return `String(getField('${field}')).includes('${value}')`
case 'not_contains':
return `!String(getField('${field}')).includes('${value}')`
case 'is_empty':
return `!getField('${field}') || getField('${field}') === ''`
case 'is_not_empty':
return `getField('${field}') && getField('${field}') !== ''`
case 'greater_than':
return `Number(getField('${field}')) > ${Number(value) || 0}`
case 'less_than':
return `Number(getField('${field}')) < ${Number(value) || 0}`
default:
return `getField('${field}') === '${value}'`
}
}).join(` ${operator} `)
// Generate action
const actionCode = action === 'show' ? `showField('${fieldName}')` :
action === 'hide' ? `hideField('${fieldName}')` :
action === 'enable' ? `enableField('${fieldName}')` :
action === 'disable' ? `disableField('${fieldName}')` :
`showField('${fieldName}')`
// Generate field change listeners
const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))]
const listenerCode = watchedFields.map(field =>
`onFieldChange('${field}', function() {
if (${conditionChecks}) {
${actionCode};
} else {
${action === 'show' ? `hideField('${fieldName}')` :
action === 'hide' ? `showField('${fieldName}')` :
action === 'enable' ? `disableField('${fieldName}')` :
action === 'disable' ? `enableField('${fieldName}')` :
`hideField('${fieldName}')`};
}
});`
).join('\n\n')
return listenerCode || '// Invalid configuration'
}
const handleSave = () => {
emit('update-component', { ...props.component, props: configModel.value })
handleClose()
@ -1316,4 +1890,29 @@ const handleClose = () => {
.example-button {
@apply text-left p-2 text-xs bg-white border border-gray-200 rounded hover:border-blue-300 hover:bg-blue-50 transition-colors;
}
/* Validation Guide Styles */
.validation-rule {
@apply flex flex-col space-y-1 p-2 bg-white rounded border border-blue-100;
}
.rule-code {
@apply font-mono text-xs font-semibold text-blue-800 bg-blue-100 px-2 py-1 rounded;
}
.rule-desc {
@apply text-gray-600;
}
.example-item {
@apply flex flex-col space-y-1 p-2 bg-white rounded border border-blue-100;
}
.example-code {
@apply font-mono text-xs font-semibold text-green-800 bg-green-100 px-2 py-1 rounded;
}
.example-desc {
@apply text-gray-600;
}
</style>

View File

@ -9,33 +9,86 @@ function handleChange(event) {
</script>
<template>
<div class="flex items-center justify-start">
<div class="relative inline-block w-11 h-6">
<div class="switch-container">
<div class="switch-wrapper">
<input
:id="context.id"
:name="context.name"
type="checkbox"
:checked="context._value"
:checked="context.value"
:disabled="context.disabled"
@change="handleChange"
class="sr-only peer"
class="switch-input"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<label :for="context.id" class="switch-label">
<span class="switch-track">
<span class="switch-thumb"></span>
</span>
</label>
</div>
</div>
</template>
<style scoped>
/* Additional styles for better visual feedback */
.peer:checked + div {
background-color: #3b82f6;
.switch-container {
@apply flex items-center justify-start;
}
.peer:disabled + div {
opacity: 0.5;
cursor: not-allowed;
.switch-wrapper {
@apply relative;
}
.peer:focus + div {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
.switch-input {
@apply sr-only;
}
.switch-label {
@apply cursor-pointer;
}
.switch-track {
@apply relative inline-block w-11 h-6 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out;
}
.switch-input:checked + .switch-label .switch-track {
@apply bg-blue-600;
}
.switch-input:disabled + .switch-label .switch-track {
@apply opacity-50 cursor-not-allowed;
}
.switch-input:disabled + .switch-label {
@apply cursor-not-allowed;
}
.switch-thumb {
@apply absolute top-[2px] left-[2px] w-5 h-5 bg-white border border-gray-300 rounded-full transition-transform duration-200 ease-in-out;
}
.switch-input:checked + .switch-label .switch-thumb {
@apply transform translate-x-5 border-white;
}
.switch-input:focus + .switch-label .switch-track {
@apply ring-4 ring-blue-300 ring-opacity-50;
}
/* Mac-specific fixes */
@media (prefers-reduced-motion: reduce) {
.switch-track,
.switch-thumb {
transition: none;
}
}
/* Force hardware acceleration on Mac for smoother animations */
.switch-thumb {
transform: translateZ(0);
will-change: transform;
}
.switch-input:checked + .switch-label .switch-thumb {
transform: translateX(1.25rem) translateZ(0);
}
</style>

View File

@ -21,6 +21,7 @@ The documentation is organized into the following sections:
- [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
- [Roadmap](./form-builder/ROADMAP.md) - Development roadmap and missing essential features checklist
### Process Execution
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface

358
doc/form-builder/ROADMAP.md Normal file
View File

@ -0,0 +1,358 @@
# Form Builder Development Roadmap
This document outlines the planned improvements and missing essential features for the Corrad ProcessMaker Form Builder. It serves as a development checklist to track progress on enhancing form building capabilities.
## Current Status
**Last Updated**: December 2024
**Current Version**: v2.0 with JavaScript API and dynamic calculations
### Recently Completed Features ✅
- JavaScript Execution Engine (`FormScriptEngine`) for real-time calculations
- Dynamic field updates with `onFieldChange` and `onLoad` event handlers
- Enhanced debugging with comprehensive logging
- API functions: `setField()`, `getField()` for programmatic form control
- Grid system for responsive layouts
- Form versioning and management
- Security improvements with sandboxed JavaScript execution
---
## Missing Essential Settings Analysis
### 🔴 Critical Priority (Must Have)
#### **1. Conditional Logic System**
**Status**: ✅ Completed December 2024
**Components Affected**: All form components
**Description**: Essential for dynamic forms that show/hide fields based on user input
**Implemented Features**:
- ✅ Field visibility rules (show/hide based on other field values)
- ✅ Field enabling/disabling based on conditions
- ✅ Multi-field conditional logic (AND/OR operators)
- ✅ Visual condition builder interface
- ✅ Field dependency chains
- ✅ JavaScript code generation and execution
- ✅ Integration with existing FormScriptEngine
- ✅ Real-time condition evaluation
**Impact**: High - Critical for advanced form workflows
**Implementation Details**:
- Added `conditionalLogic` properties to all form components
- Created visual condition builder in FormBuilderFieldSettingsModal
- Built ConditionalLogicEngine to generate and execute JavaScript
- Integrated with existing FormScriptEngine for seamless execution
- Supports multiple operators: equals, not_equals, contains, not_contains, is_empty, is_not_empty, greater_than, less_than
- Supports multiple actions: show, hide, enable, disable fields
- Real-time preview of generated JavaScript code
- Automatic script generation and injection
**Files Modified**:
- `components/FormBuilderComponents.vue` - Added conditional logic properties
- `components/FormBuilderFieldSettingsModal.vue` - Added condition builder interface
- `components/ConditionalLogicEngine.vue` - New component for script generation
- `pages/form-builder/index.vue` - Integrated conditional logic engine
---
#### **2. Enhanced Validation System**
**Status**: 🟡 Partially Implemented
**Current**: Basic required, email, URL, min/max validations
**Components Affected**: All input components
**Missing Features**:
- [ ] Pattern matching validation (regex support)
- [ ] Custom validation functions
- [ ] Real-time validation feedback
- [ ] Cross-field validation (confirm password, date ranges)
- [ ] Conditional validation rules
- [ ] Custom error message templates
- [ ] Validation groups and schemas
**Impact**: High - Essential for data quality and user experience
---
#### **3. Accessibility Features**
**Status**: ❌ Not Implemented
**Components Affected**: All components
**Description**: WCAG compliance and screen reader support
**Missing Features**:
- [ ] ARIA labels and descriptions
- [ ] Keyboard navigation support
- [ ] Screen reader announcements
- [ ] High contrast mode support
- [ ] Focus management and indicators
- [ ] Alternative text for visual elements
- [ ] Accessibility validation tool
**Impact**: High - Required for compliance and inclusivity
---
### 🟡 High Priority (Should Have)
#### **4. Dynamic List Component Enhancements**
**Status**: ✅ Completed December 2024
**Current Settings**: `['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'width']`
**Implemented Essential Settings**:
- [x] Item validation (validate individual list items)
- [x] Uniqueness validation (prevent duplicate items)
- [x] Item type support (text, number, email, url)
- [x] Search/filter within list items
- [x] Bulk operations (select all, delete selected)
- [x] Import/Export functionality (JSON, CSV, TXT)
- [x] Item counter display
- [x] Delete confirmation
- [x] Visual sorting indicators (drag handles)
- [x] Enhanced UI with validation feedback
**Missing Features** (moved to future roadmap):
- [ ] Drag & drop reordering functionality (requires vue-draggable)
- [ ] Custom item templates (rich formatting)
**Impact**: Medium-High - Important for data collection workflows
---
#### **5. Enhanced Select Component**
**Status**: 🟡 Partially Implemented
**Current Settings**: `['label', 'name', 'placeholder', 'help', 'options', 'width']`
**Missing Essential Settings**:
- [ ] Searchable/filterable dropdown
- [ ] Multi-select with chips display
- [ ] Option groups/categories (optgroups)
- [ ] Dynamic options loading from API endpoints
- [ ] Custom option templates (with icons, descriptions)
- [ ] Maximum selections limit for multi-select
- [ ] Async option loading with loading states
**Impact**: Medium-High - Common requirement for complex forms
---
#### **6. Rich Text and Enhanced Text Components**
**Status**: ❌ Not Implemented
**Components Affected**: textarea, text components
**Missing Features**:
- [ ] WYSIWYG rich text editor integration
- [ ] Character counter display
- [ ] Auto-resize for textarea
- [ ] Auto-complete/suggestions
- [ ] Advanced input masks for specific formats
- [ ] Markdown support and preview
- [ ] Spell check integration
**Impact**: Medium-High - Needed for content-heavy forms
---
### 🟢 Medium Priority (Could Have)
#### **7. Enhanced File Upload Components**
**Status**: 🟡 Partially Implemented
**Current**: Basic file and dropzone with size/type restrictions
**Missing Features**:
- [ ] Progress indicators for upload status
- [ ] File preview thumbnails (images, PDFs)
- [ ] Drag reordering for multiple files
- [ ] File description/tagging capabilities
- [ ] Cloud storage integration (S3, Google Drive)
- [ ] Virus scanning configuration
- [ ] Image editing capabilities (crop, resize)
**Impact**: Medium - Enhances file handling workflows
---
#### **8. Advanced Number Component**
**Status**: 🟡 Partially Implemented
**Current**: Basic min/max/step support
**Missing Features**:
- [ ] Currency formatting (with symbol, locale)
- [ ] Percentage mode
- [ ] Thousand separators configuration
- [ ] Decimal precision control
- [ ] Scientific notation support
- [ ] Number formatting templates
- [ ] Unit of measurement support
**Impact**: Medium - Important for financial and scientific forms
---
#### **9. Enhanced Date/Time Components**
**Status**: 🟡 Partially Implemented
**Current**: Basic date, time, datetime-local
**Missing Features**:
- [ ] Date range restrictions (min/max dates)
- [ ] Disabled dates (weekends, holidays)
- [ ] Custom date format display
- [ ] Timezone selection and conversion
- [ ] Relative date options (today, tomorrow, etc.)
- [ ] Date picker themes and localization
- [ ] Recurring date patterns
**Impact**: Medium - Useful for scheduling and booking forms
---
### 🔵 Low Priority (Nice to Have)
#### **10. Advanced Layout and Styling**
**Status**: ❌ Not Implemented
**Missing Features**:
- [ ] Custom CSS classes for advanced styling
- [ ] Component themes and appearance presets
- [ ] Advanced grid layout controls
- [ ] Responsive breakpoint settings
- [ ] Animation and transition effects
- [ ] Dark mode support
- [ ] Print-friendly layouts
**Impact**: Low - Aesthetic and branding enhancements
---
#### **11. Integration and API Features**
**Status**: ❌ Not Implemented
**Missing Features**:
- [ ] Webhook integrations for form submissions
- [ ] External API data binding
- [ ] Real-time collaboration features
- [ ] Form analytics and usage tracking
- [ ] A/B testing capabilities
- [ ] Multi-language support
- [ ] Form embedding options
**Impact**: Low - Advanced enterprise features
---
## Development Timeline
### Phase 1: Foundation (Q1 2025)
- [ ] Implement conditional logic system
- [ ] Enhance validation with pattern matching
- [ ] Add basic accessibility features
- [ ] Improve dynamic list component
### Phase 2: Enhancement (Q2 2025)
- [ ] Enhanced select component with search
- [ ] Rich text editor integration
- [ ] Advanced file upload features
- [ ] Number formatting enhancements
### Phase 3: Polish (Q3 2025)
- [ ] Date/time component improvements
- [ ] Advanced layout controls
- [ ] Performance optimizations
- [ ] Comprehensive testing
### Phase 4: Integration (Q4 2025)
- [ ] API integration features
- [ ] Analytics and reporting
- [ ] Multi-language support
- [ ] Advanced enterprise features
---
## Implementation Notes
### Technical Considerations
- Ensure backward compatibility with existing forms
- Maintain performance with large forms
- Follow Vue 3 composition API patterns
- Implement proper TypeScript definitions
- Add comprehensive unit and integration tests
### Design Principles
- Keep the interface intuitive and user-friendly
- Maintain consistency with existing UI patterns
- Ensure mobile responsiveness
- Follow accessibility best practices
- Provide clear documentation and examples
---
## Progress Tracking
Use the following format to track completed features:
```markdown
### ✅ [Feature Name] - Completed [Date]
**Implemented by**: [Developer Name]
**PR/Commit**: [Link to implementation]
**Notes**: [Any important implementation details]
```
### ✅ JavaScript API Integration - Completed December 2024
**Implemented by**: Development Team
**Notes**: Added FormScriptEngine with real-time calculations and field manipulation
### ✅ Dynamic List Component Enhancements - Completed December 2024
**Implemented by**: AI Assistant
**Components Modified**:
- `components/FormBuilderComponents.vue` - Enhanced component definition with new properties
- `components/FormBuilderConfiguration.vue` - Added comprehensive configuration interface
- `components/ComponentPreview.vue` - Implemented advanced functionality
**Features Added**:
- Item validation with custom rules (required, min/max length, email, URL)
- Uniqueness validation to prevent duplicate items
- Multiple item types support (text, number, email, URL)
- Search/filter functionality within list items
- Bulk operations (select all, delete selected items)
- Import/Export capabilities (JSON, CSV, TXT formats)
- Item counter display showing current/max items
- Delete confirmation dialog
- Enhanced UI with validation feedback and error messages
- Visual sorting indicators (drag handles for future drag & drop)
**Notes**: Significantly enhanced the dynamic list component from basic functionality to a feature-rich, professional-grade list management system
### ✅ Conditional Logic System - Completed December 2024
**Implemented by**: AI Assistant
**Components Modified**:
- `components/FormBuilderComponents.vue` - Added conditional logic properties to component definitions
- `components/FormBuilderFieldSettingsModal.vue` - Created visual condition builder interface
- `components/ConditionalLogicEngine.vue` - New component for JavaScript generation and execution
- `pages/form-builder/index.vue` - Integrated conditional logic engine with form builder
**Features Added**:
- Visual condition builder with drag-and-drop interface for setting up field dependencies
- Support for multiple condition operators (equals, not_equals, contains, is_empty, etc.)
- Multiple action types (show, hide, enable, disable fields)
- AND/OR logic operators for complex condition chains
- Real-time JavaScript code generation and preview
- Integration with existing FormScriptEngine for seamless execution
- Field dependency validation and error handling
- Auto-generated event listeners for field changes
- Initial condition evaluation on form load
**Impact**: Enables dynamic forms with sophisticated business logic and user experience flows
---
## Contributing
When implementing features from this roadmap:
1. Update the status from ❌ to 🟡 (In Progress) when starting
2. Move to ✅ when completed with implementation details
3. Update the main README.md if new documentation is added
4. Ensure all new features have corresponding tests
5. Update the user guide with new functionality
---
**Document Version**: 1.0
**Next Review**: March 2025

View File

@ -206,6 +206,13 @@
@field-validate="handleScriptFieldValidate"
/>
<ConditionalLogicEngine
ref="conditionalLogicEngine"
:form-components="formStore.formComponents"
:form-data="previewFormData"
@script-generated="handleConditionalLogicGenerated"
/>
<FormKit
ref="previewForm"
type="form"
@ -863,6 +870,8 @@ import { nextTick } from 'vue';
import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
import FormScriptEngine from '~/components/FormScriptEngine.vue';
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
definePageMeta({
title: "Form Builder",
@ -904,6 +913,7 @@ const showFieldSettings = ref(false);
const showFieldSettingsPanel = ref(false);
const previewForm = ref(null);
const formScriptEngine = ref(null);
const conditionalLogicEngine = ref(null);
// Settings tabs configuration
const settingsTabs = [
@ -2427,6 +2437,12 @@ const deleteComponent = () => {
toast.success('Component deleted successfully');
}
};
const handleConditionalLogicGenerated = (script) => {
// Add the generated script to the form's custom script
formStore.formCustomScript += `\n// Conditional Logic Script\n${script}`;
toast.success('Conditional logic script added successfully');
};
</script>
<style scoped>