/** * Alpine.js Widget Components for Schema Forms * * Centralized widget component definitions for dynamic form rendering. * Each widget receives explicit parameters instead of context object for better traceability. */ document.addEventListener('alpine:init', () => { // Base widget factory with common functionality const baseWidget = (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ fieldName, data, schema, uiSchema, fieldErrors, mode, // Field schema access get fieldSchema() { return this.schema?.properties?.[this.fieldName] || {}; }, // Common metadata get label() { // Check uiSchema first (RJSF format) if (this.uiSchema?.[this.fieldName]?.['ui:label']) { return this.uiSchema[this.fieldName]['ui:label']; } // Fall back to schema title if (this.fieldSchema.title) { return this.fieldSchema.title; } // Default: format field name return this.fieldName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); }, get helpText() { return this.fieldSchema.description || ''; }, // Field-level unit extraction from uiSchema (RJSF format) get unit() { return this.uiSchema?.[this.fieldName]?.['ui:unit'] || null; }, // Error handling get hasError() { return !!this.fieldErrors?.[this.fieldName]; }, get errors() { return this.fieldErrors?.[this.fieldName] || []; }, // Mode checks get isViewMode() { return this.mode === 'view'; }, get isEditMode() { return this.mode === 'edit'; }, }); // Text widget Alpine.data('textWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get value() { return this.data[this.fieldName] || ''; }, set value(v) { this.data[this.fieldName] = v; }, })); // Textarea widget Alpine.data('textareaWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get value() { return this.data[this.fieldName] || ''; }, set value(v) { this.data[this.fieldName] = v; }, })); // Number widget with unit support Alpine.data('numberWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get value() { return this.data[this.fieldName]; }, set value(v) { this.data[this.fieldName] = v === '' || v === null ? null : parseFloat(v); }, get hasValue() { return this.value !== null && this.value !== undefined && this.value !== ''; }, // Format value with unit for view mode get displayValue() { if (!this.hasValue) return '—'; return this.unit ? `${this.value} ${this.unit}` : String(this.value); }, })); // Select widget Alpine.data('selectWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get value() { return this.data[this.fieldName] || ''; }, set value(v) { this.data[this.fieldName] = v; }, get options() { return this.fieldSchema.enum || []; }, })); // Checkbox widget Alpine.data('checkboxWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get checked() { return !!this.data[this.fieldName]; }, set checked(v) { this.data[this.fieldName] = v; }, })); // Interval widget with unit support Alpine.data('intervalWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get start() { return this.data[this.fieldName]?.start ?? null; }, set start(v) { if (!this.data[this.fieldName]) this.data[this.fieldName] = {}; this.data[this.fieldName].start = v === '' || v === null ? null : parseFloat(v); }, get end() { return this.data[this.fieldName]?.end ?? null; }, set end(v) { if (!this.data[this.fieldName]) this.data[this.fieldName] = {}; this.data[this.fieldName].end = v === '' || v === null ? null : parseFloat(v); }, // Format interval with unit for view mode get displayValue() { const s = this.start, e = this.end; const unitStr = this.unit ? ` ${this.unit}` : ''; if (s !== null && e !== null) return `${s} – ${e}${unitStr}`; if (s !== null) return `≥ ${s}${unitStr}`; if (e !== null) return `≤ ${e}${unitStr}`; return '—'; }, get isSameValue() { return this.start !== null && this.start === this.end; }, // Validation: start must be <= end get hasValidationError() { if (this.isViewMode) return false; const s = this.start; const e = this.end; // Only validate if both values are provided if (s !== null && e !== null && typeof s === 'number' && typeof e === 'number') { return s > e; } return false; }, // Override hasError to include validation error get hasError() { return this.hasValidationError || !!this.fieldErrors?.[this.fieldName]; }, // Override errors to include validation error message get errors() { const serverErrors = this.fieldErrors?.[this.fieldName] || []; const validationErrors = this.hasValidationError ? ['Start value must be less than or equal to end value'] : []; return [...validationErrors, ...serverErrors]; }, })); // PubMed link widget Alpine.data('pubmedWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get value() { return this.data[this.fieldName] || ''; }, set value(v) { this.data[this.fieldName] = v; }, get pubmedUrl() { return this.value ? `https://pubmed.ncbi.nlm.nih.gov/${this.value}` : null; }, })); // Compound link widget Alpine.data('compoundWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({ ...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode), get value() { return this.data[this.fieldName] || ''; }, set value(v) { this.data[this.fieldName] = v; }, })); });