@coherent.js/forms 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/validators.js", "../src/form-hydration.js"],
4
- "sourcesContent": ["/**\n * Coherent.js Forms - Validators\n *\n * Form validation utilities\n *\n * @module forms/validators\n */\n\nimport { validators as packageValidators } from './validation.js';\n\n/**\n * Built-in validators with signature: (value, options, translator, allValues) => errorMessage | null\n */\nexport const validators = {\n required: (value, options = {}) => {\n if (value === null || value === undefined || value === '') {\n return options.message || validators.required.message || 'This field is required';\n }\n return null;\n },\n\n email: (value) => {\n if (!value) return null;\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(value)) {\n return 'Please enter a valid email address';\n }\n return null;\n },\n\n minLength: (value, options = {}) => {\n if (!value) return null;\n const min = options.min || 0;\n if (value.length < min) {\n return options.message || `Must be at least ${min} characters`;\n }\n return null;\n },\n\n maxLength: (value, options = {}) => {\n if (!value) return null;\n const max = options.max || Infinity;\n if (value.length > max) {\n return options.message || `Must be no more than ${max} characters`;\n }\n return null;\n },\n\n min: (value, options = {}) => {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n const minValue = options.min || 0;\n if (isNaN(num) || num < minValue) {\n return options.message || `Must be at least ${minValue}`;\n }\n return null;\n },\n\n max: (value, options = {}) => {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n const maxValue = options.max || Infinity;\n if (isNaN(num) || num > maxValue) {\n return options.message || `Must be no more than ${maxValue}`;\n }\n return null;\n },\n\n pattern: (value, options = {}) => {\n if (!value) return null;\n const regex = options.pattern || options.regex;\n if (regex && !regex.test(value)) {\n return options.message || 'Invalid format';\n }\n return null;\n },\n\n url: (value) => {\n if (!value) return null;\n try {\n new URL(value);\n return null;\n } catch {\n return 'Please enter a valid URL';\n }\n },\n\n number: (value) => {\n if (value === null || value === undefined || value === '') return null;\n if (isNaN(Number(value))) {\n return 'Must be a valid number';\n }\n return null;\n },\n\n integer: (value) => {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n if (isNaN(num) || !Number.isInteger(num)) {\n return 'Must be a whole number';\n }\n return null;\n },\n\n phone: (value) => {\n if (!value) return null;\n const phoneRegex = /^[\\d\\s\\-\\+\\(\\)]+$/;\n if (!phoneRegex.test(value) || value.replace(/\\D/g, '').length < 10) {\n return 'Please enter a valid phone number';\n }\n return null;\n },\n\n date: (value) => {\n if (!value) return null;\n const date = new Date(value);\n if (isNaN(date.getTime())) {\n return 'Please enter a valid date';\n }\n return null;\n },\n\n match: (value, options = {}, translator, allValues = {}) => {\n if (!value) return null;\n const fieldName = options.field || options.fieldName;\n if (value !== allValues[fieldName]) {\n return options.message || `Must match ${fieldName}`;\n }\n return null;\n },\n\n custom: (value, options = {}, translator, allValues) => {\n const validatorFn = options.validator || options.fn;\n if (!validatorFn) return null;\n const isValid = validatorFn(value, allValues);\n return isValid ? null : (options.message || 'Validation failed');\n },\n\n fileType: (value, options = {}) => {\n if (!value) return null;\n \n const allowedTypes = options.accept || options.types || [];\n \n // Handle File object\n if (value.type !== undefined) {\n const fileType = value.type;\n const fileExt = value.name ? value.name.split('.').pop().toLowerCase() : '';\n \n // Check MIME type or extension\n const isValid = allowedTypes.some(type => {\n if (type.startsWith('.')) {\n return fileExt === type.slice(1).toLowerCase();\n }\n if (type.includes('/')) {\n if (type.endsWith('/*')) {\n return fileType.startsWith(type.replace('/*', '/'));\n }\n return fileType === type;\n }\n return fileExt === type.toLowerCase();\n });\n \n if (!isValid) {\n return options.message || `File type must be one of: ${allowedTypes.join(', ')}`;\n }\n return null;\n }\n \n return null;\n },\n\n fileSize: (value, options = {}) => {\n if (!value) return null;\n \n const maxSize = options.maxSize || Infinity;\n \n // Handle File object\n if (value.size !== undefined) {\n if (value.size > maxSize) {\n const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);\n return options.message || `File size must be less than ${maxSizeMB}MB`;\n }\n return null;\n }\n \n return null;\n },\n\n fileExtension: (value, options = {}) => {\n if (!value) return null;\n \n const allowedExtensions = options.extensions || [];\n const fileName = value.name || value;\n const ext = `.${ fileName.split('.').pop().toLowerCase()}`;\n \n const isValid = allowedExtensions.some(allowed => {\n return ext === allowed.toLowerCase();\n });\n \n if (!isValid) {\n return options.message || `File extension must be one of: ${allowedExtensions.join(', ')}`;\n }\n return null;\n },\n\n alpha: (value) => {\n if (!value) return null;\n const alphaRegex = /^[a-zA-Z]+$/;\n if (!alphaRegex.test(value)) {\n return 'Must contain only letters';\n }\n return null;\n },\n\n alphanumeric: (value) => {\n if (!value) return null;\n const alphanumericRegex = /^[a-zA-Z0-9]+$/;\n if (!alphanumericRegex.test(value)) {\n return 'Must contain only letters and numbers';\n }\n return null;\n },\n\n uppercase: (value) => {\n if (!value) return null;\n if (value !== value.toUpperCase()) {\n return 'Must be uppercase';\n }\n return null;\n },\n\n // Get a registered validator\n get: (name) => {\n return validators[name];\n },\n\n // Compose multiple validators\n compose: (validatorList) => {\n return (value, options, translator, allValues) => {\n for (const validator of validatorList) {\n const error = typeof validator === 'function'\n ? validator(value, options, translator, allValues)\n : null;\n if (error) {\n return error;\n }\n }\n return null;\n };\n },\n\n // Debounce async validator\n debounce: (validator, delay = 300) => {\n let timeoutId;\n return (value) => {\n return new Promise((resolve) => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(async () => {\n const result = await validator(value);\n resolve(result);\n }, delay);\n });\n };\n },\n\n // Cancellable async validator\n cancellable: (validator) => {\n let abortController;\n const wrapped = async (value) => {\n if (abortController) {\n abortController.abort();\n }\n // AbortController is a global browser/Node.js API\n abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;\n try {\n return await validator(value, abortController ? abortController.signal : null);\n } catch (error) {\n if (error.name === 'AbortError') {\n return null;\n }\n throw error;\n }\n };\n wrapped.cancel = () => {\n if (abortController) {\n abortController.abort();\n }\n };\n return wrapped;\n },\n\n // Conditional validator\n when: (condition, validator) => {\n return (value, options = {}, translator, allValues = {}) => {\n // Pass options as context if it looks like context (has non-validator properties)\n const context = options.min !== undefined || options.max !== undefined ? allValues : options;\n const shouldValidate = typeof condition === 'function' \n ? condition(value, context) \n : condition;\n \n if (!shouldValidate) {\n return null;\n }\n \n return typeof validator === 'function'\n ? validator(value, options, translator, allValues)\n : null;\n };\n },\n\n // Validator chain builder\n chain: (options = {}) => {\n const validatorList = [];\n const stopOnFirstError = options.stopOnFirstError !== false;\n \n const chain = {\n required: (opts) => {\n validatorList.push((v, o, t, a) => validators.required(v, opts || o, t, a));\n return chain;\n },\n email: (opts) => {\n validatorList.push((v, o, t, a) => validators.email(v, opts || o, t, a));\n return chain;\n },\n minLength: (opts) => {\n validatorList.push((v, o, t, a) => validators.minLength(v, opts || o, t, a));\n return chain;\n },\n maxLength: (opts) => {\n validatorList.push((v, o, t, a) => validators.maxLength(v, opts || o, t, a));\n return chain;\n },\n custom: (fn, message) => {\n validatorList.push((v, o, t, a) => {\n // Custom validator returns null if valid, message if invalid\n const result = fn(v, a);\n return result === null || result === true || result === undefined ? null : (message || result);\n });\n return chain;\n },\n validate: (value, opts, translator, allValues) => {\n if (stopOnFirstError) {\n // Stop on first error - return single error or null\n for (const validator of validatorList) {\n const error = validator(value, opts, translator, allValues);\n if (error) {\n return error;\n }\n }\n return null;\n } else {\n // Collect all errors - return array or null\n const errors = [];\n for (const validator of validatorList) {\n const error = validator(value, opts, translator, allValues);\n if (error) {\n errors.push(error);\n }\n }\n return errors.length > 0 ? errors : null;\n }\n }\n };\n \n return chain;\n }\n};\n\n/**\n * Validate a single field\n */\nexport function validateField(value, validatorList, formData = {}) {\n for (const validator of validatorList) {\n const error = validator(value, formData);\n if (error) {\n return error;\n }\n }\n return null;\n}\n\n/**\n * Validate entire form\n */\nexport function validateForm(formData, fieldValidators) {\n const errors = {};\n \n for (const [fieldName, validatorList] of Object.entries(fieldValidators)) {\n const value = formData[fieldName];\n const error = validateField(value, validatorList, formData);\n if (error) {\n errors[fieldName] = error;\n }\n }\n \n return Object.keys(errors).length > 0 ? errors : null;\n}\n\n/**\n * Create a validator\n */\nexport function createValidator(validatorFn, message) {\n return (value, options, translator, allValues) => {\n const result = validatorFn(value, options, translator, allValues);\n // If validator returns a string, use it as the error message\n if (typeof result === 'string') {\n return result;\n }\n // If validator returns falsy (null, false, undefined), no error\n if (!result) {\n return null;\n }\n // If validator returns truthy (true, object, etc), use provided message\n return message || 'Validation failed';\n };\n}\n\n/**\n * Register a custom validator\n *\n * index.js re-exports validation.js's `validators`, which shadows this\n * module's object in the star export \u2014 so registering only here left\n * `validators[name]` undefined for every consumer. Register in both: the\n * package registry so callers can reach it, and the local one so `get()` and\n * `compose()` keep resolving it by name.\n */\nexport function registerValidator(name, validatorFn) {\n validators[name] = validatorFn;\n packageValidators[name] = validatorFn;\n}\n\n/**\n * Compose multiple validators\n */\nexport function composeValidators(...validatorFns) {\n return (value, options, translator, allValues) => {\n for (const validator of validatorFns) {\n const error = validator(value, options, translator, allValues);\n if (error) {\n return error;\n }\n }\n return null;\n };\n}\n\nexport default {\n validators,\n validateField,\n validateForm,\n createValidator,\n registerValidator,\n composeValidators\n};\n", "/**\n * Form Hydration for Coherent.js\n *\n * Progressive enhancement for server-rendered forms\n * Reads validation metadata from HTML and attaches client-side behavior\n *\n * @module forms/form-hydration\n */\n\nimport { validators } from './validators.js';\n\n/**\n * Hydrate a server-rendered form with client-side validation and behavior\n *\n * @param {string|HTMLFormElement} formSelector - Form selector or element\n * @param {Object} options - Hydration options\n * @returns {Object} Form controller\n */\nexport function hydrateForm(formSelector, options = {}) {\n // Browser-only check\n if (typeof document === 'undefined') {\n console.warn('hydrateForm can only run in browser environment');\n return null;\n }\n\n const form = typeof formSelector === 'string'\n ? document.querySelector(formSelector)\n : formSelector;\n\n if (!form) {\n console.warn(`Form not found: ${formSelector}`);\n return null;\n }\n\n const opts = {\n validateOnBlur: true,\n validateOnChange: false,\n validateOnSubmit: true,\n showErrorsOnTouch: true,\n debounce: 300,\n ...options\n };\n\n // Form state\n const state = {\n values: {},\n errors: {},\n touched: {},\n isSubmitting: false,\n fields: new Map()\n };\n\n // Debounce timers\n const debounceTimers = new Map();\n\n /**\n * Parse validators from data-validators attribute\n */\n function parseValidators(validatorString) {\n if (!validatorString) return [];\n\n return validatorString.split(',').map(v => {\n const trimmed = v.trim();\n\n // Handle validators with parameters: minLength:8\n const [name, ...params] = trimmed.split(':');\n\n if (validators[name]) {\n return params.length > 0\n ? validators[name](...params.map(p => isNaN(p) ? p : Number(p)))\n : validators[name];\n }\n\n return null;\n }).filter(Boolean);\n }\n\n /**\n * Discover and register fields from form HTML\n */\n function discoverFields() {\n const inputs = form.querySelectorAll('[name]');\n\n inputs.forEach(input => {\n const name = input.getAttribute('name');\n const field = {\n name,\n element: input,\n type: input.getAttribute('type') || 'text',\n required: input.hasAttribute('required') || input.dataset.required === 'true',\n validators: parseValidators(input.dataset.validators),\n errorElement: null\n };\n\n // Find or create error display element\n const errorId = `${name}-error`;\n field.errorElement = document.getElementById(errorId) || createErrorElement(name, input);\n\n state.fields.set(name, field);\n state.values[name] = getFieldValue(input);\n state.touched[name] = false;\n state.errors[name] = null;\n });\n }\n\n /**\n * Create error display element\n */\n function createErrorElement(name, inputElement) {\n const errorDiv = document.createElement('div');\n errorDiv.id = `${name}-error`;\n errorDiv.className = 'error-message';\n errorDiv.setAttribute('role', 'alert');\n errorDiv.style.display = 'none';\n\n // Insert after input or its parent field wrapper\n const fieldWrapper = inputElement.closest('.form-field') || inputElement.parentElement;\n fieldWrapper.appendChild(errorDiv);\n\n return errorDiv;\n }\n\n /**\n * Get field value based on input type\n */\n function getFieldValue(input) {\n if (input.type === 'checkbox') {\n return input.checked;\n } else if (input.type === 'radio') {\n const checked = form.querySelector(`[name=\"${input.name}\"]:checked`);\n return checked ? checked.value : null;\n } else {\n return input.value;\n }\n }\n\n /**\n * Set field value\n */\n function setFieldValue(name, value) {\n const field = state.fields.get(name);\n if (!field) return;\n\n const { element } = field;\n\n if (element.type === 'checkbox') {\n element.checked = Boolean(value);\n } else if (element.type === 'radio') {\n const radio = form.querySelector(`[name=\"${name}\"][value=\"${value}\"]`);\n if (radio) radio.checked = true;\n } else {\n element.value = value;\n }\n\n state.values[name] = value;\n }\n\n /**\n * Validate a single field\n */\n function validateField(name) {\n const field = state.fields.get(name);\n if (!field) return true;\n\n const value = state.values[name];\n let error = null;\n\n // Required validation\n if (field.required && (value === null || value === undefined || value === '')) {\n error = 'This field is required';\n }\n\n // Run custom validators\n if (!error && field.validators.length > 0) {\n for (const validator of field.validators) {\n const result = validator.validate\n ? validator.validate(value, state.values)\n : validator(value, state.values);\n\n if (result !== true && result !== undefined && result !== null) {\n error = validator.message || result || 'Validation failed';\n break;\n }\n }\n }\n\n state.errors[name] = error;\n displayError(name, error);\n\n return !error;\n }\n\n /**\n * Display error message\n */\n function displayError(name, error) {\n const field = state.fields.get(name);\n if (!field) return;\n\n const { element, errorElement } = field;\n\n if (error && state.touched[name] && opts.showErrorsOnTouch) {\n // Show error\n errorElement.textContent = error;\n errorElement.style.display = 'block';\n element.setAttribute('aria-invalid', 'true');\n element.classList.add('error');\n } else {\n // Hide error\n errorElement.textContent = '';\n errorElement.style.display = 'none';\n element.setAttribute('aria-invalid', 'false');\n element.classList.remove('error');\n }\n }\n\n /**\n * Validate entire form\n */\n function validateForm() {\n let isValid = true;\n\n for (const name of state.fields.keys()) {\n const fieldValid = validateField(name);\n if (!fieldValid) isValid = false;\n }\n\n return isValid;\n }\n\n /**\n * Handle input change\n */\n function handleChange(event) {\n const input = event.target;\n const name = input.getAttribute('name');\n\n if (!state.fields.has(name)) return;\n\n state.values[name] = getFieldValue(input);\n\n if (opts.validateOnChange) {\n // Debounce validation\n if (debounceTimers.has(name)) {\n clearTimeout(debounceTimers.get(name));\n }\n\n const timer = setTimeout(() => {\n validateField(name);\n debounceTimers.delete(name);\n }, opts.debounce);\n\n debounceTimers.set(name, timer);\n }\n }\n\n /**\n * Handle input blur\n */\n function handleBlur(event) {\n const input = event.target;\n const name = input.getAttribute('name');\n\n if (!state.fields.has(name)) return;\n\n state.touched[name] = true;\n\n if (opts.validateOnBlur) {\n validateField(name);\n }\n }\n\n /**\n * Handle form submission\n */\n function handleSubmit(event) {\n event.preventDefault();\n\n // Mark all fields as touched\n for (const name of state.fields.keys()) {\n state.touched[name] = true;\n }\n\n const isValid = validateForm();\n\n if (!isValid) {\n // Focus first error field\n const firstErrorField = Array.from(state.fields.values())\n .find(field => state.errors[field.name]);\n\n if (firstErrorField) {\n firstErrorField.element.focus();\n }\n\n // Call onError callback\n if (options.onError) {\n options.onError(state.errors);\n }\n\n return;\n }\n\n // Form is valid, prepare submission\n state.isSubmitting = true;\n\n const submitData = { ...state.values };\n\n // Call onSubmit callback\n if (options.onSubmit) {\n const result = options.onSubmit(submitData, event);\n\n // If onSubmit returns false, don't submit\n if (result === false) {\n state.isSubmitting = false;\n return;\n }\n\n // If onSubmit returns a promise, wait for it\n if (result && typeof result.then === 'function') {\n result\n .then(() => {\n state.isSubmitting = false;\n if (options.onSuccess) {\n options.onSuccess(submitData);\n }\n })\n .catch(error => {\n state.isSubmitting = false;\n if (options.onError) {\n options.onError(error);\n }\n });\n return;\n }\n }\n\n // Default: submit the form normally\n if (!options.onSubmit) {\n form.submit();\n }\n\n state.isSubmitting = false;\n }\n\n /**\n * Attach event listeners\n */\n function attachEventListeners() {\n // Input change events\n state.fields.forEach(field => {\n field.element.addEventListener('input', handleChange);\n field.element.addEventListener('blur', handleBlur);\n });\n\n // Form submit\n form.addEventListener('submit', handleSubmit);\n }\n\n /**\n * Detach event listeners (cleanup)\n */\n function detachEventListeners() {\n state.fields.forEach(field => {\n field.element.removeEventListener('input', handleChange);\n field.element.removeEventListener('blur', handleBlur);\n });\n\n form.removeEventListener('submit', handleSubmit);\n\n // Clear debounce timers\n debounceTimers.forEach(timer => clearTimeout(timer));\n debounceTimers.clear();\n }\n\n /**\n * Reset form to initial state\n */\n function reset() {\n state.fields.forEach(field => {\n setFieldValue(field.name, '');\n state.touched[field.name] = false;\n state.errors[field.name] = null;\n displayError(field.name, null);\n });\n\n state.isSubmitting = false;\n form.reset();\n }\n\n // Initialize\n discoverFields();\n attachEventListeners();\n\n // Public API\n return {\n validateField,\n validateForm,\n setFieldValue,\n getFieldValue: (name) => state.values[name],\n getError: (name) => state.errors[name],\n getErrors: () => ({ ...state.errors }),\n getValues: () => ({ ...state.values }),\n setTouched: (name, touched = true) => {\n state.touched[name] = touched;\n },\n reset,\n destroy: detachEventListeners,\n isValid: () => Object.values(state.errors).every(e => !e),\n isSubmitting: () => state.isSubmitting,\n getState: () => ({\n values: { ...state.values },\n errors: { ...state.errors },\n touched: { ...state.touched },\n isSubmitting: state.isSubmitting\n })\n };\n}\n\nexport default hydrateForm;\n"],
5
- "mappings": "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",
6
- "names": []
3
+ "sources": ["../src/validators.js", "../src/form-builder.js", "../src/form-hydration.js"],
4
+ "sourcesContent": ["/**\n * Coherent.js Forms - Validators\n *\n * Form validation utilities\n *\n * @module forms/validators\n */\n\nimport { validators as packageValidators } from './validation.js';\n\n/**\n * Built-in validators with signature: (value, options, translator, allValues) => errorMessage | null\n */\nexport const validators = {\n required: (value, options = {}) => {\n if (value === null || value === undefined || value === '') {\n return options.message || validators.required.message || 'This field is required';\n }\n return null;\n },\n\n email: (value) => {\n if (!value) return null;\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(value)) {\n return 'Please enter a valid email address';\n }\n return null;\n },\n\n minLength: (value, options = {}) => {\n if (!value) return null;\n const min = options.min || 0;\n if (value.length < min) {\n return options.message || `Must be at least ${min} characters`;\n }\n return null;\n },\n\n maxLength: (value, options = {}) => {\n if (!value) return null;\n const max = options.max || Infinity;\n if (value.length > max) {\n return options.message || `Must be no more than ${max} characters`;\n }\n return null;\n },\n\n min: (value, options = {}) => {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n const minValue = options.min || 0;\n if (isNaN(num) || num < minValue) {\n return options.message || `Must be at least ${minValue}`;\n }\n return null;\n },\n\n max: (value, options = {}) => {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n const maxValue = options.max || Infinity;\n if (isNaN(num) || num > maxValue) {\n return options.message || `Must be no more than ${maxValue}`;\n }\n return null;\n },\n\n pattern: (value, options = {}) => {\n if (!value) return null;\n const regex = options.pattern || options.regex;\n if (regex && !regex.test(value)) {\n return options.message || 'Invalid format';\n }\n return null;\n },\n\n url: (value) => {\n if (!value) return null;\n try {\n new URL(value);\n return null;\n } catch {\n return 'Please enter a valid URL';\n }\n },\n\n number: (value) => {\n if (value === null || value === undefined || value === '') return null;\n if (isNaN(Number(value))) {\n return 'Must be a valid number';\n }\n return null;\n },\n\n integer: (value) => {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n if (isNaN(num) || !Number.isInteger(num)) {\n return 'Must be a whole number';\n }\n return null;\n },\n\n phone: (value) => {\n if (!value) return null;\n const phoneRegex = /^[\\d\\s\\-\\+\\(\\)]+$/;\n if (!phoneRegex.test(value) || value.replace(/\\D/g, '').length < 10) {\n return 'Please enter a valid phone number';\n }\n return null;\n },\n\n date: (value) => {\n if (!value) return null;\n const date = new Date(value);\n if (isNaN(date.getTime())) {\n return 'Please enter a valid date';\n }\n return null;\n },\n\n match: (value, options = {}, translator, allValues = {}) => {\n if (!value) return null;\n const fieldName = options.field || options.fieldName;\n if (value !== allValues[fieldName]) {\n return options.message || `Must match ${fieldName}`;\n }\n return null;\n },\n\n custom: (value, options = {}, translator, allValues) => {\n const validatorFn = options.validator || options.fn;\n if (!validatorFn) return null;\n const isValid = validatorFn(value, allValues);\n return isValid ? null : (options.message || 'Validation failed');\n },\n\n fileType: (value, options = {}) => {\n if (!value) return null;\n \n const allowedTypes = options.accept || options.types || [];\n \n // Handle File object\n if (value.type !== undefined) {\n const fileType = value.type;\n const fileExt = value.name ? value.name.split('.').pop().toLowerCase() : '';\n \n // Check MIME type or extension\n const isValid = allowedTypes.some(type => {\n if (type.startsWith('.')) {\n return fileExt === type.slice(1).toLowerCase();\n }\n if (type.includes('/')) {\n if (type.endsWith('/*')) {\n return fileType.startsWith(type.replace('/*', '/'));\n }\n return fileType === type;\n }\n return fileExt === type.toLowerCase();\n });\n \n if (!isValid) {\n return options.message || `File type must be one of: ${allowedTypes.join(', ')}`;\n }\n return null;\n }\n \n return null;\n },\n\n fileSize: (value, options = {}) => {\n if (!value) return null;\n \n const maxSize = options.maxSize || Infinity;\n \n // Handle File object\n if (value.size !== undefined) {\n if (value.size > maxSize) {\n const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);\n return options.message || `File size must be less than ${maxSizeMB}MB`;\n }\n return null;\n }\n \n return null;\n },\n\n fileExtension: (value, options = {}) => {\n if (!value) return null;\n \n const allowedExtensions = options.extensions || [];\n const fileName = value.name || value;\n const ext = `.${ fileName.split('.').pop().toLowerCase()}`;\n \n const isValid = allowedExtensions.some(allowed => {\n return ext === allowed.toLowerCase();\n });\n \n if (!isValid) {\n return options.message || `File extension must be one of: ${allowedExtensions.join(', ')}`;\n }\n return null;\n },\n\n alpha: (value) => {\n if (!value) return null;\n const alphaRegex = /^[a-zA-Z]+$/;\n if (!alphaRegex.test(value)) {\n return 'Must contain only letters';\n }\n return null;\n },\n\n alphanumeric: (value) => {\n if (!value) return null;\n const alphanumericRegex = /^[a-zA-Z0-9]+$/;\n if (!alphanumericRegex.test(value)) {\n return 'Must contain only letters and numbers';\n }\n return null;\n },\n\n uppercase: (value) => {\n if (!value) return null;\n if (value !== value.toUpperCase()) {\n return 'Must be uppercase';\n }\n return null;\n },\n\n // Get a registered validator\n get: (name) => {\n return validators[name];\n },\n\n // Compose multiple validators\n compose: (validatorList) => {\n return (value, options, translator, allValues) => {\n for (const validator of validatorList) {\n const error = typeof validator === 'function'\n ? validator(value, options, translator, allValues)\n : null;\n if (error) {\n return error;\n }\n }\n return null;\n };\n },\n\n // Debounce async validator\n debounce: (validator, delay = 300) => {\n let timeoutId;\n return (value) => {\n return new Promise((resolve) => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(async () => {\n const result = await validator(value);\n resolve(result);\n }, delay);\n });\n };\n },\n\n // Cancellable async validator\n cancellable: (validator) => {\n let abortController;\n const wrapped = async (value) => {\n if (abortController) {\n abortController.abort();\n }\n // AbortController is a global browser/Node.js API\n abortController = typeof AbortController !== 'undefined' ? new AbortController() : null;\n try {\n return await validator(value, abortController ? abortController.signal : null);\n } catch (error) {\n if (error.name === 'AbortError') {\n return null;\n }\n throw error;\n }\n };\n wrapped.cancel = () => {\n if (abortController) {\n abortController.abort();\n }\n };\n return wrapped;\n },\n\n // Conditional validator\n when: (condition, validator) => {\n return (value, options = {}, translator, allValues = {}) => {\n // Pass options as context if it looks like context (has non-validator properties)\n const context = options.min !== undefined || options.max !== undefined ? allValues : options;\n const shouldValidate = typeof condition === 'function' \n ? condition(value, context) \n : condition;\n \n if (!shouldValidate) {\n return null;\n }\n \n return typeof validator === 'function'\n ? validator(value, options, translator, allValues)\n : null;\n };\n },\n\n // Validator chain builder\n chain: (options = {}) => {\n const validatorList = [];\n const stopOnFirstError = options.stopOnFirstError !== false;\n \n const chain = {\n required: (opts) => {\n validatorList.push((v, o, t, a) => validators.required(v, opts || o, t, a));\n return chain;\n },\n email: (opts) => {\n validatorList.push((v, o, t, a) => validators.email(v, opts || o, t, a));\n return chain;\n },\n minLength: (opts) => {\n validatorList.push((v, o, t, a) => validators.minLength(v, opts || o, t, a));\n return chain;\n },\n maxLength: (opts) => {\n validatorList.push((v, o, t, a) => validators.maxLength(v, opts || o, t, a));\n return chain;\n },\n custom: (fn, message) => {\n validatorList.push((v, o, t, a) => {\n // Custom validator returns null if valid, message if invalid\n const result = fn(v, a);\n return result === null || result === true || result === undefined ? null : (message || result);\n });\n return chain;\n },\n validate: (value, opts, translator, allValues) => {\n if (stopOnFirstError) {\n // Stop on first error - return single error or null\n for (const validator of validatorList) {\n const error = validator(value, opts, translator, allValues);\n if (error) {\n return error;\n }\n }\n return null;\n } else {\n // Collect all errors - return array or null\n const errors = [];\n for (const validator of validatorList) {\n const error = validator(value, opts, translator, allValues);\n if (error) {\n errors.push(error);\n }\n }\n return errors.length > 0 ? errors : null;\n }\n }\n };\n \n return chain;\n }\n};\n\n/**\n * Validate a single field\n */\nexport function validateField(value, validatorList, formData = {}) {\n for (const validator of validatorList) {\n const error = validator(value, formData);\n if (error) {\n return error;\n }\n }\n return null;\n}\n\n/**\n * Validate entire form\n */\nexport function validateForm(formData, fieldValidators) {\n const errors = {};\n \n for (const [fieldName, validatorList] of Object.entries(fieldValidators)) {\n const value = formData[fieldName];\n const error = validateField(value, validatorList, formData);\n if (error) {\n errors[fieldName] = error;\n }\n }\n \n return Object.keys(errors).length > 0 ? errors : null;\n}\n\n/**\n * Create a validator\n */\nexport function createValidator(validatorFn, message) {\n return (value, options, translator, allValues) => {\n const result = validatorFn(value, options, translator, allValues);\n // If validator returns a string, use it as the error message\n if (typeof result === 'string') {\n return result;\n }\n // If validator returns falsy (null, false, undefined), no error\n if (!result) {\n return null;\n }\n // If validator returns truthy (true, object, etc), use provided message\n return message || 'Validation failed';\n };\n}\n\n/**\n * Register a custom validator\n *\n * index.js re-exports validation.js's `validators`, which shadows this\n * module's object in the star export \u2014 so registering only here left\n * `validators[name]` undefined for every consumer. Register in both: the\n * package registry so callers can reach it, and the local one so `get()` and\n * `compose()` keep resolving it by name.\n */\nexport function registerValidator(name, validatorFn) {\n validators[name] = validatorFn;\n packageValidators[name] = validatorFn;\n}\n\n/**\n * Compose multiple validators\n */\nexport function composeValidators(...validatorFns) {\n return (value, options, translator, allValues) => {\n for (const validator of validatorFns) {\n const error = validator(value, options, translator, allValues);\n if (error) {\n return error;\n }\n }\n return null;\n };\n}\n\nexport default {\n validators,\n validateField,\n validateForm,\n createValidator,\n registerValidator,\n composeValidators\n};\n", "/**\n * Coherent.js Form Builder\n * \n * Utilities for building forms with Coherent.js\n * \n * @module forms/form-builder\n */\n\nimport { render as renderToHTML } from '@coherent.js/core';\n\n/**\n * Class applied to each structural slot. Consumers override any subset via\n * the `classNames` option; hydrateForm accepts `invalid` and `error` so the\n * client writes the same names the server rendered.\n */\nexport const DEFAULT_CLASS_NAMES = {\n /** Wrapper around label, control and error */\n field: 'form-field',\n label: '',\n /** Base class on the control, before any per-field className */\n control: '',\n /** Added to the control while it has a visible error */\n invalid: 'error',\n /** The error message element */\n error: 'error-message',\n submit: 'submit-button'\n};\n\n/**\n * Attribute names are interpolated into the markup unescaped by\n * formatAttributes, so a passthrough has to reject anything that is not a\n * plain name \u2014 otherwise `{'x onclick=alert(1)': ''}` injects an attribute.\n */\nconst VALID_ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;\n\n/**\n * `onclick` and friends are syntactically valid names, and a string value\n * renders as an inline handler \u2014 script execution from whatever produced the\n * field config, and the thing this builder stopped emitting on the form\n * itself. Handlers belong in hydration.\n */\nconst EVENT_HANDLER_NAME = /^on/i;\n\nfunction safeAttributes(attributes) {\n if (!attributes || typeof attributes !== 'object') return {};\n\n const safe = {};\n for (const [name, value] of Object.entries(attributes)) {\n if (!VALID_ATTRIBUTE_NAME.test(name)) {\n console.warn(`[coherent.js/forms] Ignoring invalid attribute name: ${JSON.stringify(name)}`);\n continue;\n }\n if (EVENT_HANDLER_NAME.test(name)) {\n console.warn(\n `[coherent.js/forms] Ignoring inline event handler \"${name}\". ` +\n 'Attach handlers with hydrateForm instead.'\n );\n continue;\n }\n safe[name] = value;\n }\n return safe;\n}\n\n/** Join class names, dropping empties so no element carries `class=\"\"`. */\nfunction joinClasses(...names) {\n return names.filter(Boolean).join(' ');\n}\n\n/**\n * Form Builder\n * Helps create form components with validation\n */\nexport class FormBuilder {\n constructor(options = {}) {\n this.options = {\n validateOnChange: true,\n validateOnBlur: true,\n name: options.name || 'form',\n ...options\n };\n \n this.fields = new Map();\n this.groups = new Map();\n this.values = {};\n this.errors = {};\n this.touched = {};\n this.initialValues = {};\n this.submitHandler = null;\n this.errorHandler = null;\n this._isSubmitting = false;\n }\n\n /**\n * Add a field to the form (alias for field)\n */\n addField(name, config = {}) {\n return this.field(name, config);\n }\n\n /**\n * Add a field to the form\n */\n field(name, config = {}) {\n const fieldConfig = {\n name,\n type: config.type || 'text',\n label: config.label || name,\n placeholder: config.placeholder || '',\n defaultValue: config.defaultValue || '',\n validators: config.validators || [],\n required: config.required || false,\n visible: config.visible !== false,\n showWhen: config.showWhen,\n ...config\n };\n \n this.fields.set(name, fieldConfig);\n\n // Set default value\n if (config.defaultValue !== undefined) {\n this.values[name] = config.defaultValue;\n this.initialValues[name] = config.defaultValue;\n }\n\n return this;\n }\n\n /**\n * Remove a field from the form\n */\n removeField(name) {\n this.fields.delete(name);\n delete this.values[name];\n delete this.errors[name];\n delete this.touched[name];\n return this;\n }\n\n /**\n * Update field configuration\n */\n updateField(name, config) {\n const field = this.fields.get(name);\n if (field) {\n this.fields.set(name, { ...field, ...config });\n }\n return this;\n }\n\n /**\n * Get all fields as array\n */\n getFields() {\n return Array.from(this.fields.values());\n }\n\n /**\n * Add a field group\n */\n addGroup(name, config = {}) {\n this.groups.set(name, {\n name,\n label: config.label || name,\n fields: config.fields || [],\n ...config\n });\n\n // Add fields in the group\n if (config.fields) {\n config.fields.forEach(fieldConfig => {\n this.addField(fieldConfig.name, fieldConfig);\n });\n }\n\n return this;\n }\n\n /**\n * Get field configuration\n */\n getField(name) {\n return this.fields.get(name);\n }\n\n /**\n * Set field value\n */\n setValue(name, value) {\n this.values[name] = value;\n this.touched[name] = true;\n\n const field = this.fields.get(name);\n if (field && (field.validateOnChange || this.options.validateOnChange)) {\n this.validateField(name);\n }\n }\n\n /**\n * Set multiple values\n */\n setValues(values) {\n Object.assign(this.values, values);\n return this;\n }\n\n /**\n * Get field value\n */\n getValue(name) {\n return this.values[name];\n }\n\n /**\n * Get all values\n */\n getValues() {\n return { ...this.values };\n }\n\n /**\n * Get field error\n */\n getFieldError(name) {\n return this.errors[name];\n }\n\n /**\n * Check if form has errors\n */\n hasErrors() {\n return Object.keys(this.errors).length > 0;\n }\n\n /**\n * Clear all errors\n */\n clearErrors() {\n this.errors = {};\n return this;\n }\n\n /**\n * Check if form is dirty (values changed from initial)\n */\n isDirty() {\n return Object.keys(this.values).some(key => {\n return this.values[key] !== this.initialValues[key];\n });\n }\n\n /**\n * Check if form is valid\n */\n isValid() {\n const result = this.validate();\n return Object.keys(result).length === 0;\n }\n\n /**\n * Validate a field\n */\n validateField(name) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n // Skip validation for hidden fields (support both showWhen and showIf)\n const showCondition = field.showWhen || field.showIf;\n if (showCondition && !showCondition(this.values)) {\n delete this.errors[name];\n return null;\n }\n\n const value = this.values[name];\n \n // Check required\n if (field.required && (value === undefined || value === null || value === '')) {\n const error = 'This field is required';\n this.errors[name] = error;\n return error;\n }\n\n // Skip further validation if empty and not required\n if (!value && !field.required) {\n delete this.errors[name];\n return null;\n }\n\n // Type-based validation\n if (value) {\n if (field.type === 'email') {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(value)) {\n const error = 'Please enter a valid email address';\n this.errors[name] = error;\n return error;\n }\n } else if (field.type === 'url') {\n try {\n new URL(value);\n } catch {\n const error = 'Please enter a valid URL';\n this.errors[name] = error;\n return error;\n }\n } else if (field.type === 'number') {\n if (isNaN(Number(value))) {\n const error = 'Please enter a valid number';\n this.errors[name] = error;\n return error;\n }\n }\n }\n\n // Check custom validate function\n if (field.validate) {\n const error = field.validate(value, this.values);\n if (error) {\n this.errors[name] = error;\n return error;\n }\n }\n\n // Run validators\n for (const validator of field.validators || []) {\n const error = validator(value, this.values);\n if (error) {\n this.errors[name] = error;\n return error;\n }\n }\n\n delete this.errors[name];\n return null;\n }\n\n /**\n * Validate all fields\n */\n validate() {\n const errors = {};\n\n for (const [name] of this.fields) {\n // The same predicate buildForm() renders by, so a field that is not on\n // the page can never block submission.\n if (!this.isFieldVisible(name)) continue;\n\n const error = this.validateField(name);\n if (error) {\n errors[name] = error;\n }\n }\n\n this.errors = errors;\n return errors;\n }\n\n /**\n * Set submit handler\n */\n onSubmit(handler) {\n this.submitHandler = handler;\n return this;\n }\n\n /**\n * Set error handler\n */\n onError(handler) {\n this.errorHandler = handler;\n return this;\n }\n\n /**\n * Submit the form\n */\n async submit() {\n const errors = this.validate();\n \n if (Object.keys(errors).length > 0) {\n return { success: false, errors };\n }\n\n if (!this.submitHandler) {\n return { success: true, data: this.values };\n }\n\n this._isSubmitting = true;\n\n try {\n const result = await this.submitHandler(this.values);\n this._isSubmitting = false;\n return { success: true, data: result };\n } catch (error) {\n this._isSubmitting = false;\n if (this.errorHandler) {\n this.errorHandler(error);\n }\n return { success: false, error };\n }\n }\n\n /**\n * Serialize form data\n */\n serialize() {\n return { ...this.values };\n }\n\n /**\n * Convert form to HTML string\n */\n toHTML(options = {}) {\n // Delegates to buildForm() so there is one source of truth. The previous\n // hand-rolled string builder dropped action, method and every field\n // attribute beyond type/name/id, and interpolated values unescaped.\n return renderToHTML(this.buildForm(options));\n }\n\n /**\n * Mark field as touched\n */\n touch(name) {\n this.touched[name] = true;\n }\n\n /**\n * Build input component with validation metadata for hydration\n */\n buildInput(name, classNames = this.resolveClassNames()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const value = this.values[name] || '';\n const error = this.errors[name];\n const isTouched = this.touched[name];\n\n // Build validator names string for data-validators attribute\n const validatorNames = field.validators\n .map(v => {\n if (typeof v === 'function') return v.name || 'custom';\n if (typeof v === 'string') return v;\n return null;\n })\n .filter(Boolean)\n .join(',');\n\n const controlClass = joinClasses(\n classNames.control,\n field.className,\n error && isTouched ? classNames.invalid : null\n );\n\n const inputProps = {\n // Spread first: name, id, type and the aria-* pair below are the\n // builder's own invariants, and hydration keys off them.\n ...safeAttributes(field.attributes),\n type: field.type,\n name: field.name,\n id: field.name,\n value: value,\n 'aria-invalid': error ? 'true' : 'false',\n 'aria-describedby': error ? `${name}-error` : undefined\n };\n\n if (field.placeholder) inputProps.placeholder = field.placeholder;\n if (controlClass) inputProps.className = controlClass;\n if (field.disabled) inputProps.disabled = true;\n if (field.readonly) inputProps.readonly = true;\n\n // Add validation metadata for client-side hydration\n if (field.required) {\n inputProps.required = true;\n inputProps['data-required'] = 'true';\n }\n\n if (validatorNames) {\n inputProps['data-validators'] = validatorNames;\n }\n\n // Note: Event handlers are attached during hydration, not inline\n // This enables progressive enhancement and CSP compliance\n\n // textarea and select are elements, not input types. `type=\"textarea\"` is\n // not valid HTML \u2014 browsers render it as a single-line text box.\n if (field.type === 'textarea' || field.type === 'select') {\n const { type: _type, value: _value, ...rest } = inputProps;\n const props = { ...rest };\n\n if (field.type === 'textarea') {\n return { textarea: { ...props, text: String(value) } };\n }\n\n const { placeholder: _placeholder, ...selectProps } = props;\n\n return {\n select: {\n ...selectProps,\n children: (field.options ?? []).map(option => {\n const { value: optionValue, label = optionValue } =\n typeof option === 'object' && option !== null ? option : { value: option };\n\n return {\n option: {\n value: optionValue,\n selected: String(optionValue) === String(value) || undefined,\n text: String(label)\n }\n };\n })\n }\n };\n }\n\n return {\n input: inputProps\n };\n }\n\n /**\n * Build label component\n */\n buildLabel(name, classNames = this.resolveClassNames()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const props = { for: field.name, text: field.label };\n if (classNames.label) props.className = classNames.label;\n\n return { label: props };\n }\n\n /**\n * Build error component\n */\n buildError(name, classNames = this.resolveClassNames()) {\n const error = this.errors[name];\n const isTouched = this.touched[name];\n\n if (!error || !isTouched) return null;\n\n const props = { id: `${name}-error`, role: 'alert', text: error };\n if (classNames.error) props.className = classNames.error;\n\n return { div: props };\n }\n\n /**\n * Merge configured class names over the defaults\n */\n resolveClassNames(overrides = {}) {\n return { ...DEFAULT_CLASS_NAMES, ...this.options.classNames, ...overrides };\n }\n\n /**\n * Build complete field component\n */\n buildField(name, classNames = this.resolveClassNames()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const children = [\n this.buildLabel(name, classNames),\n this.buildInput(name, classNames)\n ];\n\n const error = this.buildError(name, classNames);\n if (error) {\n children.push(error);\n }\n\n // data-field is the structural hook hydrateForm uses to find the wrapper,\n // so classes stay entirely the consumer's to choose.\n const props = { 'data-field': name, children };\n if (classNames.field) props.className = classNames.field;\n\n return { div: props };\n }\n\n /**\n * Build entire form\n */\n buildForm(options = {}) {\n const settings = { ...this.options, ...options };\n const classNames = this.resolveClassNames(options.classNames);\n const fields = [];\n\n for (const [name] of this.fields) {\n // validate() has always skipped fields hidden by showWhen/showIf; render\n // agreed with it only by accident, because nothing was ever hidden.\n if (!this.isFieldVisible(name)) continue;\n fields.push(this.buildField(name, classNames));\n }\n\n if (settings.submitButton !== false) {\n const button = { type: 'submit', text: settings.submitText || 'Submit' };\n if (classNames.submit) button.className = classNames.submit;\n fields.push({ button });\n }\n\n const form = {};\n\n if (settings.action) form.action = settings.action;\n if (settings.method) form.method = settings.method;\n if (settings.name) form.name = settings.name;\n if (settings.id) form.id = settings.id;\n if (settings.className) form.className = settings.className;\n if (settings.enctype) form.enctype = settings.enctype;\n\n // Plain HTML by default: the form posts to `action` and the browser runs\n // its own validation with JavaScript off. hydrateForm binds its own submit\n // listener, so it never needed the inline handler this used to emit \u2014 and\n // an inline handler breaks under a strict CSP besides.\n if (settings.enhance) {\n form.onsubmit = typeof settings.enhance === 'string'\n ? settings.enhance\n : 'handleSubmit(event)';\n }\n\n if (settings.novalidate === true) form.novalidate = true;\n\n form.children = fields;\n\n return { form };\n }\n\n /**\n * Set the form action URL\n */\n setAction(action) {\n this.options.action = action;\n return this;\n }\n\n /**\n * Set the form submission method\n */\n setMethod(method) {\n this.options.method = method;\n return this;\n }\n\n /**\n * Build the form component (alias for buildForm)\n */\n build(options = {}) {\n return this.buildForm(options);\n }\n\n /**\n * Render the form to a component (alias for buildForm)\n */\n render(options = {}) {\n return this.buildForm(options);\n }\n\n /**\n * Check if form is currently submitting\n */\n isSubmitting() {\n return this._isSubmitting;\n }\n\n /**\n * Get a field group\n */\n getGroup(name) {\n return this.groups.get(name);\n }\n\n /**\n * Check if a field is visible\n */\n isFieldVisible(name) {\n const field = this.fields.get(name);\n if (!field) return false;\n\n // visible:false wins outright \u2014 a showWhen that happens to return true\n // must not resurrect a field the caller switched off.\n if (field.visible === false) return false;\n\n // Support both showWhen and showIf\n const showCondition = field.showWhen || field.showIf;\n if (showCondition) {\n return showCondition(this.values);\n }\n\n return true;\n }\n\n /**\n * Reset form\n */\n reset() {\n // Reset to initial values or empty strings\n this.values = {};\n for (const [name, field] of this.fields) {\n if (field.defaultValue !== undefined) {\n this.values[name] = field.defaultValue;\n } else {\n this.values[name] = '';\n }\n }\n this.errors = {};\n this.touched = {};\n this._isSubmitting = false;\n return this;\n }\n}\n\n/**\n * Create a form builder\n */\nexport function createFormBuilder(options = {}) {\n const form = new FormBuilder(options);\n\n // Add fields if provided\n if (options.fields) {\n options.fields.forEach(fieldConfig => {\n form.addField(fieldConfig.name, fieldConfig);\n });\n }\n\n return form;\n}\n\n/**\n * Build a form component from a configuration object.\n *\n * Returns a renderable component, as the type declarations have always said.\n * It previously returned the FormBuilder itself, so `render(buildForm(...))`\n * threw \"Invalid component structure\". Use createFormBuilder() when you want\n * the builder.\n *\n * @param {Object} config - Form configuration; `fields` may be an array of\n * field objects or an object keyed by field name. Remaining keys (action,\n * method, name, className, enctype, submitText) configure the form element.\n * @returns {Object} A Coherent.js component\n */\nexport function buildForm(config = {}) {\n const { fields = [], ...options } = Array.isArray(config) ? { fields: config } : config;\n const builder = new FormBuilder(options);\n\n if (Array.isArray(fields)) {\n for (const field of fields) {\n if (field && field.name) builder.field(field.name, field);\n }\n } else {\n for (const [name, field] of Object.entries(fields)) {\n builder.field(name, field);\n }\n }\n\n return builder.buildForm(options);\n}\n\nexport default {\n FormBuilder,\n createFormBuilder,\n buildForm\n};\n", "/**\n * Form Hydration for Coherent.js\n *\n * Progressive enhancement for server-rendered forms\n * Reads validation metadata from HTML and attaches client-side behavior\n *\n * @module forms/form-hydration\n */\n\nimport { validators } from './validators.js';\nimport { DEFAULT_CLASS_NAMES } from './form-builder.js';\n\n/**\n * Hydrate a server-rendered form with client-side validation and behavior\n *\n * @param {string|HTMLFormElement} formSelector - Form selector or element\n * @param {Object} options - Hydration options\n * @returns {Object} Form controller\n */\nexport function hydrateForm(formSelector, options = {}) {\n // Browser-only check\n if (typeof document === 'undefined') {\n console.warn('hydrateForm can only run in browser environment');\n return null;\n }\n\n const form = typeof formSelector === 'string'\n ? document.querySelector(formSelector)\n : formSelector;\n\n if (!form) {\n console.warn(`Form not found: ${formSelector}`);\n return null;\n }\n\n const opts = {\n validateOnBlur: true,\n validateOnChange: false,\n validateOnSubmit: true,\n showErrorsOnTouch: true,\n debounce: 300,\n ...options,\n // Must match whatever the server rendered, so pass the same names given to\n // buildForm's `classNames` when they were customised.\n classNames: {\n invalid: DEFAULT_CLASS_NAMES.invalid,\n error: DEFAULT_CLASS_NAMES.error,\n ...options.classNames\n }\n };\n\n // Form state\n const state = {\n values: {},\n errors: {},\n touched: {},\n isSubmitting: false,\n fields: new Map()\n };\n\n // Debounce timers\n const debounceTimers = new Map();\n\n /**\n * Parse validators from data-validators attribute\n */\n function parseValidators(validatorString) {\n if (!validatorString) return [];\n\n return validatorString.split(',').map(v => {\n const trimmed = v.trim();\n\n // Handle validators with parameters: minLength:8\n const [name, ...params] = trimmed.split(':');\n\n if (validators[name]) {\n return params.length > 0\n ? validators[name](...params.map(p => isNaN(p) ? p : Number(p)))\n : validators[name];\n }\n\n return null;\n }).filter(Boolean);\n }\n\n /**\n * Discover and register fields from form HTML\n */\n function discoverFields() {\n const inputs = form.querySelectorAll('[name]');\n\n inputs.forEach(input => {\n const name = input.getAttribute('name');\n const field = {\n name,\n element: input,\n type: input.getAttribute('type') || 'text',\n required: input.hasAttribute('required') || input.dataset.required === 'true',\n validators: parseValidators(input.dataset.validators),\n errorElement: null\n };\n\n // Find or create error display element\n const errorId = `${name}-error`;\n field.errorElement = document.getElementById(errorId) || createErrorElement(name, input);\n\n state.fields.set(name, field);\n state.values[name] = getFieldValue(input);\n state.touched[name] = false;\n state.errors[name] = null;\n });\n }\n\n /**\n * Create error display element\n */\n function createErrorElement(name, inputElement) {\n const errorDiv = document.createElement('div');\n errorDiv.id = `${name}-error`;\n if (opts.classNames.error) errorDiv.className = opts.classNames.error;\n errorDiv.setAttribute('role', 'alert');\n errorDiv.style.display = 'none';\n\n // Insert after input or its parent field wrapper. Keyed off data-field\n // rather than a class, so consumers can name the wrapper what they like.\n const fieldWrapper = inputElement.closest('[data-field]') || inputElement.parentElement;\n fieldWrapper.appendChild(errorDiv);\n\n return errorDiv;\n }\n\n /**\n * Get field value based on input type\n */\n function getFieldValue(input) {\n if (input.type === 'checkbox') {\n return input.checked;\n } else if (input.type === 'radio') {\n const checked = form.querySelector(`[name=\"${input.name}\"]:checked`);\n return checked ? checked.value : null;\n } else {\n return input.value;\n }\n }\n\n /**\n * Set field value\n */\n function setFieldValue(name, value) {\n const field = state.fields.get(name);\n if (!field) return;\n\n const { element } = field;\n\n if (element.type === 'checkbox') {\n element.checked = Boolean(value);\n } else if (element.type === 'radio') {\n const radio = form.querySelector(`[name=\"${name}\"][value=\"${value}\"]`);\n if (radio) radio.checked = true;\n } else {\n element.value = value;\n }\n\n state.values[name] = value;\n }\n\n /**\n * Validate a single field\n */\n function validateField(name) {\n const field = state.fields.get(name);\n if (!field) return true;\n\n const value = state.values[name];\n let error = null;\n\n // Required validation\n if (field.required && (value === null || value === undefined || value === '')) {\n error = 'This field is required';\n }\n\n // Run custom validators\n if (!error && field.validators.length > 0) {\n for (const validator of field.validators) {\n const result = validator.validate\n ? validator.validate(value, state.values)\n : validator(value, state.values);\n\n if (result !== true && result !== undefined && result !== null) {\n error = validator.message || result || 'Validation failed';\n break;\n }\n }\n }\n\n state.errors[name] = error;\n displayError(name, error);\n\n return !error;\n }\n\n /**\n * The invalid class as individual tokens.\n *\n * classList.add/remove take one token each \u2014 passing `'is-invalid has-error'`\n * throws InvalidCharacterError.\n */\n function invalidClasses() {\n return opts.classNames.invalid ? opts.classNames.invalid.trim().split(/\\s+/) : [];\n }\n\n /**\n * Display error message\n */\n function displayError(name, error) {\n const field = state.fields.get(name);\n if (!field) return;\n\n const { element, errorElement } = field;\n\n if (error && state.touched[name] && opts.showErrorsOnTouch) {\n // Show error\n errorElement.textContent = error;\n errorElement.style.display = 'block';\n element.setAttribute('aria-invalid', 'true');\n invalidClasses().forEach(name => element.classList.add(name));\n } else {\n // Hide error\n errorElement.textContent = '';\n errorElement.style.display = 'none';\n element.setAttribute('aria-invalid', 'false');\n invalidClasses().forEach(name => element.classList.remove(name));\n }\n }\n\n /**\n * Validate entire form\n */\n function validateForm() {\n let isValid = true;\n\n for (const name of state.fields.keys()) {\n const fieldValid = validateField(name);\n if (!fieldValid) isValid = false;\n }\n\n return isValid;\n }\n\n /**\n * Handle input change\n */\n function handleChange(event) {\n const input = event.target;\n const name = input.getAttribute('name');\n\n if (!state.fields.has(name)) return;\n\n state.values[name] = getFieldValue(input);\n\n if (opts.validateOnChange) {\n // Debounce validation\n if (debounceTimers.has(name)) {\n clearTimeout(debounceTimers.get(name));\n }\n\n const timer = setTimeout(() => {\n validateField(name);\n debounceTimers.delete(name);\n }, opts.debounce);\n\n debounceTimers.set(name, timer);\n }\n }\n\n /**\n * Handle input blur\n */\n function handleBlur(event) {\n const input = event.target;\n const name = input.getAttribute('name');\n\n if (!state.fields.has(name)) return;\n\n state.touched[name] = true;\n\n if (opts.validateOnBlur) {\n validateField(name);\n }\n }\n\n /**\n * Handle form submission\n */\n function handleSubmit(event) {\n event.preventDefault();\n\n // Mark all fields as touched\n for (const name of state.fields.keys()) {\n state.touched[name] = true;\n }\n\n const isValid = validateForm();\n\n if (!isValid) {\n // Focus first error field\n const firstErrorField = Array.from(state.fields.values())\n .find(field => state.errors[field.name]);\n\n if (firstErrorField) {\n firstErrorField.element.focus();\n }\n\n // Call onError callback\n if (options.onError) {\n options.onError(state.errors);\n }\n\n return;\n }\n\n // Form is valid, prepare submission\n state.isSubmitting = true;\n\n const submitData = { ...state.values };\n\n // Call onSubmit callback\n if (options.onSubmit) {\n const result = options.onSubmit(submitData, event);\n\n // If onSubmit returns false, don't submit\n if (result === false) {\n state.isSubmitting = false;\n return;\n }\n\n // If onSubmit returns a promise, wait for it\n if (result && typeof result.then === 'function') {\n result\n .then(() => {\n state.isSubmitting = false;\n if (options.onSuccess) {\n options.onSuccess(submitData);\n }\n })\n .catch(error => {\n state.isSubmitting = false;\n if (options.onError) {\n options.onError(error);\n }\n });\n return;\n }\n }\n\n // Default: submit the form normally\n if (!options.onSubmit) {\n form.submit();\n }\n\n state.isSubmitting = false;\n }\n\n /**\n * Attach event listeners\n */\n function attachEventListeners() {\n // Input change events\n state.fields.forEach(field => {\n field.element.addEventListener('input', handleChange);\n field.element.addEventListener('blur', handleBlur);\n });\n\n // Form submit\n form.addEventListener('submit', handleSubmit);\n }\n\n /**\n * Detach event listeners (cleanup)\n */\n function detachEventListeners() {\n state.fields.forEach(field => {\n field.element.removeEventListener('input', handleChange);\n field.element.removeEventListener('blur', handleBlur);\n });\n\n form.removeEventListener('submit', handleSubmit);\n\n // Clear debounce timers\n debounceTimers.forEach(timer => clearTimeout(timer));\n debounceTimers.clear();\n }\n\n /**\n * Reset form to initial state\n */\n function reset() {\n state.fields.forEach(field => {\n setFieldValue(field.name, '');\n state.touched[field.name] = false;\n state.errors[field.name] = null;\n displayError(field.name, null);\n });\n\n state.isSubmitting = false;\n form.reset();\n }\n\n // Initialize\n discoverFields();\n attachEventListeners();\n\n // Public API\n return {\n validateField,\n validateForm,\n setFieldValue,\n getFieldValue: (name) => state.values[name],\n getError: (name) => state.errors[name],\n getErrors: () => ({ ...state.errors }),\n getValues: () => ({ ...state.values }),\n setTouched: (name, touched = true) => {\n state.touched[name] = touched;\n },\n reset,\n destroy: detachEventListeners,\n isValid: () => Object.values(state.errors).every(e => !e),\n isSubmitting: () => state.isSubmitting,\n getState: () => ({\n values: { ...state.values },\n errors: { ...state.errors },\n touched: { ...state.touched },\n isSubmitting: state.isSubmitting\n })\n };\n}\n\nexport default hydrateForm;\n"],
5
+ "mappings": "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",
6
+ "names": ["name"]
7
7
  }
