Enhance Component Preview and Form Builder Functionality

- Updated ComponentPreview.vue to improve handling of readonly states for select, checkbox, and radio components, ensuring proper styling and interaction.
- Modified button component in ComponentPreview.vue to conditionally display button text and icon based on new props for better customization.
- Enhanced FormBuilderComponents.vue by adding new properties for button configuration, including showLabel, showButtonText, buttonText, and icon.
- Introduced new form field settings in FormBuilderFieldSettingsModal.vue to allow users to customize button size, icon, and visibility options for labels and text.
- Improved overall user experience by refining placeholder visibility in builder mode and enhancing the button action script template functionality.
This commit is contained in:
Afiq 2025-08-04 15:37:07 +08:00
parent bae98c2b17
commit 707d8fe3b0
3 changed files with 244 additions and 29 deletions

View File

@ -27,14 +27,20 @@
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
:readonly="component.props.readonly || isPreview"
:disabled="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
:disabled="isPreview || (component.props.readonly && ['select', 'checkbox', 'radio'].includes(component.type))"
: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,
'readonly-select': component.props.readonly && component.type === 'select',
'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
'readonly-radio': component.props.readonly && component.type === 'radio'
}" />
<!-- Heading -->
<div v-else-if="component.type === 'heading'" class="py-2">
@ -325,14 +331,15 @@
<!-- Button Component -->
<div v-else-if="component.type === 'button'" class="py-2">
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-2">
<label v-if="component.props.label && component.props.showLabel !== false" class="block text-sm font-medium text-gray-700 mb-2">
{{ 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">
{{ component.props.label || 'Button' }}
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
<Icon v-if="component.props.icon" :name="component.props.icon" class="w-4 h-4" :class="{'ml-2': component.props.showButtonText !== false}" />
</RsButton>
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
@ -394,7 +401,7 @@
'no-header': !component.props.showHeader
}"
>
<!-- In preview mode, show the nested components or placeholder -->
<!-- In preview mode, show only the nested components (no placeholder) -->
<div v-if="isPreview" class="section-fields">
<!-- Render nested components if they exist -->
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
@ -402,17 +409,7 @@
<component-preview :component="childComponent" :is-preview="true" />
</template>
</div>
<!-- Show placeholder if no nested components -->
<div v-else class="section-placeholder">
<div class="placeholder-content">
<Icon name="material-symbols:dashboard-customize-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
<p class="text-sm text-gray-500 text-center mb-2">Form Section Container</p>
<p class="text-xs text-gray-400 text-center">
This section will contain the form fields grouped here
</p>
</div>
</div>
<!-- No placeholder in preview mode when empty -->
</div>
<!-- In form builder mode, show drop zone -->
@ -477,14 +474,15 @@
class="section-drop-placeholder"
:class="{
'drop-active': sectionDropStates[component.id]?.isDraggingOver,
'empty': !component.props.children || component.props.children.length === 0
'empty': !component.props.children || component.props.children.length === 0,
'hidden-placeholder': component.props.showPlaceholder === false
}"
@dragover.prevent="handleSectionDragOver($event, component.id)"
@dragleave="handleSectionDragLeave($event, component.id)"
@drop="handleSectionDrop($event, component.id)"
@dragenter.prevent="handleSectionDragEnter($event, component.id)"
>
<div class="placeholder-content">
<div v-if="component.props.showPlaceholder !== false" class="placeholder-content">
<Icon name="material-symbols:add-box-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
<p class="text-sm text-gray-500 text-center mb-1">Drop Components Here</p>
<p class="text-xs text-gray-400 text-center">
@ -1271,6 +1269,32 @@ const saveNestedComponentSettings = (updatedComponent) => {
pointer-events: none;
}
/* Readonly styles for select, checkbox, and radio components */
:deep(.readonly-select),
:deep(.readonly-checkbox),
:deep(.readonly-radio) {
cursor: default !important;
}
:deep(.readonly-select select),
:deep(.readonly-select .formkit-inner),
:deep(.readonly-checkbox input[type="checkbox"]),
:deep(.readonly-radio input[type="radio"]) {
pointer-events: none !important;
opacity: 0.8 !important;
background-color: #f3f4f6 !important;
}
:deep(.readonly-checkbox) .formkit-options,
:deep(.readonly-radio) .formkit-options {
pointer-events: none !important;
}
:deep(.readonly-checkbox) .formkit-wrapper,
:deep(.readonly-radio) .formkit-wrapper {
cursor: not-allowed !important;
}
/* Image Preview Component */
.image-preview-container {
display: flex;
@ -1485,6 +1509,13 @@ const saveNestedComponentSettings = (updatedComponent) => {
background-color: #f1f5f9;
}
.section-drop-placeholder.hidden-placeholder {
border: none;
background-color: transparent;
padding: 0;
min-height: 40px;
}
/* Ghost styles for dragging */
.ghost {
opacity: 0.5;

View File

@ -598,6 +598,10 @@ const availableComponents = [
variant: 'primary', // primary, secondary, success, warning, danger
size: 'md', // sm, md, lg
disabled: false,
showLabel: true, // Whether to show the label above the button
showButtonText: true, // Whether to show text on the button
buttonText: '', // Text to display on button (falls back to label if empty)
icon: '', // Optional icon to display on button
onClick: '' // Custom JavaScript code to execute
}
},
@ -623,6 +627,7 @@ const availableComponents = [
spacing: 'normal', // compact, normal, relaxed
width: '100%',
gridColumn: 'span 12',
showPlaceholder: true, // Whether to show the placeholder in builder mode
children: [], // Array to hold nested components
// Conditional Logic Properties
conditionalLogic: {

View File

@ -497,19 +497,113 @@
/>
</div>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="select"
label="Button Size"
name="size"
v-model="configModel.size"
:options="[
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' }
]"
help="Size of the button"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="text"
label="Icon (Optional)"
name="icon"
v-model="configModel.icon"
help="Icon name (e.g., 'material-symbols:add')"
:classes="{ outer: 'field-wrapper' }"
placeholder="material-symbols:add"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="switch"
label="Show Label Above Button"
name="showLabel"
v-model="configModel.showLabel"
help="Display the label text above the button"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="switch"
label="Show Text on Button"
name="showButtonText"
v-model="configModel.showButtonText"
help="Display text on the button (icon-only if disabled)"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
<FormKit
type="select"
label="Button Size"
name="size"
v-model="configModel.size"
:options="[
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' }
]"
help="Size of the button"
v-if="configModel.showButtonText !== false"
type="text"
label="Button Text"
name="buttonText"
v-model="configModel.buttonText"
help="Text displayed on the button (uses label if empty)"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Submit, Save, Continue"
/>
<!-- Button Action Script -->
<div class="mt-6 space-y-3">
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Button Click Action</h5>
<p class="text-xs text-gray-600">
Write JavaScript code to execute when this button is clicked. You can access form data and perform actions.
</p>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
<div class="mb-2 text-xs text-gray-600">
<strong>Available functions:</strong> getField(), setField(), showField(), hideField(), enableField(), disableField(), showSuccess(), showError(), showInfo()
</div>
<textarea
v-model="configModel.onClick"
class="w-full h-32 font-mono text-sm p-2 border border-gray-300 rounded"
placeholder="// Example:
// Get values from other fields
const name = getField('name');
const email = getField('email');
// Show a message
if (name && email) {
showSuccess('Form data is valid!');
} else {
showError('Please fill in all required fields');
}"
></textarea>
</div>
<!-- Quick Templates -->
<div class="flex flex-wrap gap-2">
<button
@click="insertButtonScriptTemplate('validation')"
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
>
Insert Validation Example
</button>
<button
@click="insertButtonScriptTemplate('calculation')"
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
>
Insert Calculation Example
</button>
<button
@click="insertButtonScriptTemplate('toggle')"
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
>
Insert Toggle Fields Example
</button>
</div>
</div>
</template>
<!-- Dynamic List Configuration -->
@ -902,6 +996,7 @@
/>
<FormKit
v-if="configModel.showHeader"
type="select"
label="Header Size"
name="headerSize"
@ -916,6 +1011,7 @@
/>
<FormKit
v-if="configModel.showHeader"
type="switch"
label="Collapsible"
name="collapsible"
@ -925,13 +1021,22 @@
/>
<FormKit
v-if="configModel.showHeader && configModel.collapsible"
type="switch"
label="Start Collapsed"
name="collapsed"
v-model="configModel.collapsed"
help="Start with this section collapsed"
:classes="{ outer: 'field-wrapper' }"
:disabled="!configModel.collapsible"
/>
<FormKit
type="switch"
label="Show Placeholder"
name="showPlaceholder"
v-model="configModel.showPlaceholder"
help="Show placeholder when empty in builder mode"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</div>
@ -2162,6 +2267,80 @@ const handleReset = () => {
}
}
// Button script template helper
const insertButtonScriptTemplate = (templateType) => {
let template = '';
switch (templateType) {
case 'validation':
template = `// Form validation example
const name = getField('name');
const email = getField('email');
const phone = getField('phone');
// Validate required fields
if (!name || !email) {
showError('Please fill in all required fields');
return;
}
// Validate email format
if (email && !email.includes('@')) {
showError('Please enter a valid email address');
return;
}
// Success message
showSuccess('Form validation passed!');
`;
break;
case 'calculation':
template = `// Calculation example
const quantity = Number(getField('quantity') || 0);
const price = Number(getField('price') || 0);
const taxRate = 0.08; // 8% tax
// Calculate subtotal
const subtotal = quantity * price;
setField('subtotal', subtotal.toFixed(2));
// Calculate tax
const tax = subtotal * taxRate;
setField('tax', tax.toFixed(2));
// Calculate total
const total = subtotal + tax;
setField('total', total.toFixed(2));
showInfo('Calculation completed');
`;
break;
case 'toggle':
template = `// Toggle fields visibility example
const selectedOption = getField('options');
// Hide all detail fields first
hideField('details_option_1');
hideField('details_option_2');
hideField('details_option_3');
// Show only the relevant detail field
if (selectedOption === 'option_1') {
showField('details_option_1');
} else if (selectedOption === 'option_2') {
showField('details_option_2');
} else if (selectedOption === 'option_3') {
showField('details_option_3');
}
`;
break;
default:
return;
}
configModel.value.onClick = template;
};
const handleClose = () => {
isOpen.value = false
emit('close')