PluginProbe
The Innovative Form Builder – IvyForms / 0.6
The Innovative Form Builder – IvyForms v0.6
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / public / fields / DateFormField.vue

DateFormField.vue in The Innovative Form Builder – IvyForms 0.6, at frontend/src/views/public/fields/DateFormField.vue

774 lines 25.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div
3 :class="[
4 'ivyforms-field',
5 'ivyforms-field__date',
6 'ivyforms-field__date_' + field.id,
7 field.cssClasses,
8 {
9 'ivyforms-field__date--is-readonly': !!field.readOnly,
10 },
11 ]"
12 >
13 <!-- Input Mode (manual entry) -->
14 <IvyFormItem
15 v-if="field.dateFieldType === 'input'"
16 :label="field.hideLabel ? '' : field.label"
17 :required="field.required"
18 :error="error"
19 :prop="field.type + '_' + field.fieldIndex"
20 :show-info="!!field.description"
21 :info-text="field.description || ''"
22 :show-info-icon="false"
23 :label-position="field.labelPosition"
24 >
25 <IvyTextInput
26 :id="fieldID"
27 v-model="textInputValue"
28 :aria-label="field.label"
29 class="ivyforms-field__date-input"
30 type="text"
31 :placeholder="field.placeholder || field.dateFormat"
32 :disabled="disabled || field.readOnly"
33 :readonly="field.readOnly"
34 @blur="onTextInputBlur"
35 @update:model-value="onTextInputChange"
36 />
37 </IvyFormItem>
38
39 <!-- Dropdown Mode -->
40 <IvyFormItem
41 v-else-if="field.dateFieldType === 'dropdown'"
42 :label="field.label"
43 :required="field.required"
44 :error="error"
45 :prop="field.type + '_' + field.fieldIndex"
46 :show-info="!!field.description"
47 :info-text="field.description || ''"
48 :show-info-icon="false"
49 :label-position="field.labelPosition"
50 :hidden-label="field.hideLabel"
51 >
52 <div class="ivyforms-flex ivyforms-gap-12 ivyforms-width-100">
53 <div
54 v-for="fieldConfig in dateFieldsConfig"
55 :key="fieldConfig.type"
56 class="ivyforms-flex ivyforms-flex-1 ivyforms-flex-direction-column"
57 >
58 <label
59 v-if="fieldConfig.type === 'day'"
60 :for="`day-select-${field.fieldIndex}`"
61 class="medium-14 ivyforms-mb-6 ivyforms-field__date__sublabel"
62 >
63 {{ getLabel('day') }}
64 </label>
65 <label
66 v-else-if="fieldConfig.type === 'month'"
67 :for="`month-select-${field.fieldIndex}`"
68 class="medium-14 ivyforms-mb-6 ivyforms-field__date__sublabel"
69 >
70 {{ getLabel('month') }}
71 </label>
72 <label
73 v-else-if="fieldConfig.type === 'year'"
74 :for="`year-select-${field.fieldIndex}`"
75 class="medium-14 ivyforms-mb-6 ivyforms-field__date__sublabel"
76 >
77 {{ getLabel('year') }}
78 </label>
79
80 <IvySelectInput
81 v-if="fieldConfig.type === 'day'"
82 :id="`day-select-${field.fieldIndex}`"
83 v-model="selectedDay"
84 class="ivyforms-field__date-day"
85 :error="!!(error && !selectedDay)"
86 placeholder="&#45;&#45;"
87 :disabled="disabled || field.readOnly"
88 :clearable="!field.readOnly"
89 field-type="date"
90 :aria-label="`${fieldConfig.type}-select-${field.fieldIndex}`"
91 @update:model-value="onPartChange"
92 >
93 <IvySelectOption
94 v-for="d in dayOptions"
95 :key="d.value"
96 :label="d.label"
97 :value="d.value"
98 field-type="date"
99 />
100 </IvySelectInput>
101 <IvySelectInput
102 v-else-if="fieldConfig.type === 'month'"
103 :id="`month-select-${field.fieldIndex}`"
104 v-model="selectedMonth"
105 class="ivyforms-field__date-month"
106 :error="!!(error && !selectedMonth)"
107 placeholder="&#45;&#45;"
108 :disabled="disabled || field.readOnly"
109 :clearable="!field.readOnly"
110 field-type="date"
111 :aria-label="`${fieldConfig.type}-select-${field.fieldIndex}`"
112 @update:model-value="onPartChange"
113 >
114 <IvySelectOption
115 v-for="m in monthOptions"
116 :key="m.value"
117 :label="m.label"
118 :value="m.value"
119 field-type="date"
120 />
121 </IvySelectInput>
122 <IvySelectInput
123 v-else-if="fieldConfig.type === 'year'"
124 :id="`year-select-${field.fieldIndex}`"
125 v-model="selectedYear"
126 class="ivyforms-field__date-year"
127 :error="!!(error && !selectedYear)"
128 placeholder="&#45;&#45;"
129 :disabled="disabled || field.readOnly"
130 :clearable="!field.readOnly"
131 field-type="date"
132 :aria-label="`${fieldConfig.type}-select-${field.fieldIndex}`"
133 @update:model-value="onPartChange"
134 >
135 <IvySelectOption
136 v-for="y in yearOptions"
137 :key="y.value"
138 :label="y.label"
139 :value="y.value"
140 field-type="date"
141 />
142 </IvySelectInput>
143 </div>
144 </div>
145 </IvyFormItem>
146
147 <!-- Date Picker Mode -->
148 <IvyFormItem
149 v-else
150 :label="field.hideLabel ? '' : field.label"
151 :required="field.required"
152 :error="error"
153 :prop="field.type + '_' + field.fieldIndex"
154 :show-info="!!field.description"
155 :info-text="field.description || ''"
156 :show-info-icon="false"
157 :label-position="field.labelPosition"
158 >
159 <IvyDatePicker
160 :id="fieldID"
161 v-model="localModelValue"
162 :aria-label="field.label"
163 class="ivyforms-field__date-picker"
164 :format="field.dateFormat || 'MM/DD/YYYY'"
165 :value-format="dayjsFormat"
166 :placeholder="field.placeholder"
167 :disabled="disabled || field.readOnly"
168 :clearable="true"
169 :disabled-date="disabledDatePickerDates"
170 />
171 </IvyFormItem>
172 </div>
173 </template>
174
175 <script setup lang="ts">
176 import { computed, inject, ref, watch, nextTick, onMounted } from 'vue'
177 import { useDateField } from '@/composables/useDateField'
178 import { useLabels } from '@/composables/useLabels'
179 import type { Field } from '@/types/field'
180 import IvyFormItem from '@/views/_components/form/IvyFormItem.vue'
181 import IvyDatePicker from '@/views/_components/datepicker/IvyDatePicker.vue'
182 import IvySelectInput from '@/views/_components/input/IvySelectInput.vue'
183 import IvySelectOption from '@/views/_components/input/IvySelectOption.vue'
184 import IvyTextInput from '@/views/_components/input/IvyTextInput.vue'
185
186 const { getLabel } = useLabels()
187
188 interface FieldProps {
189 modelValue: string | null | undefined
190 field: Field
191 disabled?: boolean
192 error?: string
193 }
194
195 const props = withDefaults(defineProps<FieldProps>(), {
196 modelValue: '',
197 disabled: false,
198 error: '',
199 })
200
201 const emit = defineEmits(['update:modelValue'])
202 const clearFieldError = inject<(fieldKey: string) => void>('clearFieldError', () => {})
203 const fieldKey = computed(() => props.field.type + '_' + props.field.fieldIndex)
204
205 const { parseDateString, formatDateString, getDaysArray, getMonthsArray, getYearsArray } =
206 useDateField()
207
208 /**
209 * Convert form date format to Day.js format for Element Plus DatePicker
210 * Converts formats like MM/DD/YYYY to Day.js format like MM/DD/YYYY
211 */
212 function convertToDayjsFormat(format: string): string {
213 // Element Plus DatePicker uses Day.js format tokens
214 // Our formats use: MM, DD, YYYY, M, D (which are Day.js compatible)
215 // Just need to ensure proper Day.js format
216 return format
217 .replace(/MM/g, 'MM') // Month with leading zero
218 .replace(/M(?!M)/g, 'M') // Month without leading zero
219 .replace(/DD/g, 'DD') // Day with leading zero
220 .replace(/D(?!D)/g, 'D') // Day without leading zero
221 .replace(/YYYY/g, 'YYYY') // Full year
222 .replace(/YY(?!YY)/g, 'YY') // Short year
223 }
224
225 // Input mode ref - single text input for date
226 const textInputValue = ref<string>('')
227
228 // Dropdown mode refs - strictly typed as strings to prevent numeric values
229 const selectedDay = ref<string>('')
230 const selectedMonth = ref<string>('')
231 const selectedYear = ref<string>('')
232
233 // Create watchers to ensure dropdown values are always strings and never become numbers
234 watch(selectedDay, (newVal) => {
235 if (
236 typeof newVal === 'number' ||
237 (typeof newVal === 'string' && newVal.includes('-9007199254740991'))
238 ) {
239 selectedDay.value = ''
240 }
241 })
242 watch(selectedMonth, (newVal) => {
243 if (
244 typeof newVal === 'number' ||
245 (typeof newVal === 'string' && newVal.includes('-9007199254740991'))
246 ) {
247 selectedMonth.value = ''
248 }
249 })
250 watch(selectedYear, (newVal) => {
251 if (
252 typeof newVal === 'number' ||
253 (typeof newVal === 'string' && newVal.includes('-9007199254740991'))
254 ) {
255 selectedYear.value = ''
256 }
257 })
258
259 // Watch for year changes to clear invalid month/day selections
260 watch(selectedYear, () => {
261 if (selectedYear.value && (minDate.value || maxDate.value)) {
262 // Check if current month is still valid
263 if (selectedMonth.value) {
264 const isMonthValid = monthOptions.value.some((m) => m.value === selectedMonth.value)
265 if (!isMonthValid) {
266 selectedMonth.value = ''
267 selectedDay.value = ''
268 }
269 }
270 }
271 })
272
273 // Watch for month changes to clear invalid day selections
274 watch(selectedMonth, () => {
275 if (selectedMonth.value && selectedYear.value && (minDate.value || maxDate.value)) {
276 // Check if current day is still valid
277 if (selectedDay.value) {
278 const isDayValid = dayOptions.value.some((d) => d.value === selectedDay.value)
279 if (!isDayValid) {
280 selectedDay.value = ''
281 }
282 }
283 }
284 })
285
286 // Flag to prevent watch from overwriting values during internal updates
287 const isInternalUpdate = ref(false)
288
289 // Determine field order and placeholders based on date format
290 const dateFieldsConfig = computed(() => {
291 const format = ((props.field.dateFormat as string) || 'MM/DD/YYYY').toUpperCase()
292 const fields: Array<{ type: 'day' | 'month' | 'year'; order: number; placeholder: string }> = []
293
294 // Find position of each component in the format
295 const dayIndex = format.indexOf('D')
296 const monthIndex = format.indexOf('M')
297 const yearIndex = format.indexOf('Y')
298
299 if (dayIndex !== -1) {
300 fields.push({ type: 'day', order: dayIndex, placeholder: format.includes('DD') ? 'DD' : 'D' })
301 }
302 if (monthIndex !== -1) {
303 fields.push({
304 type: 'month',
305 order: monthIndex,
306 placeholder: format.includes('MM') ? 'MM' : 'M',
307 })
308 }
309 if (yearIndex !== -1) {
310 fields.push({
311 type: 'year',
312 order: yearIndex,
313 placeholder: format.includes('YYYY') ? 'YYYY' : 'YY',
314 })
315 }
316
317 // Sort by position in format string
318 return fields.sort((a, b) => a.order - b.order)
319 })
320
321 const handleInput = (value: string | null | undefined, shouldClearError: boolean = true) => {
322 isInternalUpdate.value = true
323 emit('update:modelValue', value ?? '')
324 if (shouldClearError) {
325 clearFieldError(fieldKey.value)
326 }
327 nextTick(() => {
328 isInternalUpdate.value = false
329 })
330 }
331
332 // Computed for date picker mode - just pass through the value
333 const localModelValue = computed({
334 get() {
335 return props.modelValue != null ? String(props.modelValue) : ''
336 },
337 set(dateValue: string) {
338 if (dateValue) {
339 handleInput(dateValue, true)
340 } else {
341 handleInput('', true)
342 }
343 },
344 })
345
346 // Computed Day.js format for the datepicker
347 const dayjsFormat = computed(() => {
348 return convertToDayjsFormat((props.field.dateFormat as string) || 'MM/DD/YYYY')
349 })
350
351 // Parse minDateValue and maxDateValue to Date objects for validation
352 const minDate = computed(() => {
353 if (!props.field.minDateValue) return null
354 const parsed = parseDateString(
355 String(props.field.minDateValue),
356 (props.field.dateFormat as string) || 'MM/DD/YYYY',
357 )
358 if (parsed.day && parsed.month && parsed.year) {
359 return new Date(Number(parsed.year), Number(parsed.month) - 1, Number(parsed.day))
360 }
361 return null
362 })
363
364 const maxDate = computed(() => {
365 if (!props.field.maxDateValue) return null
366 const parsed = parseDateString(
367 String(props.field.maxDateValue),
368 (props.field.dateFormat as string) || 'MM/DD/YYYY',
369 )
370 if (parsed.day && parsed.month && parsed.year) {
371 return new Date(Number(parsed.year), Number(parsed.month) - 1, Number(parsed.day))
372 }
373 return null
374 })
375
376 // Function to disable dates outside the min/max range for date picker
377 const disabledDatePickerDates = (time: Date) => {
378 if (minDate.value && time < minDate.value) {
379 return true
380 }
381 if (maxDate.value && time > maxDate.value) {
382 return true
383 }
384 return false
385 }
386
387 // Handle text input mode changes
388 function onTextInputChange(value: string | number | null | undefined) {
389 // Just update the local value as user types
390 textInputValue.value = value != null ? String(value) : ''
391 emit('update:modelValue', value === undefined || value === null ? '' : value)
392 }
393
394 function onTextInputBlur() {
395 // Validate and format the date when user leaves the input
396 const value = textInputValue.value.trim()
397
398 if (!value) {
399 handleInput('', true)
400 return
401 }
402
403 // Try to parse the date according to the format
404 const parsed = parseDateString(value, (props.field.dateFormat as string) || 'MM/DD/YYYY')
405
406 if (parsed.day && parsed.month && parsed.year) {
407 // Validate the date values
408 const dayNum = Number(parsed.day)
409 const monthNum = Number(parsed.month)
410 const yearNum = Number(parsed.year)
411
412 // Check if date is valid
413 const isValidDate =
414 dayNum >= 1 &&
415 dayNum <= 31 &&
416 monthNum >= 1 &&
417 monthNum <= 12 &&
418 yearNum >= 1900 &&
419 yearNum <= 2100
420
421 if (isValidDate) {
422 const enteredDate = new Date(yearNum, monthNum - 1, dayNum)
423
424 // Validate against min/max date if set
425 if (minDate.value && enteredDate < minDate.value) {
426 // Date is before minimum - keep the input as is to show error
427 handleInput(value, false)
428 return
429 }
430 if (maxDate.value && enteredDate > maxDate.value) {
431 // Date is after maximum - keep the input as is to show error
432 handleInput(value, false)
433 return
434 }
435
436 // Format the date properly
437 const formatted = formatDateString(
438 String(dayNum),
439 String(monthNum),
440 String(yearNum),
441 (props.field.dateFormat as string) || 'MM/DD/YYYY',
442 )
443
444 textInputValue.value = formatted
445 handleInput(formatted, true)
446 } else {
447 // Invalid date - keep the input as is to show error
448 handleInput(value, false)
449 }
450 } else {
451 // Could not parse - keep the input as is to show error
452 handleInput(value, false)
453 }
454 }
455
456 // Handle dropdown mode changes
457 function onPartChange() {
458 nextTick(() => {
459 // Ensure values are strings and validate they're not invalid numeric strings
460 const dayStr = selectedDay.value == null ? '' : String(selectedDay.value)
461 const monthStr = selectedMonth.value == null ? '' : String(selectedMonth.value)
462 const yearStr = selectedYear.value == null ? '' : String(selectedYear.value)
463
464 // Check for invalid values that might contain MIN_SAFE_INTEGER or other invalid numbers
465 const isValidDay = dayStr && !dayStr.includes('-') && dayStr !== '0' && !isNaN(Number(dayStr))
466 const isValidMonth =
467 monthStr && !monthStr.includes('-') && monthStr !== '0' && !isNaN(Number(monthStr))
468 const isValidYear =
469 yearStr && !yearStr.includes('-') && yearStr.length === 4 && !isNaN(Number(yearStr))
470
471 // Only emit when all three values are valid or all are empty
472 const hasAllValues = isValidDay && isValidMonth && isValidYear
473 const hasNoValues = !dayStr && !monthStr && !yearStr
474
475 if (hasAllValues) {
476 const formatted = formatDateString(
477 dayStr,
478 monthStr,
479 yearStr,
480 (props.field.dateFormat as string) || 'MM/DD/YYYY',
481 )
482
483 // Validate against min/max date if set
484 if (minDate.value || maxDate.value) {
485 const selectedDate = new Date(Number(yearStr), Number(monthStr) - 1, Number(dayStr))
486 if (minDate.value && selectedDate < minDate.value) {
487 // Date is before minimum - don't update
488 return
489 }
490 if (maxDate.value && selectedDate > maxDate.value) {
491 // Date is after maximum - don't update
492 return
493 }
494 }
495
496 handleInput(formatted, true)
497 } else if (hasNoValues) {
498 handleInput('', true)
499 }
500 // Don't emit anything for partial or invalid states
501 })
502 }
503
504 // Options for dropdowns
505 const dayOptions = computed(() => {
506 const allDays = getDaysArray()
507
508 // If min/max dates are set, filter days accordingly
509 if (minDate.value || maxDate.value) {
510 // If we have a selected month and year, filter days for that specific month/year
511 if (selectedMonth.value && selectedYear.value) {
512 const month = Number(selectedMonth.value)
513 const year = Number(selectedYear.value)
514
515 return allDays.filter((day) => {
516 if (!day.value) return true // Keep the empty option
517 const dayNum = Number(day.value)
518 const testDate = new Date(year, month - 1, dayNum)
519
520 if (minDate.value && testDate < minDate.value) return false
521 if (maxDate.value && testDate > maxDate.value) return false
522 return true
523 })
524 }
525
526 // If only year is selected, show days that could be valid in any month of that year
527 if (selectedYear.value) {
528 const year = Number(selectedYear.value)
529
530 return allDays.filter((day) => {
531 if (!day.value) return true // Keep the empty option
532 const dayNum = Number(day.value)
533
534 // Check if this day is valid in any month of the selected year
535 for (let month = 1; month <= 12; month++) {
536 const testDate = new Date(year, month - 1, dayNum)
537
538 // Skip invalid dates (e.g., Feb 30)
539 if (testDate.getDate() !== dayNum) continue
540
541 const isValid =
542 (!minDate.value || testDate >= minDate.value) &&
543 (!maxDate.value || testDate <= maxDate.value)
544 if (isValid) return true
545 }
546 return false
547 })
548 }
549
550 // No year/month selected yet - show days that could be valid in the allowed date range
551 return allDays.filter((day) => {
552 if (!day.value) return true // Keep the empty option
553 const dayNum = Number(day.value)
554
555 // Get the range of years to check
556 const minYear = minDate.value ? minDate.value.getFullYear() : new Date().getFullYear() - 100
557 const maxYear = maxDate.value ? maxDate.value.getFullYear() : new Date().getFullYear() + 100
558
559 // Check if this day number appears in any valid month/year combination
560 for (let year = minYear; year <= maxYear; year++) {
561 for (let month = 1; month <= 12; month++) {
562 const testDate = new Date(year, month - 1, dayNum)
563
564 // Skip invalid dates (e.g., Feb 30)
565 if (testDate.getDate() !== dayNum) continue
566
567 const isValid =
568 (!minDate.value || testDate >= minDate.value) &&
569 (!maxDate.value || testDate <= maxDate.value)
570 if (isValid) return true
571 }
572 }
573 return false
574 })
575 }
576
577 return allDays
578 })
579
580 const monthOptions = computed(() => {
581 const allMonths = getMonthsArray()
582
583 // If min/max dates are set, filter months accordingly
584 if (minDate.value || maxDate.value) {
585 // If we have a selected year, filter months for that specific year
586 if (selectedYear.value) {
587 const year = Number(selectedYear.value)
588
589 return allMonths.filter((month) => {
590 if (!month.value) return true // Keep the empty option
591 const monthNum = Number(month.value)
592
593 // Check first and last day of the month
594 const firstDay = new Date(year, monthNum - 1, 1)
595 const lastDay = new Date(year, monthNum, 0)
596
597 // Include month if any day in it falls within the valid range
598 if (minDate.value && lastDay < minDate.value) return false
599 if (maxDate.value && firstDay > maxDate.value) return false
600 return true
601 })
602 }
603
604 // No year selected yet - show months that could be valid in the allowed date range
605 return allMonths.filter((month) => {
606 if (!month.value) return true // Keep the empty option
607 const monthNum = Number(month.value)
608
609 // Get the range of years to check
610 const minYear = minDate.value ? minDate.value.getFullYear() : new Date().getFullYear() - 100
611 const maxYear = maxDate.value ? maxDate.value.getFullYear() : new Date().getFullYear() + 100
612
613 // Check if this month appears in any valid year
614 for (let year = minYear; year <= maxYear; year++) {
615 const firstDay = new Date(year, monthNum - 1, 1)
616 const lastDay = new Date(year, monthNum, 0)
617
618 // Include month if any day in it falls within the valid range
619 const isValid =
620 (!minDate.value || lastDay >= minDate.value) &&
621 (!maxDate.value || firstDay <= maxDate.value)
622 if (isValid) return true
623 }
624 return false
625 })
626 }
627
628 return allMonths
629 })
630
631 const yearOptions = computed(() => {
632 let minYear = new Date().getFullYear() - 100
633 let maxYear = new Date().getFullYear() + 100
634
635 // If minDateValue is set, use its year as the minimum
636 if (minDate.value) {
637 minYear = minDate.value.getFullYear()
638 }
639
640 // If maxDateValue is set, use its year as the maximum
641 if (maxDate.value) {
642 maxYear = maxDate.value.getFullYear()
643 }
644
645 return getYearsArray(minYear, maxYear)
646 })
647
648 // Watch for modelValue changes to update inputs
649 watch(
650 () => [props.modelValue, props.field.dateFormat],
651 () => {
652 // Skip if this is an internal update (from user input)
653 if (isInternalUpdate.value) {
654 return
655 }
656
657 // Use modelValue if available, otherwise use defaultValue
658 let val = props.modelValue != null ? String(props.modelValue) : ''
659
660 // If no modelValue, check for defaultValue
661 if (!val && props.field.defaultValue) {
662 val = String(props.field.defaultValue)
663 }
664
665 // Sanitize: reject any invalid values containing MIN_SAFE_INTEGER or other invalid data
666 if (val && (val.includes('-9007199254740991') || val.includes('MIN_SAFE_INTEGER'))) {
667 val = ''
668 // Emit empty value to clean up the corrupted data
669 handleInput('', false)
670 }
671
672 if (val && val.trim() !== '') {
673 if (props.field.dateFieldType === 'input') {
674 // For text input mode, just set the formatted value
675 textInputValue.value = val
676 } else if (props.field.dateFieldType === 'dropdown') {
677 const parsed = parseDateString(val, (props.field.dateFormat as string) || 'MM/DD/YYYY')
678 // Only set if parsed values are valid strings (not empty and not invalid)
679 selectedDay.value = parsed.day && parsed.day !== '' && parsed.day !== '0' ? parsed.day : ''
680 selectedMonth.value =
681 parsed.month && parsed.month !== '' && parsed.month !== '0' ? parsed.month : ''
682 selectedYear.value =
683 parsed.year && parsed.year !== '' && !parsed.year.includes('-') ? parsed.year : ''
684 }
685 } else {
686 if (props.field.dateFieldType === 'input') {
687 textInputValue.value = ''
688 } else if (props.field.dateFieldType === 'dropdown') {
689 selectedDay.value = ''
690 selectedMonth.value = ''
691 selectedYear.value = ''
692 }
693 }
694 },
695 { immediate: true },
696 )
697
698 // Watch for default value changes to update inputs when modelValue is empty
699 watch(
700 () => props.field.defaultValue,
701 (newDefaultValue) => {
702 // Only apply default value if modelValue is empty or undefined
703 const currentValue = props.modelValue != null ? String(props.modelValue) : ''
704 if (currentValue) {
705 return // Don't override user's current value
706 }
707
708 if (newDefaultValue && typeof newDefaultValue === 'string') {
709 if (props.field.dateFieldType === 'input') {
710 // For text input mode, just set the formatted value
711 textInputValue.value = newDefaultValue
712 } else if (props.field.dateFieldType === 'dropdown') {
713 const parsed = parseDateString(
714 newDefaultValue,
715 (props.field.dateFormat as string) || 'MM/DD/YYYY',
716 )
717 selectedDay.value = parsed.day || ''
718 selectedMonth.value = parsed.month || ''
719 selectedYear.value = parsed.year || ''
720 }
721 } else {
722 // Clear values if no default value
723 if (props.field.dateFieldType === 'input') {
724 textInputValue.value = ''
725 } else if (props.field.dateFieldType === 'dropdown') {
726 selectedDay.value = ''
727 selectedMonth.value = ''
728 selectedYear.value = ''
729 }
730 }
731 },
732 { immediate: false },
733 )
734
735 const fieldID = computed(() => {
736 return `ivyforms-field__date-input_${props.field.formId || ''}_${props.field.fieldIndex || ''}`
737 })
738
739 // Ensure fields start with empty value if no modelValue or defaultValue
740 onMounted(() => {
741 const hasModelValue = props.modelValue != null && String(props.modelValue) !== ''
742 const hasDefaultValue =
743 props.field.defaultValue != null && String(props.field.defaultValue) !== ''
744
745 // If no value exists, emit empty string to ensure form doesn't try to validate placeholders or undefined values
746 if (!hasModelValue && !hasDefaultValue) {
747 if (props.field.dateFieldType === 'input' || props.field.dateFieldType === 'dropdown') {
748 handleInput('', false)
749 }
750 }
751 })
752 </script>
753
754 <style lang="scss">
755 .ivyforms-field__date {
756 &__sublabel {
757 color: var(--map-base-text-0);
758 }
759
760 &--is-readonly {
761 opacity: 0.6;
762 pointer-events: none;
763 }
764
765 .ivyforms-field__date-picker,
766 .ivyforms-field__date-input,
767 .ivyforms-field__date-day,
768 .ivyforms-field__date-month,
769 .ivyforms-field__date-year {
770 width: 100%;
771 }
772 }
773 </style>
774