From cc5093655a296b8fcadcc7be827a00390e1fcb74 Mon Sep 17 00:00:00 2001 From: Afiq Date: Thu, 7 Aug 2025 16:25:11 +0800 Subject: [PATCH] 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. --- components/ComponentPreview.vue | 56 ++++++++-- components/RepeatingTable.vue | 173 ++++++++++++++++++------------- plugins/form-hydration.client.js | 39 +++++++ stores/formBuilder.js | 14 ++- 4 files changed, 202 insertions(+), 80 deletions(-) create mode 100644 plugins/form-hydration.client.js diff --git a/components/ComponentPreview.vue b/components/ComponentPreview.vue index 17de7ae..6176210 100644 --- a/components/ComponentPreview.vue +++ b/components/ComponentPreview.vue @@ -482,7 +482,7 @@ - +
@@ -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%; diff --git a/components/RepeatingTable.vue b/components/RepeatingTable.vue index 8bd8bd8..3b5946c 100644 --- a/components/RepeatingTable.vue +++ b/components/RepeatingTable.vue @@ -106,11 +106,10 @@ :style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }" >
- + + + {{ formatCellValue(record[column.name], column) }} +
@@ -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" >
@@ -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: ` - - {{ formatValue }} - - ` -}) +// 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') diff --git a/plugins/form-hydration.client.js b/plugins/form-hydration.client.js new file mode 100644 index 0000000..8824ee0 --- /dev/null +++ b/plugins/form-hydration.client.js @@ -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 + } + } + } + } +}) diff --git a/stores/formBuilder.js b/stores/formBuilder.js index 7a3dc4c..3d6501c 100644 --- a/stores/formBuilder.js +++ b/stores/formBuilder.js @@ -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