package/dist/index.js CHANGED
@@ -1,5 +1,40 @@
1
1
  // src/form-builder.js
2
2
  import { render as renderToHTML } from "@coherent.js/core";
3
+ var DEFAULT_CLASS_NAMES = {
4
+ /** Wrapper around label, control and error */
5
+ field: "form-field",
6
+ label: "",
7
+ /** Base class on the control, before any per-field className */
8
+ control: "",
9
+ /** Added to the control while it has a visible error */
10
+ invalid: "error",
11
+ /** The error message element */
12
+ error: "error-message",
13
+ submit: "submit-button"
14
+ };
15
+ var VALID_ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;
16
+ var EVENT_HANDLER_NAME = /^on/i;
17
+ function safeAttributes(attributes) {
18
+ if (!attributes || typeof attributes !== "object") return {};
19
+ const safe = {};
20
+ for (const [name, value] of Object.entries(attributes)) {
21
+ if (!VALID_ATTRIBUTE_NAME.test(name)) {
22
+ console.warn(`[coherent.js/forms] Ignoring invalid attribute name: ${JSON.stringify(name)}`);
23
+ continue;
24
+ }
25
+ if (EVENT_HANDLER_NAME.test(name)) {
26
+ console.warn(
27
+ `[coherent.js/forms] Ignoring inline event handler "${name}". Attach handlers with hydrateForm instead.`
28
+ );
29
+ continue;
30
+ }
31
+ safe[name] = value;
32
+ }
33
+ return safe;
34
+ }
35
+ function joinClasses(...names) {
36
+ return names.filter(Boolean).join(" ");
37
+ }
3
38
  var FormBuilder = class {
4
39
  constructor(options = {}) {
5
40
  this.options = {
@@ -227,11 +262,8 @@ var FormBuilder = class {
227
262
  */
228
263
  validate() {
229
264
  const errors = {};
230
- for (const [name, field] of this.fields) {
231
- const showCondition = field.showWhen || field.showIf;
232
- if (showCondition && !showCondition(this.values)) {
233
- continue;
234
- }
265
+ for (const [name] of this.fields) {
266
+ if (!this.isFieldVisible(name)) continue;
235
267
  const error = this.validateField(name);
236
268
  if (error) {
237
269
  errors[name] = error;
@@ -299,7 +331,7 @@ var FormBuilder = class {
299
331
  /**
300
332
  * Build input component with validation metadata for hydration
301
333
  */
302
- buildInput(name) {
334
+ buildInput(name, classNames = this.resolveClassNames()) {
303
335
  const field = this.fields.get(name);
304
336
  if (!field) return null;
305
337
  const value = this.values[name] || "";
@@ -310,16 +342,26 @@ var FormBuilder = class {
310
342
  if (typeof v === "string") return v;
311
343
  return null;
312
344
  }).filter(Boolean).join(",");
345
+ const controlClass = joinClasses(
346
+ classNames.control,
347
+ field.className,
348
+ error && isTouched ? classNames.invalid : null
349
+ );
313
350
  const inputProps = {
351
+ // Spread first: name, id, type and the aria-* pair below are the
352
+ // builder's own invariants, and hydration keys off them.
353
+ ...safeAttributes(field.attributes),
314
354
  type: field.type,
315
355
  name: field.name,
316
356
  id: field.name,
317
357
  value,
318
- placeholder: field.placeholder,
319
358
  "aria-invalid": error ? "true" : "false",
320
- "aria-describedby": error ? `${name}-error` : void 0,
321
- className: error && isTouched ? "error" : ""
359
+ "aria-describedby": error ? `${name}-error` : void 0
322
360
  };
361
+ if (field.placeholder) inputProps.placeholder = field.placeholder;
362
+ if (controlClass) inputProps.className = controlClass;
363
+ if (field.disabled) inputProps.disabled = true;
364
+ if (field.readonly) inputProps.readonly = true;
323
365
  if (field.required) {
324
366
  inputProps.required = true;
325
367
  inputProps["data-required"] = "true";
@@ -357,71 +399,63 @@ var FormBuilder = class {
357
399
  /**
358
400
  * Build label component
359
401
  */
360
- buildLabel(name) {
402
+ buildLabel(name, classNames = this.resolveClassNames()) {
361
403
  const field = this.fields.get(name);
362
404
  if (!field) return null;
363
- return {
364
- label: {
365
- for: field.name,
366
- text: field.label
367
- }
368
- };
405
+ const props = { for: field.name, text: field.label };
406
+ if (classNames.label) props.className = classNames.label;
407
+ return { label: props };
369
408
  }
370
409
  /**
371
410
  * Build error component
372
411
  */
373
- buildError(name) {
412
+ buildError(name, classNames = this.resolveClassNames()) {
374
413
  const error = this.errors[name];
375
414
  const isTouched = this.touched[name];
376
415
  if (!error || !isTouched) return null;
377
- return {
378
- div: {
379
- id: `${name}-error`,
380
- className: "error-message",
381
- role: "alert",
382
- text: error
383
- }
384
- };
416
+ const props = { id: `${name}-error`, role: "alert", text: error };
417
+ if (classNames.error) props.className = classNames.error;
418
+ return { div: props };
419
+ }
420
+ /**
421
+ * Merge configured class names over the defaults
422
+ */
423
+ resolveClassNames(overrides = {}) {
424
+ return { ...DEFAULT_CLASS_NAMES, ...this.options.classNames, ...overrides };
385
425
  }
386
426
  /**
387
427
  * Build complete field component
388
428
  */
389
- buildField(name) {
429
+ buildField(name, classNames = this.resolveClassNames()) {
390
430
  const field = this.fields.get(name);
391
431
  if (!field) return null;
392
432
  const children = [
393
- this.buildLabel(name),
394
- this.buildInput(name)
433
+ this.buildLabel(name, classNames),
434
+ this.buildInput(name, classNames)
395
435
  ];
396
- const error = this.buildError(name);
436
+ const error = this.buildError(name, classNames);
397
437
  if (error) {
398
438
  children.push(error);
399
439
  }
400
- return {
401
- div: {
402
- className: "form-field",
403
- "data-field": name,
404
- children
405
- }
406
- };
440
+ const props = { "data-field": name, children };
441
+ if (classNames.field) props.className = classNames.field;
442
+ return { div: props };
407
443
  }
408
444
  /**
409
445
  * Build entire form
410
446
  */
411
447
  buildForm(options = {}) {
448
+ const settings = { ...this.options, ...options };
449
+ const classNames = this.resolveClassNames(options.classNames);
412
450
  const fields = [];
413
451
  for (const [name] of this.fields) {
414
- fields.push(this.buildField(name));
452
+ if (!this.isFieldVisible(name)) continue;
453
+ fields.push(this.buildField(name, classNames));
415
454
  }
416
- const settings = { ...this.options, ...options };
417
455
  if (settings.submitButton !== false) {
418
- fields.push({
419
- button: {
420
- type: "submit",
421
- text: settings.submitText || "Submit",
422
- className: "submit-button"
423
- }
424
- });
456
+ const button = { type: "submit", text: settings.submitText || "Submit" };
457
+ if (classNames.submit) button.className = classNames.submit;
458
+ fields.push({ button });
425
459
  }
426
460
  const form = {};
427
461
  if (settings.action) form.action = settings.action;
@@ -430,8 +464,10 @@ var FormBuilder = class {
430
464
  if (settings.id) form.id = settings.id;
431
465
  if (settings.className) form.className = settings.className;
432
466
  if (settings.enctype) form.enctype = settings.enctype;
433
- form.onsubmit = "handleSubmit(event)";
434
- form.novalidate = true;
467
+ if (settings.enhance) {
468
+ form.onsubmit = typeof settings.enhance === "string" ? settings.enhance : "handleSubmit(event)";
469
+ }
470
+ if (settings.novalidate === true) form.novalidate = true;
435
471
  form.children = fields;
436
472
  return { form };
437
473
  }
@@ -479,11 +515,12 @@ var FormBuilder = class {
479
515
  isFieldVisible(name) {
480
516
  const field = this.fields.get(name);
481
517
  if (!field) return false;
518
+ if (field.visible === false) return false;
482
519
  const showCondition = field.showWhen || field.showIf;
483
520
  if (showCondition) {
484
521
  return showCondition(this.values);
485
522
  }
486
- return field.visible !== false;
523
+ return true;
487
524
  }
488
525
  /**
489
526
  * Reset form
@@ -1054,7 +1091,14 @@ function hydrateForm(formSelector, options = {}) {
1054
1091
  validateOnSubmit: true,
1055
1092
  showErrorsOnTouch: true,
1056
1093
  debounce: 300,
1057
- ...options
1094
+ ...options,
1095
+ // Must match whatever the server rendered, so pass the same names given to
1096
+ // buildForm's `classNames` when they were customised.
1097
+ classNames: {
1098
+ invalid: DEFAULT_CLASS_NAMES.invalid,
1099
+ error: DEFAULT_CLASS_NAMES.error,
1100
+ ...options.classNames
1101
+ }
1058
1102
  };
1059
1103
  const state = {
1060
1104
  values: {},
@@ -1098,10 +1142,10 @@ function hydrateForm(formSelector, options = {}) {
1098
1142
  function createErrorElement(name, inputElement) {
1099
1143
  const errorDiv = document.createElement("div");
1100
1144
  errorDiv.id = `${name}-error`;
1101
- errorDiv.className = "error-message";
1145
+ if (opts.classNames.error) errorDiv.className = opts.classNames.error;
1102
1146
  errorDiv.setAttribute("role", "alert");
1103
1147
  errorDiv.style.display = "none";
1104
- const fieldWrapper = inputElement.closest(".form-field") || inputElement.parentElement;
1148
+ const fieldWrapper = inputElement.closest("[data-field]") || inputElement.parentElement;
1105
1149
  fieldWrapper.appendChild(errorDiv);
1106
1150
  return errorDiv;
1107
1151
  }
@@ -1150,6 +1194,9 @@ function hydrateForm(formSelector, options = {}) {
1150
1194
  displayError(name, error);
1151
1195
  return !error;
1152
1196
  }
1197
+ function invalidClasses() {
1198
+ return opts.classNames.invalid ? opts.classNames.invalid.trim().split(/\s+/) : [];
1199
+ }
1153
1200
  function displayError(name, error) {
1154
1201
  const field = state.fields.get(name);
1155
1202
  if (!field) return;
@@ -1158,12 +1205,12 @@ function hydrateForm(formSelector, options = {}) {
1158
1205
  errorElement.textContent = error;
1159
1206
  errorElement.style.display = "block";
1160
1207
  element.setAttribute("aria-invalid", "true");
1161
- element.classList.add("error");
1208
+ invalidClasses().forEach((name2) => element.classList.add(name2));
1162
1209
  } else {
1163
1210
  errorElement.textContent = "";
1164
1211
  errorElement.style.display = "none";
1165
1212
  element.setAttribute("aria-invalid", "false");
1166
- element.classList.remove("error");
1213
+ invalidClasses().forEach((name2) => element.classList.remove(name2));
1167
1214
  }
1168
1215
  }
1169
1216
  function validateForm2() {
@@ -1295,6 +1342,7 @@ function hydrateForm(formSelector, options = {}) {
1295
1342
  };
1296
1343
  }
1297
1344
  export {
1345
+ DEFAULT_CLASS_NAMES,
1298
1346
  FormBuilder,
1299
1347
  FormValidator,
1300
1348
  buildForm,