Enhance ComponentPreview and RepeatingTable with Debugging and Loading States

- Updated ComponentPreview.vue to include debugging logs for table data and component mounting, improving traceability during development.
- Modified RepeatingTable.vue to replace the SimpleCellValue component with direct template rendering for better performance and clarity.
- Enhanced form submission logging in RepeatingTable.vue to provide detailed insights into form data handling.
- Introduced loading state styles in ComponentPreview.vue to improve user experience during data fetching and rendering.
- Ensured SSR safety in form data updates within formBuilder.js to prevent hydration issues, enhancing application stability.
This commit is contained in:
Afiq 2025-08-07 16:25:11 +08:00
parent bf5e1630b3
commit cc5093655a
4 changed files with 202 additions and 80 deletions

View File

@ -482,7 +482,7 @@
</div>
</div>
<!-- Repeating Table Component -->
<!-- Repeating Table Component -->
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
<div class="table-container-wrapper">
<RepeatingTable
@ -490,6 +490,8 @@
:model-value="getTableData(component.props.name)"
:is-preview="isPreview"
@update:model-value="updateTableData"
@debug-data="(data) => console.log('[ComponentPreview] Table debug data:', data)"
@mounted="() => console.log('[ComponentPreview] RepeatingTable mounted with data:', getTableData(component.props.name))"
/>
</div>
</div>
@ -1411,12 +1413,13 @@ const getTableData = (tableName) => {
return currentData;
}
// If data doesn't exist, initialize it immediately
// If data doesn't exist, initialize it
const initialData = [];
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
formStore.updatePreviewFormData(updatedFormData);
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
nextTick(() => {
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
formStore.updatePreviewFormData(updatedFormData);
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
});
return initialData;
};
@ -2513,6 +2516,47 @@ onMounted(() => {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
}
/* Table Loading State Styles */
.table-loading-state {
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.loading-header {
@apply p-4 md:p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200;
}
.loading-title {
@apply text-lg font-semibold text-gray-800 leading-tight mb-1;
}
.loading-subtitle {
@apply text-sm text-gray-600 leading-relaxed;
}
.loading-table {
@apply p-4;
}
.loading-row {
@apply flex gap-4 mb-3;
}
.loading-header-row {
@apply border-b border-gray-200 pb-3 mb-4;
}
.loading-cell {
@apply flex-1 h-6 bg-gray-200 rounded animate-pulse;
min-width: 80px;
}
.loading-header-row .loading-cell {
@apply h-4 bg-gray-300;
}
/* Custom HTML Component Styles */
.custom-html-wrapper {
width: 100%;

View File

@ -106,11 +106,10 @@
:style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
>
<div class="cell-content">
<SimpleCellValue
:value="record[column.name]"
:column="column"
:record="record"
/>
<!-- Direct template rendering instead of SimpleCellValue component -->
<span class="cell-value">
{{ formatCellValue(record[column.name], column) }}
</span>
</div>
</td>
<td v-if="showActions" class="actions-cell">
@ -222,7 +221,7 @@
type="form"
:actions="false"
v-model="formData"
@submit="saveRecord"
@submit="(formData) => { console.log('[RepeatingTable] Form submitted with:', formData); saveRecord(formData); }"
class="record-form"
>
<div class="form-grid">
@ -298,59 +297,7 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import { useDebounceFn } from '@vueuse/core'
// Simple cell component that shows data immediately
const SimpleCellValue = defineComponent({
props: {
value: {
type: [String, Number, Boolean, Object],
default: null
},
column: {
type: Object,
required: true
},
record: {
type: Object,
required: true
}
},
setup(props) {
const formatValue = computed(() => {
const value = props.value
if (value === null || value === undefined || value === '') {
return '-'
}
if (props.column.type === 'date') {
return new Date(value).toLocaleDateString()
}
if (props.column.type === 'time') {
return value
}
if (props.column.type === 'checkbox') {
return value ? 'Yes' : 'No'
}
if (props.column.type === 'select' && props.column.options) {
const option = props.column.options.find(opt => opt.value === value)
return option ? option.label : value
}
return value.toString()
})
return {
formatValue
}
},
template: `
<span class="cell-value">
{{ formatValue }}
</span>
`
})
// SimpleCellValue component removed - using direct template rendering instead
const props = defineProps({
config: {
@ -367,7 +314,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'debug-data', 'mounted'])
// Reactive state
const data = ref([...(props.modelValue || [])])
@ -380,6 +327,9 @@ const formData = ref({})
const formId = ref(`table-form-${uuidv4()}`)
const tableContainer = ref(null)
// Debug flag
const debug = ref(true)
// Pagination state
const currentPage = ref(1)
const pageSize = ref(10)
@ -401,15 +351,20 @@ watch(searchQuery, (newQuery) => {
// Computed properties with memoization
const filteredData = computed(() => {
if (!searchQuery.value) return data.value
if (!searchQuery.value) {
console.log('[RepeatingTable] filteredData (no search):', data.value)
return data.value
}
const query = searchQuery.value.toLowerCase()
return data.value.filter(record => {
const result = data.value.filter(record => {
return props.config.columns.some(column => {
const value = record[column.name]
return value && value.toString().toLowerCase().includes(query)
})
})
console.log('[RepeatingTable] filteredData (with search):', result)
return result
})
// Column virtualization for large datasets
@ -466,12 +421,11 @@ const getColumnWidth = (column) => {
return width
}
// Record key generation for better Vue rendering
// Record key generation for better Vue rendering - production safe
const getRecordKey = (record, index) => {
if (!recordKeys.value.has(record)) {
recordKeys.value.set(record, `record-${index}-${Date.now()}`)
}
return recordKeys.value.get(record)
// Use a more reliable key generation that works in production
const recordId = record.id || record._id || `record-${index}`
return `${recordId}-${index}`
}
const isAddDisabled = computed(() => {
@ -494,7 +448,15 @@ const totalPages = computed(() => {
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredData.value.slice(start, end)
const result = filteredData.value.slice(start, end)
console.log('[RepeatingTable] paginatedData computed:', {
start,
end,
filteredDataLength: filteredData.value.length,
resultLength: result.length,
result: result
})
return result
})
const startRecord = computed(() => {
@ -538,7 +500,7 @@ const visiblePages = computed(() => {
// Guard to prevent recursive updates
const isUpdatingFromProps = ref(false)
// Watch for external data changes
// Watch for external data changes with SSR safety
watch(() => props.modelValue, (newValue) => {
// Handle the case where newValue might be undefined/null
const safeNewValue = newValue || []
@ -549,6 +511,12 @@ watch(() => props.modelValue, (newValue) => {
if (newDataStr === currentDataStr) return
// Prevent hydration issues by ensuring updates happen on client side
if (process.server) {
data.value = [...safeNewValue]
return
}
isUpdatingFromProps.value = true
data.value = [...safeNewValue]
// Clear caches when data changes
@ -564,14 +532,20 @@ watch(() => props.modelValue, (newValue) => {
watch(data, (newData, oldData) => {
if (isUpdatingFromProps.value) return
// Prevent emissions during SSR to avoid hydration issues
if (process.server) return
// Check if data actually changed
const newDataStr = JSON.stringify(newData)
const oldDataStr = JSON.stringify(oldData)
if (newDataStr === oldDataStr) return
console.log('[RepeatingTable] Data changed, emitting update:', newData)
nextTick(() => {
emit('update:modelValue', [...newData])
emit('debug-data', newData)
})
}, { deep: true })
@ -585,6 +559,7 @@ const openAddModal = () => {
formData.value[column.name] = getDefaultValue(column.type)
})
console.log('[RepeatingTable] openAddModal - initialized formData:', formData.value)
showModal.value = true
}
@ -608,15 +583,36 @@ const submitForm = () => {
}
}
const saveRecord = (formData) => {
const saveRecord = (submittedFormData) => {
console.log('[RepeatingTable] saveRecord called with:', submittedFormData)
console.log('[RepeatingTable] Current data before save:', data.value)
if (editingIndex.value !== null) {
// Update existing record
data.value[editingIndex.value] = { ...formData }
data.value[editingIndex.value] = {
...submittedFormData,
id: data.value[editingIndex.value].id || `record-${editingIndex.value}-${Date.now()}`
}
console.log('[RepeatingTable] Updated record at index:', editingIndex.value, 'with data:', submittedFormData)
} else {
// Add new record
data.value.push({ ...formData })
// Add new record with unique ID
const newRecord = {
...submittedFormData,
id: `record-${data.value.length}-${Date.now()}`
}
data.value.push(newRecord)
console.log('[RepeatingTable] Added new record with data:', newRecord)
}
console.log('[RepeatingTable] Data after save:', data.value)
// Force reactivity update in production
nextTick(() => {
// Trigger a reactive update by reassigning the array
data.value = [...data.value]
console.log('[RepeatingTable] Forced reactivity update, data:', data.value)
})
closeModal()
}
@ -698,6 +694,34 @@ const goToPage = (page) => {
currentPage.value = page
}
// Format cell value directly in template
const formatCellValue = (value, column) => {
console.log(`[formatCellValue] Column: ${column.name}, Value:`, value, 'Type:', typeof value)
if (value === null || value === undefined || value === '') {
return '-'
}
if (column.type === 'date') {
return new Date(value).toLocaleDateString()
}
if (column.type === 'time') {
return value
}
if (column.type === 'checkbox') {
return value ? 'Yes' : 'No'
}
if (column.type === 'select' && column.options) {
const option = column.options.find(opt => opt.value === value)
return option ? option.label : value
}
return value.toString()
}
// Calculate minimum table width based on columns
const calculateMinTableWidth = computed(() => {
if (!props.config.columns) return 'auto'
@ -729,6 +753,9 @@ const calculateMinTableWidth = computed(() => {
// Debug table width on mount
onMounted(() => {
console.log('[RepeatingTable] Component mounted with data:', data.value)
emit('mounted')
nextTick(() => {
if (tableContainer.value) {
const table = tableContainer.value.querySelector('.data-table')

View File

@ -0,0 +1,39 @@
export default defineNuxtPlugin(() => {
// This plugin runs only on the client side to handle form data hydration
const { $formStore } = useNuxtApp()
// Ensure form store data is properly initialized on client hydration
if (process.client) {
// Add any additional client-side form data initialization logic here
console.log('[FormHydration] Client-side form data hydration plugin loaded')
// Handle any form data that needs to be restored after hydration
const restoreFormData = () => {
try {
// Check if there's any persisted form data that needs restoration
const persistedData = sessionStorage.getItem('form-builder-data')
if (persistedData && $formStore) {
const parsed = JSON.parse(persistedData)
if (parsed.previewFormData) {
$formStore.updatePreviewFormData(parsed.previewFormData)
console.log('[FormHydration] Restored form data from session storage')
}
}
} catch (error) {
console.warn('[FormHydration] Failed to restore form data:', error)
}
}
// Restore data after page load
window.addEventListener('load', restoreFormData)
// Clean up
return {
provide: {
formHydration: {
restoreFormData
}
}
}
}
})

View File

@ -1002,9 +1002,21 @@ export const useFormBuilderStore = defineStore('formBuilder', {
});
},
// Update preview form data
// Update preview form data - SSR safe
updatePreviewFormData(data) {
this.previewFormData = { ...data };
// Only persist to sessionStorage on client side to prevent hydration issues
if (process.client) {
try {
sessionStorage.setItem('form-builder-data', JSON.stringify({
previewFormData: this.previewFormData,
timestamp: Date.now()
}));
} catch (error) {
console.warn('[FormStore] Failed to persist form data:', error);
}
}
},
// Optimize the grid layout by analyzing the current components