@coherent.js/forms 1.1.2 → 2.0.0-rc.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/patterns.js", "../src/validators.js", "../src/form-builder.js", "../src/form-hydration.js"],
4
- "sourcesContent": ["/**\n * Shared validation patterns.\n *\n * Internal to @coherent.js/forms \u2014 not re-exported from index.js.\n */\n\n/**\n * Email shape check: a local part, then a dotted domain.\n *\n * Domain labels use `[^\\s@.]` rather than `[^\\s@]` so that the literal dot\n * separators are the only thing that can match a dot. Allowing `[^\\s@]+` on\n * both sides of `\\.` makes the split ambiguous, and a non-matching subject\n * with many dots (\"a@\" + \"a.\" * n + \" \") then costs O(n\u00B2) backtracking \u2014\n * CodeQL js/polynomial-redos.\n *\n * This is a shape check, not RFC 5322 conformance. Deliverability is only\n * ever established by sending mail.\n */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@.]+(?:\\.[^\\s@.]+)+$/;\n\n/**\n * Longest address RFC 5321 permits, used to bound work before matching.\n */\nexport const EMAIL_MAX_LENGTH = 254;\n\n/**\n * Test whether a value has the shape of an email address.\n *\n * @param {unknown} value - Value to check\n * @returns {boolean} True if the value looks like an email address\n */\nexport function isEmailShaped(value) {\n return (\n typeof value === 'string' &&\n value.length <= EMAIL_MAX_LENGTH &&\n EMAIL_PATTERN.test(value)\n );\n}\n", "/**\n * Coherent.js Forms - Validators\n *\n * Form validation utilities\n *\n * @module forms/validators\n */\n\nimport { validators as packageValidators } from './validation.js';\nimport { isEmailShaped } from './patterns.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 if (!isEmailShaped(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';\nimport { isEmailShaped } from './patterns.js';\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 * 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 if (!isEmailShaped(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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",
3
+ "sources": ["../src/patterns.js", "../src/rules.js", "../src/form-builder.js", "../src/form-hydration.js"],
4
+ "sourcesContent": ["/**\n * Shared validation patterns.\n *\n * Internal to @coherent.js/forms \u2014 not re-exported from index.js.\n */\n\n/**\n * Email shape check: a local part, then a dotted domain.\n *\n * Domain labels use `[^\\s@.]` rather than `[^\\s@]` so that the literal dot\n * separators are the only thing that can match a dot. Allowing `[^\\s@]+` on\n * both sides of `\\.` makes the split ambiguous, and a non-matching subject\n * with many dots (\"a@\" + \"a.\" * n + \" \") then costs O(n\u00B2) backtracking \u2014\n * CodeQL js/polynomial-redos.\n *\n * This is a shape check, not RFC 5322 conformance. Deliverability is only\n * ever established by sending mail.\n */\nexport const EMAIL_PATTERN = /^[^\\s@]+@[^\\s@.]+(?:\\.[^\\s@.]+)+$/;\n\n/**\n * Longest address RFC 5321 permits, used to bound work before matching.\n */\nexport const EMAIL_MAX_LENGTH = 254;\n\n/**\n * Test whether a value has the shape of an email address.\n *\n * @param {unknown} value - Value to check\n * @returns {boolean} True if the value looks like an email address\n */\nexport function isEmailShaped(value) {\n return (\n typeof value === 'string' &&\n value.length <= EMAIL_MAX_LENGTH &&\n EMAIL_PATTERN.test(value)\n );\n}\n", "/**\n * Coherent.js Forms - validation rules\n *\n * The single `validators` registry behind `@coherent.js/forms`,\n * `@coherent.js/forms/validation` and `@coherent.js/forms/validators`.\n * Internal: import it through one of those entry points.\n *\n * ## Calling convention\n *\n * A **validator** is `(value, formData) => string | null`: an error message,\n * or `null` when the value passes. Schemas, `validateField`, `validateForm`,\n * `FormBuilder` fields and `hydrateForm` all run validators this way.\n *\n * Each built-in is a **factory** that returns a validator:\n *\n * validators.required() validators.required('Required!')\n * validators.minLength(8) validators.minLength(8, 'Too short')\n *\n * A built-in may also be listed without calling it (`[validators.required]`);\n * runners call the factory with its defaults first. A string names a built-in\n * or registered validator, and a built-in's name may carry its arguments:\n * `'minLength:8'` is `validators.minLength(8)` (see argsFromString).\n *\n * For backward compatibility a built-in can still be called **directly** with\n * a value and an options object, returning the error or `null`:\n *\n * validators.minLength('abc', { min: 5 }) // 'Minimum length is 5'\n * validators.email(null) // null\n *\n * A lone argument that could be a message \u2014 a non-empty string, `undefined`\n * or `null` \u2014 is always read as a factory call, so `validators.email('a@b.c')`\n * returns a validator; call `validators.email('a@b.c', {})` (or\n * `validators.email()('a@b.c')`) to check a value directly.\n *\n * @module forms/rules\n */\n\nimport { isEmailShaped } from './patterns.js';\n\n/** Built-in factories, so runners can tell them from plain validators. */\nconst BUILTIN = Symbol('coherent.forms.builtin');\n\n/**\n * Validators whose origin is known, so the form builder can describe them to\n * the client: validator \u2192 `{ name, args }`.\n */\nconst DESCRIPTORS = new WeakMap();\n\n/** Names on `validators` that are helpers, not validation rules. */\nconst HELPER_NAMES = new Set(['get', 'compose', 'debounce', 'cancellable', 'when', 'chain']);\n\nconst isEmpty = value => value === null || value === undefined || value === '';\n\nconst isOptionsObject = value =>\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof RegExp);\n\n/**\n * Factory arguments for the text after the colon of a `'name:\u2026'` string\n * entry, or `null` when it does not fit the rule.\n *\n * - No parameter (`required`, `email`, \u2026): the text is the message \u2014\n * `'required:Please enter your name'`.\n * - `'list'` (`oneOf`, `fileType`, `fileExtension`): every comma-separated\n * value goes into the one array parameter \u2014 `'oneOf:red,green,blue'`.\n * - `'regexp'` (`pattern`): the whole text is the regular expression source,\n * commas included \u2014 `'pattern:^[a-z]{2,8}$'`.\n * - Otherwise the text up to the first comma is the parameter (a number for\n * `'number'`, so `'minLength:abc'` is rejected) and the rest, commas\n * included, is the message \u2014 `'minLength:8,Use at least 8 characters'`.\n *\n * @param {Array} params - The rule's factory parameters\n * @param {'string'|'number'|'list'|'regexp'} kind - How the text supplies them\n * @param {string|undefined} text - Text after the colon (`undefined`: none)\n * @returns {Array|null}\n */\nfunction argsFromString(params, kind, text) {\n if (text === undefined || text.trim() === '') return [];\n if (params.length === 0) return [text.trim()];\n\n if (kind === 'list') {\n return [text.split(',').map(part => part.trim()).filter(Boolean)];\n }\n if (kind === 'regexp') {\n try {\n return [new RegExp(text)];\n } catch {\n return null;\n }\n }\n\n const comma = text.indexOf(',');\n const raw = (comma === -1 ? text : text.slice(0, comma)).trim();\n const message = comma === -1 ? '' : text.slice(comma + 1).trim();\n\n let arg = raw;\n if (kind === 'number') {\n arg = Number(raw);\n if (raw === '' || Number.isNaN(arg)) return null;\n }\n return message ? [arg, message] : [arg];\n}\n\n/**\n * Define a built-in rule.\n *\n * @param {string} name - Registry name\n * @param {Object} spec\n * @param {Array<[string, ...string[]]>} [spec.params] - Factory parameters, in\n * order, each as `[configKey, ...optionAliases]` for direct calls\n * @param {'string'|'number'|'list'|'regexp'} [spec.stringArg] - How the\n * `'name:\u2026'` string form supplies the parameter (see argsFromString)\n * @param {(value: unknown, config: Object, formData: Object) => boolean} spec.valid\n * @param {(config: Object) => string} spec.message - Default error message\n */\nfunction defineRule(name, { params = [], stringArg = 'string', valid, message }) {\n const fromOptions = (options = {}) => {\n const config = { message: options.message };\n for (const [key, ...aliases] of params) {\n config[key] = [key, ...aliases].map(alias => options[alias]).find(v => v !== undefined);\n }\n return config;\n };\n\n const check = (value, config, formData) => {\n if (valid(value, config, formData || {})) return null;\n return config.message || builtin.message || message(config);\n };\n\n const factory = (...args) => {\n const config = { message: args[params.length] };\n params.forEach(([key], index) => { config[key] = args[index]; });\n\n // Also accepts the (value, options, translator, allValues) arguments of\n // the older direct convention, so it composes with either.\n const rule = (value, formData, _translator, allValues) =>\n check(value, config, allValues ?? formData);\n\n DESCRIPTORS.set(rule, { name, args });\n return rule;\n };\n\n // Direct calls pass a value plus an options object. Anything else is a\n // factory call, including `required(undefined)` (a message that happens to\n // be missing) \u2014 only an explicit options object, or a lone argument that\n // cannot be a message, selects the direct form.\n const isDirectCall = (args) => {\n if (args.length >= 2) {\n return params.length === 0 || isOptionsObject(args[1]);\n }\n if (args.length === 1 && params.length === 0) {\n const [arg] = args;\n return arg === '' || (arg !== undefined && arg !== null && typeof arg !== 'string');\n }\n return false;\n };\n\n function builtin(...args) {\n if (isDirectCall(args)) {\n const [value, options, , allValues] = args;\n return check(value, fromOptions(isOptionsObject(options) ? options : {}), allValues);\n }\n return factory(...args);\n }\n\n Object.defineProperty(builtin, 'name', { value: name });\n builtin[BUILTIN] = {\n name,\n factory,\n argsFromString: (text) => argsFromString(params, stringArg, text)\n };\n return builtin;\n}\n\nconst toNumber = value => (typeof value === 'number' ? value : Number(value));\n\nfunction testRegExp(regex, value) {\n if (!(regex instanceof RegExp)) return true;\n // A /g or /y regex keeps lastIndex between calls; always test from the start.\n regex.lastIndex = 0;\n return regex.test(String(value));\n}\n\nfunction fileMatchesType(file, allowedTypes) {\n const fileType = file.type;\n const fileExt = file.name ? file.name.split('.').pop().toLowerCase() : '';\n\n return 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\n/**\n * Built-in validators and helpers. See the module comment for the calling\n * convention.\n */\nexport const validators = {\n required: defineRule('required', {\n valid: value => !isEmpty(value),\n message: () => 'This field is required'\n }),\n\n email: defineRule('email', {\n valid: value => isEmpty(value) || isEmailShaped(value),\n message: () => 'Invalid email address'\n }),\n\n url: defineRule('url', {\n valid: (value) => {\n if (isEmpty(value)) return true;\n try {\n new URL(value);\n return true;\n } catch {\n return false;\n }\n },\n message: () => 'Invalid URL'\n }),\n\n minLength: defineRule('minLength', {\n params: [['min', 'minLength']],\n stringArg: 'number',\n valid: (value, { min }) => !value || !(value.length < (min ?? 0)),\n message: ({ min }) => `Minimum length is ${min}`\n }),\n\n maxLength: defineRule('maxLength', {\n params: [['max', 'maxLength']],\n stringArg: 'number',\n valid: (value, { max }) => !value || !(value.length > (max ?? Infinity)),\n message: ({ max }) => `Maximum length is ${max}`\n }),\n\n // Empty values pass (combine with `required`); a non-numeric value fails.\n min: defineRule('min', {\n params: [['min']],\n stringArg: 'number',\n valid: (value, { min }) => {\n if (isEmpty(value)) return true;\n const number = toNumber(value);\n return !Number.isNaN(number) && !(number < (min ?? -Infinity));\n },\n message: ({ min }) => `Minimum value is ${min}`\n }),\n\n max: defineRule('max', {\n params: [['max']],\n stringArg: 'number',\n valid: (value, { max }) => {\n if (isEmpty(value)) return true;\n const number = toNumber(value);\n return !Number.isNaN(number) && !(number > (max ?? Infinity));\n },\n message: ({ max }) => `Maximum value is ${max}`\n }),\n\n pattern: defineRule('pattern', {\n params: [['pattern', 'regex']],\n stringArg: 'regexp',\n valid: (value, { pattern }) => isEmpty(value) || testRegExp(pattern, value),\n message: () => 'Invalid format'\n }),\n\n /** Equal to another field, even when empty */\n matches: defineRule('matches', {\n params: [['field', 'fieldName']],\n valid: (value, { field }, formData) => value === formData[field],\n message: () => 'Fields do not match'\n }),\n\n /** Equal to another field; an empty value passes */\n match: defineRule('match', {\n params: [['field', 'fieldName']],\n valid: (value, { field }, formData) => !value || value === formData[field],\n message: ({ field }) => `Must match ${field}`\n }),\n\n oneOf: defineRule('oneOf', {\n params: [['options', 'values']],\n stringArg: 'list',\n valid: (value, { options }) => !value || !Array.isArray(options) || options.includes(value),\n message: () => 'Invalid option'\n }),\n\n custom: defineRule('custom', {\n params: [['validator', 'fn']],\n valid: (value, { validator }, formData) =>\n typeof validator !== 'function' || Boolean(validator(value, formData)),\n message: () => 'Validation failed'\n }),\n\n number: defineRule('number', {\n valid: value => isEmpty(value) || !Number.isNaN(Number(value)),\n message: () => 'Must be a valid number'\n }),\n\n integer: defineRule('integer', {\n valid: value => isEmpty(value) || Number.isInteger(Number(value)),\n message: () => 'Must be a whole number'\n }),\n\n phone: defineRule('phone', {\n valid: value => !value || (/^[\\d\\s\\-+()]+$/.test(value) && value.replace(/\\D/g, '').length >= 10),\n message: () => 'Please enter a valid phone number'\n }),\n\n date: defineRule('date', {\n valid: value => !value || !Number.isNaN(new Date(value).getTime()),\n message: () => 'Please enter a valid date'\n }),\n\n alpha: defineRule('alpha', {\n valid: value => !value || /^[a-zA-Z]+$/.test(value),\n message: () => 'Must contain only letters'\n }),\n\n alphanumeric: defineRule('alphanumeric', {\n valid: value => !value || /^[a-zA-Z0-9]+$/.test(value),\n message: () => 'Must contain only letters and numbers'\n }),\n\n uppercase: defineRule('uppercase', {\n valid: value => !value || value === String(value).toUpperCase(),\n message: () => 'Must be uppercase'\n }),\n\n fileType: defineRule('fileType', {\n params: [['accept', 'types']],\n stringArg: 'list',\n valid: (value, { accept }) =>\n !value || value.type === undefined || fileMatchesType(value, accept || []),\n message: ({ accept }) => `File type must be one of: ${(accept || []).join(', ')}`\n }),\n\n fileSize: defineRule('fileSize', {\n params: [['maxSize']],\n stringArg: 'number',\n valid: (value, { maxSize }) =>\n !value || value.size === undefined || !(value.size > (maxSize ?? Infinity)),\n message: ({ maxSize }) => `File size must be less than ${((maxSize ?? Infinity) / (1024 * 1024)).toFixed(2)}MB`\n }),\n\n fileExtension: defineRule('fileExtension', {\n params: [['extensions']],\n stringArg: 'list',\n valid: (value, { extensions }) => {\n if (!value) return true;\n const fileName = value.name || value;\n const ext = `.${String(fileName).split('.').pop().toLowerCase()}`;\n return (extensions || []).some(allowed => ext === allowed.toLowerCase());\n },\n message: ({ extensions }) => `File extension must be one of: ${(extensions || []).join(', ')}`\n }),\n\n /** A registered validator or built-in by name */\n get: (name) => (Object.hasOwn(validators, name) ? validators[name] : undefined),\n\n /** Combine validators into one returning the first error */\n compose: (validatorList) => {\n const rules = validatorList.map(resolveValidator);\n return (value, options, translator, allValues) => {\n for (const rule of rules) {\n const error = rule ? rule(value, options, translator, allValues) : null;\n if (error) {\n return error;\n }\n }\n return null;\n };\n },\n\n /** Debounce an 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 /** Run `validator` only when `condition` holds */\n when: (condition, validator) => {\n const rule = resolveValidator(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 rule ? rule(value, options, translator, allValues) : null;\n };\n },\n\n /** Validator chain builder */\n chain: (options = {}) => {\n const validatorList = [];\n const stopOnFirstError = options.stopOnFirstError !== false;\n\n const direct = name => (opts) => {\n validatorList.push((v, o, t, a) => validators[name](v, opts || o || {}, t, a));\n return chain;\n };\n\n const chain = {\n required: direct('required'),\n email: direct('email'),\n minLength: direct('minLength'),\n maxLength: direct('maxLength'),\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 }\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 return chain;\n }\n};\n\n/** The registry entry named `name`, if it is a validator (not a helper). */\nfunction lookup(name) {\n const found = Object.hasOwn(validators, name) && !HELPER_NAMES.has(name)\n ? validators[name]\n : null;\n return typeof found === 'function' ? found : null;\n}\n\n/**\n * Resolve a string entry: `'required'`, or `'name:arg1,arg2'` for a built-in\n * with arguments (`'minLength:8'`, `'oneOf:a,b,c'`, `'pattern:^\\\\d+$'`; see\n * argsFromString). A registered validator is named without arguments.\n *\n * @param {string} entry\n * @returns {{ name: string, args: Array, validator: Function }|null} `null`\n * for an unknown name or arguments that do not fit the rule\n */\nfunction resolveString(entry) {\n // An exact registry name wins, so a validator registered under a name that\n // contains a colon still resolves.\n const exact = lookup(entry);\n const colon = exact ? -1 : entry.indexOf(':');\n const name = colon === -1 ? entry : entry.slice(0, colon).trim();\n const found = exact ?? lookup(name);\n if (!found) return null;\n\n const text = colon === -1 ? undefined : entry.slice(colon + 1);\n if (!found[BUILTIN]) {\n return text === undefined ? { name, args: [], validator: found } : null;\n }\n\n const args = found[BUILTIN].argsFromString(text);\n if (!args) return null;\n return { name: found[BUILTIN].name, args, validator: found[BUILTIN].factory(...args) };\n}\n\n/**\n * Turn a schema entry into a validator: a built-in factory listed without\n * calling it (`validators.required`) is called with its defaults, a string\n * names a built-in or registered validator (`'required'`, or a built-in with\n * arguments: `'minLength:8'`), a function is used as is. Anything else \u2014 an\n * unknown name, or arguments that do not fit (`'minLength:abc'`) \u2014 is `null`\n * (skipped).\n *\n * @param {unknown} entry\n * @returns {Function|null}\n */\nexport function resolveValidator(entry) {\n if (typeof entry === 'string') {\n return resolveString(entry)?.validator ?? null;\n }\n if (typeof entry !== 'function') return null;\n return entry[BUILTIN] ? entry[BUILTIN].factory() : entry;\n}\n\n/**\n * Run validators against one value; the first error wins.\n */\nexport function validateField(value, validatorList, formData = {}) {\n const list = Array.isArray(validatorList) ? validatorList : [validatorList];\n for (const entry of list) {\n const validator = resolveValidator(entry);\n const error = validator ? validator(value, formData) : null;\n if (error) {\n return error;\n }\n }\n return null;\n}\n\n/**\n * Validate a whole form: `{ field: [validators] }`. Returns the first error\n * per failing field, or `null` when everything passes.\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 * Wrap a check function as a validator: a string result is the error, any\n * other truthy result becomes `message`, a falsy one passes.\n */\nexport function wrapValidator(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 validator under `name`. It is stored as is (not as a factory),\n * so list it as `validators[name]`. Registered validators are described to\n * the client by name, so `hydrateForm` enforces them when the same name is\n * registered in the browser.\n */\nexport function registerValidator(name, validatorFn) {\n validators[name] = validatorFn;\n if (typeof validatorFn === 'function' && !validatorFn[BUILTIN]) {\n DESCRIPTORS.set(validatorFn, { name, args: [] });\n }\n}\n\n/**\n * Compose multiple validators\n */\nexport function composeValidators(...validatorFns) {\n return validators.compose(validatorFns);\n}\n\n// ---------------------------------------------------------------------------\n// Server \u2192 client description (`data-validators`)\n// ---------------------------------------------------------------------------\n\n/** JSON cannot hold a RegExp; tag it so the client can rebuild it. */\nfunction serializeArg(arg) {\n if (arg instanceof RegExp) return { $regexp: [arg.source, arg.flags] };\n return arg;\n}\n\nfunction reviveArg(arg) {\n if (isOptionsObject(arg) && Array.isArray(arg.$regexp)) {\n try {\n return new RegExp(arg.$regexp[0], arg.$regexp[1]);\n } catch {\n return undefined;\n }\n }\n return arg;\n}\n\n/** Whether JSON can carry a factory argument to the client unchanged. */\nfunction isSerializable(arg) {\n if (arg === undefined || arg === null) return true;\n if (arg instanceof RegExp) return true;\n if (Array.isArray(arg)) return arg.every(isSerializable);\n return ['string', 'number', 'boolean'].includes(typeof arg);\n}\n\n/**\n * Describe a schema entry as `{ name, args }` for the client, or `null` when\n * it cannot be rebuilt there (an anonymous function, or a factory argument\n * such as a function that JSON cannot carry). Those still run on the server.\n */\nexport function describeValidator(entry) {\n let descriptor;\n if (typeof entry === 'string') {\n // `'minLength:8'` is described like `validators.minLength(8)`.\n descriptor = resolveString(entry);\n } else if (typeof entry === 'function') {\n descriptor = entry[BUILTIN] ? { name: entry[BUILTIN].name, args: [] } : DESCRIPTORS.get(entry);\n }\n if (!descriptor || !descriptor.args.every(isSerializable)) return null;\n\n // Trailing undefined arguments (an omitted message) are just defaults.\n const args = [...descriptor.args];\n while (args.length > 0 && args[args.length - 1] === undefined) args.pop();\n return { name: descriptor.name, args: args.map(serializeArg) };\n}\n\n/**\n * Serialize a field's validators for its `data-validators` attribute:\n * a JSON array of `{ name, args }`, or `null` when none can be described.\n */\nexport function serializeValidators(validatorList) {\n const specs = (validatorList || []).map(describeValidator).filter(Boolean);\n return specs.length > 0 ? JSON.stringify(specs) : null;\n}\n\n/**\n * Rebuild validators from a `data-validators` attribute. Reads the JSON\n * written by {@link serializeValidators}, and the older comma-separated form\n * (`required,minLength:8`). Unknown names are skipped.\n */\nexport function parseValidators(attribute) {\n if (!attribute) return [];\n const text = String(attribute).trim();\n\n let specs;\n if (text.startsWith('[')) {\n try {\n specs = JSON.parse(text);\n } catch {\n return [];\n }\n if (!Array.isArray(specs)) return [];\n } else {\n specs = text.split(',').map((part) => {\n const [name, ...params] = part.trim().split(':');\n return { name, args: params.map(p => (p !== '' && !Number.isNaN(Number(p)) ? Number(p) : p)) };\n });\n }\n\n return specs.map((spec) => {\n if (!spec || typeof spec.name !== 'string') return null;\n const { name } = spec;\n if (!Object.hasOwn(validators, name) || HELPER_NAMES.has(name)) return null;\n\n const entry = validators[name];\n if (typeof entry !== 'function') return null;\n\n // Built-ins are rebuilt through their factory, never the direct form.\n const args = Array.isArray(spec.args) ? spec.args.map(reviveArg) : [];\n return entry[BUILTIN] ? entry[BUILTIN].factory(...args) : entry;\n }).filter(Boolean);\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';\nimport { isEmailShaped } from './patterns.js';\nimport { resolveValidator, serializeValidators } from './rules.js';\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/**\n * Hidden field that carries `options.csrfToken`. Mirrors CSRF_FIELD_NAME in\n * csrf.js, which is server-only (node:crypto) and so cannot be imported here.\n */\nconst DEFAULT_CSRF_FIELD_NAME = '_csrf';\n\n/** Join class names, dropping empties so no element carries `class=\"\"`. */\nfunction joinClasses(...names) {\n return names.filter(Boolean).join(' ');\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 if (!isEmailShaped(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 (`validators.required` and `validators.required()` alike)\n for (const entry of field.validators || []) {\n const validator = resolveValidator(entry);\n const error = validator ? validator(value, this.values) : null;\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 * A copy of this form's definition \u2014 fields, groups, options and handlers \u2014\n * with fresh values, errors and touched state.\n *\n * A FormBuilder holds the state of one submission. On a server, keep the\n * shared definition at module scope and fork it per request, so one user's\n * submitted values and errors never render into another user's page.\n */\n fork() {\n const copy = new FormBuilder({ ...this.options });\n\n for (const [name, config] of this.fields) {\n copy.fields.set(name, config);\n }\n for (const [name, group] of this.groups) {\n copy.groups.set(name, group);\n }\n copy.values = { ...this.initialValues };\n copy.initialValues = { ...this.initialValues };\n copy.submitHandler = this.submitHandler;\n copy.errorHandler = this.errorHandler;\n\n return copy;\n }\n\n /**\n * The values, errors and touched state a render uses.\n *\n * Passing any of `values`, `errors` or `touched` to buildForm renders from\n * those alone \u2014 the instance's own state is neither read nor changed \u2014 so a\n * shared builder can render per-request state. Values are merged over the\n * fields' default values, and fields with an error count as touched unless\n * `touched` is given.\n */\n resolveRenderState(options = {}) {\n const { values, errors, touched } = options;\n if (values === undefined && errors === undefined && touched === undefined) {\n return { values: this.values, errors: this.errors, touched: this.touched };\n }\n\n const renderErrors = errors || {};\n return {\n values: { ...this.initialValues, ...values },\n errors: renderErrors,\n touched: touched || Object.fromEntries(Object.keys(renderErrors).map(name => [name, true]))\n };\n }\n\n /**\n * Build input component with validation metadata for hydration\n */\n buildInput(name, classNames = this.resolveClassNames(), state = this.resolveRenderState()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const value = state.values[name] || '';\n const error = state.errors[name];\n const isTouched = state.touched[name];\n\n // Describe the validators for hydrateForm, as a JSON array of\n // `{ name, args }` (`[{\"name\":\"minLength\",\"args\":[8]}]`), so the client\n // rebuilds the same rules. It used to emit `v.name || 'custom'`: a\n // factory-built validator is an anonymous closure, so every one became\n // `custom`, which the client treated as always valid. Validators that\n // cannot be described (anonymous functions) are enforced server-side only.\n const validatorSpec = serializeValidators(field.validators);\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 (validatorSpec) {\n inputProps['data-validators'] = validatorSpec;\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(), state = this.resolveRenderState()) {\n const error = state.errors[name];\n const isTouched = state.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(), state = this.resolveRenderState()) {\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, state)\n ];\n\n const error = this.buildError(name, classNames, state);\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 * `options.values`, `options.errors` and `options.touched` render\n * per-request state without touching the builder's own (see\n * resolveRenderState). `options.csrfToken` adds a hidden `_csrf` input (the\n * name is `options.csrfFieldName`); create the token with\n * `@coherent.js/forms/csrf`.\n */\n buildForm(options = {}) {\n const { values: _values, errors: _errors, touched: _touched, ...formOptions } = options;\n const settings = { ...this.options, ...formOptions };\n const classNames = this.resolveClassNames(options.classNames);\n const state = this.resolveRenderState(options);\n const fields = [];\n\n if (settings.csrfToken !== undefined && settings.csrfToken !== null && settings.csrfToken !== '') {\n fields.push({\n input: {\n type: 'hidden',\n name: settings.csrfFieldName || DEFAULT_CSRF_FIELD_NAME,\n value: String(settings.csrfToken)\n }\n });\n }\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, state.values)) continue;\n fields.push(this.buildField(name, classNames, state));\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, values = this.values) {\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(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 { parseValidators } from './rules.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 * 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
6
  "names": ["name"]
7
7
  }