@coherent.js/forms 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,40 @@
1
1
  // src/form-builder.js
2
2
  import { render as renderToHTML } from "@coherent.js/core";
3
+ var DEFAULT_CLASS_NAMES = {
4
+ /** Wrapper around label, control and error */
5
+ field: "form-field",
6
+ label: "",
7
+ /** Base class on the control, before any per-field className */
8
+ control: "",
9
+ /** Added to the control while it has a visible error */
10
+ invalid: "error",
11
+ /** The error message element */
12
+ error: "error-message",
13
+ submit: "submit-button"
14
+ };
15
+ var VALID_ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;
16
+ var EVENT_HANDLER_NAME = /^on/i;
17
+ function safeAttributes(attributes) {
18
+ if (!attributes || typeof attributes !== "object") return {};
19
+ const safe = {};
20
+ for (const [name, value] of Object.entries(attributes)) {
21
+ if (!VALID_ATTRIBUTE_NAME.test(name)) {
22
+ console.warn(`[coherent.js/forms] Ignoring invalid attribute name: ${JSON.stringify(name)}`);
23
+ continue;
24
+ }
25
+ if (EVENT_HANDLER_NAME.test(name)) {
26
+ console.warn(
27
+ `[coherent.js/forms] Ignoring inline event handler "${name}". Attach handlers with hydrateForm instead.`
28
+ );
29
+ continue;
30
+ }
31
+ safe[name] = value;
32
+ }
33
+ return safe;
34
+ }
35
+ function joinClasses(...names) {
36
+ return names.filter(Boolean).join(" ");
37
+ }
3
38
  var FormBuilder = class {
4
39
  constructor(options = {}) {
5
40
  this.options = {
@@ -227,11 +262,8 @@ var FormBuilder = class {
227
262
  */
228
263
  validate() {
229
264
  const errors = {};
230
- for (const [name, field] of this.fields) {
231
- const showCondition = field.showWhen || field.showIf;
232
- if (showCondition && !showCondition(this.values)) {
233
- continue;
234
- }
265
+ for (const [name] of this.fields) {
266
+ if (!this.isFieldVisible(name)) continue;
235
267
  const error = this.validateField(name);
236
268
  if (error) {
237
269
  errors[name] = error;
@@ -299,7 +331,7 @@ var FormBuilder = class {
299
331
  /**
300
332
  * Build input component with validation metadata for hydration
301
333
  */
302
- buildInput(name) {
334
+ buildInput(name, classNames = this.resolveClassNames()) {
303
335
  const field = this.fields.get(name);
304
336
  if (!field) return null;
305
337
  const value = this.values[name] || "";
@@ -310,16 +342,26 @@ var FormBuilder = class {
310
342
  if (typeof v === "string") return v;
311
343
  return null;
312
344
  }).filter(Boolean).join(",");
345
+ const controlClass = joinClasses(
346
+ classNames.control,
347
+ field.className,
348
+ error && isTouched ? classNames.invalid : null
349
+ );
313
350
  const inputProps = {
351
+ // Spread first: name, id, type and the aria-* pair below are the
352
+ // builder's own invariants, and hydration keys off them.
353
+ ...safeAttributes(field.attributes),
314
354
  type: field.type,
315
355
  name: field.name,
316
356
  id: field.name,
317
357
  value,
318
- placeholder: field.placeholder,
319
358
  "aria-invalid": error ? "true" : "false",
320
- "aria-describedby": error ? `${name}-error` : void 0,
321
- className: error && isTouched ? "error" : ""
359
+ "aria-describedby": error ? `${name}-error` : void 0
322
360
  };
361
+ if (field.placeholder) inputProps.placeholder = field.placeholder;
362
+ if (controlClass) inputProps.className = controlClass;
363
+ if (field.disabled) inputProps.disabled = true;
364
+ if (field.readonly) inputProps.readonly = true;
323
365
  if (field.required) {
324
366
  inputProps.required = true;
325
367
  inputProps["data-required"] = "true";
@@ -357,71 +399,63 @@ var FormBuilder = class {
357
399
  /**
358
400
  * Build label component
359
401
  */
360
- buildLabel(name) {
402
+ buildLabel(name, classNames = this.resolveClassNames()) {
361
403
  const field = this.fields.get(name);
362
404
  if (!field) return null;
363
- return {
364
- label: {
365
- for: field.name,
366
- text: field.label
367
- }
368
- };
405
+ const props = { for: field.name, text: field.label };
406
+ if (classNames.label) props.className = classNames.label;
407
+ return { label: props };
369
408
  }
370
409
  /**
371
410
  * Build error component
372
411
  */
373
- buildError(name) {
412
+ buildError(name, classNames = this.resolveClassNames()) {
374
413
  const error = this.errors[name];
375
414
  const isTouched = this.touched[name];
376
415
  if (!error || !isTouched) return null;
377
- return {
378
- div: {
379
- id: `${name}-error`,
380
- className: "error-message",
381
- role: "alert",
382
- text: error
383
- }
384
- };
416
+ const props = { id: `${name}-error`, role: "alert", text: error };
417
+ if (classNames.error) props.className = classNames.error;
418
+ return { div: props };
419
+ }
420
+ /**
421
+ * Merge configured class names over the defaults
422
+ */
423
+ resolveClassNames(overrides = {}) {
424
+ return { ...DEFAULT_CLASS_NAMES, ...this.options.classNames, ...overrides };
385
425
  }
386
426
  /**
387
427
  * Build complete field component
388
428
  */
389
- buildField(name) {
429
+ buildField(name, classNames = this.resolveClassNames()) {
390
430
  const field = this.fields.get(name);
391
431
  if (!field) return null;
392
432
  const children = [
393
- this.buildLabel(name),
394
- this.buildInput(name)
433
+ this.buildLabel(name, classNames),
434
+ this.buildInput(name, classNames)
395
435
  ];
396
- const error = this.buildError(name);
436
+ const error = this.buildError(name, classNames);
397
437
  if (error) {
398
438
  children.push(error);
399
439
  }
400
- return {
401
- div: {
402
- className: "form-field",
403
- "data-field": name,
404
- children
405
- }
406
- };
440
+ const props = { "data-field": name, children };
441
+ if (classNames.field) props.className = classNames.field;
442
+ return { div: props };
407
443
  }
408
444
  /**
409
445
  * Build entire form
410
446
  */
411
447
  buildForm(options = {}) {
448
+ const settings = { ...this.options, ...options };
449
+ const classNames = this.resolveClassNames(options.classNames);
412
450
  const fields = [];
413
451
  for (const [name] of this.fields) {
414
- fields.push(this.buildField(name));
452
+ if (!this.isFieldVisible(name)) continue;
453
+ fields.push(this.buildField(name, classNames));
415
454
  }
416
- const settings = { ...this.options, ...options };
417
455
  if (settings.submitButton !== false) {
418
- fields.push({
419
- button: {
420
- type: "submit",
421
- text: settings.submitText || "Submit",
422
- className: "submit-button"
423
- }
424
- });
456
+ const button = { type: "submit", text: settings.submitText || "Submit" };
457
+ if (classNames.submit) button.className = classNames.submit;
458
+ fields.push({ button });
425
459
  }
426
460
  const form = {};
427
461
  if (settings.action) form.action = settings.action;
@@ -430,8 +464,10 @@ var FormBuilder = class {
430
464
  if (settings.id) form.id = settings.id;
431
465
  if (settings.className) form.className = settings.className;
432
466
  if (settings.enctype) form.enctype = settings.enctype;
433
- form.onsubmit = "handleSubmit(event)";
434
- form.novalidate = true;
467
+ if (settings.enhance) {
468
+ form.onsubmit = typeof settings.enhance === "string" ? settings.enhance : "handleSubmit(event)";
469
+ }
470
+ if (settings.novalidate === true) form.novalidate = true;
435
471
  form.children = fields;
436
472
  return { form };
437
473
  }
@@ -479,11 +515,12 @@ var FormBuilder = class {
479
515
  isFieldVisible(name) {
480
516
  const field = this.fields.get(name);
481
517
  if (!field) return false;
518
+ if (field.visible === false) return false;
482
519
  const showCondition = field.showWhen || field.showIf;
483
520
  if (showCondition) {
484
521
  return showCondition(this.values);
485
522
  }
486
- return field.visible !== false;
523
+ return true;
487
524
  }
488
525
  /**
489
526
  * Reset form
@@ -532,6 +569,7 @@ var form_builder_default = {
532
569
  buildForm
533
570
  };
534
571
  export {
572
+ DEFAULT_CLASS_NAMES,
535
573
  FormBuilder,
536
574
  buildForm,
537
575
  createFormBuilder,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/form-builder.js"],
4
- "sourcesContent": ["/**\n * Coherent.js Form Builder\n * \n * Utilities for building forms with Coherent.js\n * \n * @module forms/form-builder\n */\n\nimport { render as renderToHTML } from '@coherent.js/core';\n\n/**\n * Form Builder\n * Helps create form components with validation\n */\nexport class FormBuilder {\n constructor(options = {}) {\n this.options = {\n validateOnChange: true,\n validateOnBlur: true,\n name: options.name || 'form',\n ...options\n };\n \n this.fields = new Map();\n this.groups = new Map();\n this.values = {};\n this.errors = {};\n this.touched = {};\n this.initialValues = {};\n this.submitHandler = null;\n this.errorHandler = null;\n this._isSubmitting = false;\n }\n\n /**\n * Add a field to the form (alias for field)\n */\n addField(name, config = {}) {\n return this.field(name, config);\n }\n\n /**\n * Add a field to the form\n */\n field(name, config = {}) {\n const fieldConfig = {\n name,\n type: config.type || 'text',\n label: config.label || name,\n placeholder: config.placeholder || '',\n defaultValue: config.defaultValue || '',\n validators: config.validators || [],\n required: config.required || false,\n visible: config.visible !== false,\n showWhen: config.showWhen,\n ...config\n };\n \n this.fields.set(name, fieldConfig);\n\n // Set default value\n if (config.defaultValue !== undefined) {\n this.values[name] = config.defaultValue;\n this.initialValues[name] = config.defaultValue;\n }\n\n return this;\n }\n\n /**\n * Remove a field from the form\n */\n removeField(name) {\n this.fields.delete(name);\n delete this.values[name];\n delete this.errors[name];\n delete this.touched[name];\n return this;\n }\n\n /**\n * Update field configuration\n */\n updateField(name, config) {\n const field = this.fields.get(name);\n if (field) {\n this.fields.set(name, { ...field, ...config });\n }\n return this;\n }\n\n /**\n * Get all fields as array\n */\n getFields() {\n return Array.from(this.fields.values());\n }\n\n /**\n * Add a field group\n */\n addGroup(name, config = {}) {\n this.groups.set(name, {\n name,\n label: config.label || name,\n fields: config.fields || [],\n ...config\n });\n\n // Add fields in the group\n if (config.fields) {\n config.fields.forEach(fieldConfig => {\n this.addField(fieldConfig.name, fieldConfig);\n });\n }\n\n return this;\n }\n\n /**\n * Get field configuration\n */\n getField(name) {\n return this.fields.get(name);\n }\n\n /**\n * Set field value\n */\n setValue(name, value) {\n this.values[name] = value;\n this.touched[name] = true;\n\n const field = this.fields.get(name);\n if (field && (field.validateOnChange || this.options.validateOnChange)) {\n this.validateField(name);\n }\n }\n\n /**\n * Set multiple values\n */\n setValues(values) {\n Object.assign(this.values, values);\n return this;\n }\n\n /**\n * Get field value\n */\n getValue(name) {\n return this.values[name];\n }\n\n /**\n * Get all values\n */\n getValues() {\n return { ...this.values };\n }\n\n /**\n * Get field error\n */\n getFieldError(name) {\n return this.errors[name];\n }\n\n /**\n * Check if form has errors\n */\n hasErrors() {\n return Object.keys(this.errors).length > 0;\n }\n\n /**\n * Clear all errors\n */\n clearErrors() {\n this.errors = {};\n return this;\n }\n\n /**\n * Check if form is dirty (values changed from initial)\n */\n isDirty() {\n return Object.keys(this.values).some(key => {\n return this.values[key] !== this.initialValues[key];\n });\n }\n\n /**\n * Check if form is valid\n */\n isValid() {\n const result = this.validate();\n return Object.keys(result).length === 0;\n }\n\n /**\n * Validate a field\n */\n validateField(name) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n // Skip validation for hidden fields (support both showWhen and showIf)\n const showCondition = field.showWhen || field.showIf;\n if (showCondition && !showCondition(this.values)) {\n delete this.errors[name];\n return null;\n }\n\n const value = this.values[name];\n \n // Check required\n if (field.required && (value === undefined || value === null || value === '')) {\n const error = 'This field is required';\n this.errors[name] = error;\n return error;\n }\n\n // Skip further validation if empty and not required\n if (!value && !field.required) {\n delete this.errors[name];\n return null;\n }\n\n // Type-based validation\n if (value) {\n if (field.type === 'email') {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(value)) {\n const error = 'Please enter a valid email address';\n this.errors[name] = error;\n return error;\n }\n } else if (field.type === 'url') {\n try {\n new URL(value);\n } catch {\n const error = 'Please enter a valid URL';\n this.errors[name] = error;\n return error;\n }\n } else if (field.type === 'number') {\n if (isNaN(Number(value))) {\n const error = 'Please enter a valid number';\n this.errors[name] = error;\n return error;\n }\n }\n }\n\n // Check custom validate function\n if (field.validate) {\n const error = field.validate(value, this.values);\n if (error) {\n this.errors[name] = error;\n return error;\n }\n }\n\n // Run validators\n for (const validator of field.validators || []) {\n const error = validator(value, this.values);\n if (error) {\n this.errors[name] = error;\n return error;\n }\n }\n\n delete this.errors[name];\n return null;\n }\n\n /**\n * Validate all fields\n */\n validate() {\n const errors = {};\n\n for (const [name, field] of this.fields) {\n // Skip hidden fields (support both showWhen and showIf)\n const showCondition = field.showWhen || field.showIf;\n if (showCondition && !showCondition(this.values)) {\n continue;\n }\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) {\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 inputProps = {\n type: field.type,\n name: field.name,\n id: field.name,\n value: value,\n placeholder: field.placeholder,\n 'aria-invalid': error ? 'true' : 'false',\n 'aria-describedby': error ? `${name}-error` : undefined,\n className: error && isTouched ? 'error' : ''\n };\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) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n return {\n label: {\n for: field.name,\n text: field.label\n }\n };\n }\n\n /**\n * Build error component\n */\n buildError(name) {\n const error = this.errors[name];\n const isTouched = this.touched[name];\n\n if (!error || !isTouched) return null;\n\n return {\n div: {\n id: `${name}-error`,\n className: 'error-message',\n role: 'alert',\n text: error\n }\n };\n }\n\n /**\n * Build complete field component\n */\n buildField(name) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const children = [\n this.buildLabel(name),\n this.buildInput(name)\n ];\n\n const error = this.buildError(name);\n if (error) {\n children.push(error);\n }\n\n return {\n div: {\n className: 'form-field',\n 'data-field': name,\n children\n }\n };\n }\n\n /**\n * Build entire form\n */\n buildForm(options = {}) {\n const fields = [];\n\n for (const [name] of this.fields) {\n fields.push(this.buildField(name));\n }\n\n const settings = { ...this.options, ...options };\n\n if (settings.submitButton !== false) {\n fields.push({\n button: {\n type: 'submit',\n text: settings.submitText || 'Submit',\n className: 'submit-button'\n }\n });\n }\n\n const form = {};\n\n // Emitted before the handler so the markup reads action-first, and so a\n // form still submits to the right place without JavaScript.\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 form.onsubmit = 'handleSubmit(event)';\n form.novalidate = true;\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 // Support both showWhen and showIf\n const showCondition = field.showWhen || field.showIf;\n if (showCondition) {\n return showCondition(this.values);\n }\n \n return field.visible !== false;\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"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Coherent.js Form Builder\n * \n * Utilities for building forms with Coherent.js\n * \n * @module forms/form-builder\n */\n\nimport { render as renderToHTML } from '@coherent.js/core';\n\n/**\n * Class applied to each structural slot. Consumers override any subset via\n * the `classNames` option; hydrateForm accepts `invalid` and `error` so the\n * client writes the same names the server rendered.\n */\nexport const DEFAULT_CLASS_NAMES = {\n /** Wrapper around label, control and error */\n field: 'form-field',\n label: '',\n /** Base class on the control, before any per-field className */\n control: '',\n /** Added to the control while it has a visible error */\n invalid: 'error',\n /** The error message element */\n error: 'error-message',\n submit: 'submit-button'\n};\n\n/**\n * Attribute names are interpolated into the markup unescaped by\n * formatAttributes, so a passthrough has to reject anything that is not a\n * plain name \u2014 otherwise `{'x onclick=alert(1)': ''}` injects an attribute.\n */\nconst VALID_ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;\n\n/**\n * `onclick` and friends are syntactically valid names, and a string value\n * renders as an inline handler \u2014 script execution from whatever produced the\n * field config, and the thing this builder stopped emitting on the form\n * itself. Handlers belong in hydration.\n */\nconst EVENT_HANDLER_NAME = /^on/i;\n\nfunction safeAttributes(attributes) {\n if (!attributes || typeof attributes !== 'object') return {};\n\n const safe = {};\n for (const [name, value] of Object.entries(attributes)) {\n if (!VALID_ATTRIBUTE_NAME.test(name)) {\n console.warn(`[coherent.js/forms] Ignoring invalid attribute name: ${JSON.stringify(name)}`);\n continue;\n }\n if (EVENT_HANDLER_NAME.test(name)) {\n console.warn(\n `[coherent.js/forms] Ignoring inline event handler \"${name}\". ` +\n 'Attach handlers with hydrateForm instead.'\n );\n continue;\n }\n safe[name] = value;\n }\n return safe;\n}\n\n/** Join class names, dropping empties so no element carries `class=\"\"`. */\nfunction joinClasses(...names) {\n return names.filter(Boolean).join(' ');\n}\n\n/**\n * Form Builder\n * Helps create form components with validation\n */\nexport class FormBuilder {\n constructor(options = {}) {\n this.options = {\n validateOnChange: true,\n validateOnBlur: true,\n name: options.name || 'form',\n ...options\n };\n \n this.fields = new Map();\n this.groups = new Map();\n this.values = {};\n this.errors = {};\n this.touched = {};\n this.initialValues = {};\n this.submitHandler = null;\n this.errorHandler = null;\n this._isSubmitting = false;\n }\n\n /**\n * Add a field to the form (alias for field)\n */\n addField(name, config = {}) {\n return this.field(name, config);\n }\n\n /**\n * Add a field to the form\n */\n field(name, config = {}) {\n const fieldConfig = {\n name,\n type: config.type || 'text',\n label: config.label || name,\n placeholder: config.placeholder || '',\n defaultValue: config.defaultValue || '',\n validators: config.validators || [],\n required: config.required || false,\n visible: config.visible !== false,\n showWhen: config.showWhen,\n ...config\n };\n \n this.fields.set(name, fieldConfig);\n\n // Set default value\n if (config.defaultValue !== undefined) {\n this.values[name] = config.defaultValue;\n this.initialValues[name] = config.defaultValue;\n }\n\n return this;\n }\n\n /**\n * Remove a field from the form\n */\n removeField(name) {\n this.fields.delete(name);\n delete this.values[name];\n delete this.errors[name];\n delete this.touched[name];\n return this;\n }\n\n /**\n * Update field configuration\n */\n updateField(name, config) {\n const field = this.fields.get(name);\n if (field) {\n this.fields.set(name, { ...field, ...config });\n }\n return this;\n }\n\n /**\n * Get all fields as array\n */\n getFields() {\n return Array.from(this.fields.values());\n }\n\n /**\n * Add a field group\n */\n addGroup(name, config = {}) {\n this.groups.set(name, {\n name,\n label: config.label || name,\n fields: config.fields || [],\n ...config\n });\n\n // Add fields in the group\n if (config.fields) {\n config.fields.forEach(fieldConfig => {\n this.addField(fieldConfig.name, fieldConfig);\n });\n }\n\n return this;\n }\n\n /**\n * Get field configuration\n */\n getField(name) {\n return this.fields.get(name);\n }\n\n /**\n * Set field value\n */\n setValue(name, value) {\n this.values[name] = value;\n this.touched[name] = true;\n\n const field = this.fields.get(name);\n if (field && (field.validateOnChange || this.options.validateOnChange)) {\n this.validateField(name);\n }\n }\n\n /**\n * Set multiple values\n */\n setValues(values) {\n Object.assign(this.values, values);\n return this;\n }\n\n /**\n * Get field value\n */\n getValue(name) {\n return this.values[name];\n }\n\n /**\n * Get all values\n */\n getValues() {\n return { ...this.values };\n }\n\n /**\n * Get field error\n */\n getFieldError(name) {\n return this.errors[name];\n }\n\n /**\n * Check if form has errors\n */\n hasErrors() {\n return Object.keys(this.errors).length > 0;\n }\n\n /**\n * Clear all errors\n */\n clearErrors() {\n this.errors = {};\n return this;\n }\n\n /**\n * Check if form is dirty (values changed from initial)\n */\n isDirty() {\n return Object.keys(this.values).some(key => {\n return this.values[key] !== this.initialValues[key];\n });\n }\n\n /**\n * Check if form is valid\n */\n isValid() {\n const result = this.validate();\n return Object.keys(result).length === 0;\n }\n\n /**\n * Validate a field\n */\n validateField(name) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n // Skip validation for hidden fields (support both showWhen and showIf)\n const showCondition = field.showWhen || field.showIf;\n if (showCondition && !showCondition(this.values)) {\n delete this.errors[name];\n return null;\n }\n\n const value = this.values[name];\n \n // Check required\n if (field.required && (value === undefined || value === null || value === '')) {\n const error = 'This field is required';\n this.errors[name] = error;\n return error;\n }\n\n // Skip further validation if empty and not required\n if (!value && !field.required) {\n delete this.errors[name];\n return null;\n }\n\n // Type-based validation\n if (value) {\n if (field.type === 'email') {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(value)) {\n const error = 'Please enter a valid email address';\n this.errors[name] = error;\n return error;\n }\n } else if (field.type === 'url') {\n try {\n new URL(value);\n } catch {\n const error = 'Please enter a valid URL';\n this.errors[name] = error;\n return error;\n }\n } else if (field.type === 'number') {\n if (isNaN(Number(value))) {\n const error = 'Please enter a valid number';\n this.errors[name] = error;\n return error;\n }\n }\n }\n\n // Check custom validate function\n if (field.validate) {\n const error = field.validate(value, this.values);\n if (error) {\n this.errors[name] = error;\n return error;\n }\n }\n\n // Run validators\n for (const validator of field.validators || []) {\n const error = validator(value, this.values);\n if (error) {\n this.errors[name] = error;\n return error;\n }\n }\n\n delete this.errors[name];\n return null;\n }\n\n /**\n * Validate all fields\n */\n validate() {\n const errors = {};\n\n for (const [name] of this.fields) {\n // The same predicate buildForm() renders by, so a field that is not on\n // the page can never block submission.\n if (!this.isFieldVisible(name)) continue;\n\n const error = this.validateField(name);\n if (error) {\n errors[name] = error;\n }\n }\n\n this.errors = errors;\n return errors;\n }\n\n /**\n * Set submit handler\n */\n onSubmit(handler) {\n this.submitHandler = handler;\n return this;\n }\n\n /**\n * Set error handler\n */\n onError(handler) {\n this.errorHandler = handler;\n return this;\n }\n\n /**\n * Submit the form\n */\n async submit() {\n const errors = this.validate();\n \n if (Object.keys(errors).length > 0) {\n return { success: false, errors };\n }\n\n if (!this.submitHandler) {\n return { success: true, data: this.values };\n }\n\n this._isSubmitting = true;\n\n try {\n const result = await this.submitHandler(this.values);\n this._isSubmitting = false;\n return { success: true, data: result };\n } catch (error) {\n this._isSubmitting = false;\n if (this.errorHandler) {\n this.errorHandler(error);\n }\n return { success: false, error };\n }\n }\n\n /**\n * Serialize form data\n */\n serialize() {\n return { ...this.values };\n }\n\n /**\n * Convert form to HTML string\n */\n toHTML(options = {}) {\n // Delegates to buildForm() so there is one source of truth. The previous\n // hand-rolled string builder dropped action, method and every field\n // attribute beyond type/name/id, and interpolated values unescaped.\n return renderToHTML(this.buildForm(options));\n }\n\n /**\n * Mark field as touched\n */\n touch(name) {\n this.touched[name] = true;\n }\n\n /**\n * Build input component with validation metadata for hydration\n */\n buildInput(name, classNames = this.resolveClassNames()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const value = this.values[name] || '';\n const error = this.errors[name];\n const isTouched = this.touched[name];\n\n // Build validator names string for data-validators attribute\n const validatorNames = field.validators\n .map(v => {\n if (typeof v === 'function') return v.name || 'custom';\n if (typeof v === 'string') return v;\n return null;\n })\n .filter(Boolean)\n .join(',');\n\n const controlClass = joinClasses(\n classNames.control,\n field.className,\n error && isTouched ? classNames.invalid : null\n );\n\n const inputProps = {\n // Spread first: name, id, type and the aria-* pair below are the\n // builder's own invariants, and hydration keys off them.\n ...safeAttributes(field.attributes),\n type: field.type,\n name: field.name,\n id: field.name,\n value: value,\n 'aria-invalid': error ? 'true' : 'false',\n 'aria-describedby': error ? `${name}-error` : undefined\n };\n\n if (field.placeholder) inputProps.placeholder = field.placeholder;\n if (controlClass) inputProps.className = controlClass;\n if (field.disabled) inputProps.disabled = true;\n if (field.readonly) inputProps.readonly = true;\n\n // Add validation metadata for client-side hydration\n if (field.required) {\n inputProps.required = true;\n inputProps['data-required'] = 'true';\n }\n\n if (validatorNames) {\n inputProps['data-validators'] = validatorNames;\n }\n\n // Note: Event handlers are attached during hydration, not inline\n // This enables progressive enhancement and CSP compliance\n\n // textarea and select are elements, not input types. `type=\"textarea\"` is\n // not valid HTML \u2014 browsers render it as a single-line text box.\n if (field.type === 'textarea' || field.type === 'select') {\n const { type: _type, value: _value, ...rest } = inputProps;\n const props = { ...rest };\n\n if (field.type === 'textarea') {\n return { textarea: { ...props, text: String(value) } };\n }\n\n const { placeholder: _placeholder, ...selectProps } = props;\n\n return {\n select: {\n ...selectProps,\n children: (field.options ?? []).map(option => {\n const { value: optionValue, label = optionValue } =\n typeof option === 'object' && option !== null ? option : { value: option };\n\n return {\n option: {\n value: optionValue,\n selected: String(optionValue) === String(value) || undefined,\n text: String(label)\n }\n };\n })\n }\n };\n }\n\n return {\n input: inputProps\n };\n }\n\n /**\n * Build label component\n */\n buildLabel(name, classNames = this.resolveClassNames()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const props = { for: field.name, text: field.label };\n if (classNames.label) props.className = classNames.label;\n\n return { label: props };\n }\n\n /**\n * Build error component\n */\n buildError(name, classNames = this.resolveClassNames()) {\n const error = this.errors[name];\n const isTouched = this.touched[name];\n\n if (!error || !isTouched) return null;\n\n const props = { id: `${name}-error`, role: 'alert', text: error };\n if (classNames.error) props.className = classNames.error;\n\n return { div: props };\n }\n\n /**\n * Merge configured class names over the defaults\n */\n resolveClassNames(overrides = {}) {\n return { ...DEFAULT_CLASS_NAMES, ...this.options.classNames, ...overrides };\n }\n\n /**\n * Build complete field component\n */\n buildField(name, classNames = this.resolveClassNames()) {\n const field = this.fields.get(name);\n if (!field) return null;\n\n const children = [\n this.buildLabel(name, classNames),\n this.buildInput(name, classNames)\n ];\n\n const error = this.buildError(name, classNames);\n if (error) {\n children.push(error);\n }\n\n // data-field is the structural hook hydrateForm uses to find the wrapper,\n // so classes stay entirely the consumer's to choose.\n const props = { 'data-field': name, children };\n if (classNames.field) props.className = classNames.field;\n\n return { div: props };\n }\n\n /**\n * Build entire form\n */\n buildForm(options = {}) {\n const settings = { ...this.options, ...options };\n const classNames = this.resolveClassNames(options.classNames);\n const fields = [];\n\n for (const [name] of this.fields) {\n // validate() has always skipped fields hidden by showWhen/showIf; render\n // agreed with it only by accident, because nothing was ever hidden.\n if (!this.isFieldVisible(name)) continue;\n fields.push(this.buildField(name, classNames));\n }\n\n if (settings.submitButton !== false) {\n const button = { type: 'submit', text: settings.submitText || 'Submit' };\n if (classNames.submit) button.className = classNames.submit;\n fields.push({ button });\n }\n\n const form = {};\n\n if (settings.action) form.action = settings.action;\n if (settings.method) form.method = settings.method;\n if (settings.name) form.name = settings.name;\n if (settings.id) form.id = settings.id;\n if (settings.className) form.className = settings.className;\n if (settings.enctype) form.enctype = settings.enctype;\n\n // Plain HTML by default: the form posts to `action` and the browser runs\n // its own validation with JavaScript off. hydrateForm binds its own submit\n // listener, so it never needed the inline handler this used to emit \u2014 and\n // an inline handler breaks under a strict CSP besides.\n if (settings.enhance) {\n form.onsubmit = typeof settings.enhance === 'string'\n ? settings.enhance\n : 'handleSubmit(event)';\n }\n\n if (settings.novalidate === true) form.novalidate = true;\n\n form.children = fields;\n\n return { form };\n }\n\n /**\n * Set the form action URL\n */\n setAction(action) {\n this.options.action = action;\n return this;\n }\n\n /**\n * Set the form submission method\n */\n setMethod(method) {\n this.options.method = method;\n return this;\n }\n\n /**\n * Build the form component (alias for buildForm)\n */\n build(options = {}) {\n return this.buildForm(options);\n }\n\n /**\n * Render the form to a component (alias for buildForm)\n */\n render(options = {}) {\n return this.buildForm(options);\n }\n\n /**\n * Check if form is currently submitting\n */\n isSubmitting() {\n return this._isSubmitting;\n }\n\n /**\n * Get a field group\n */\n getGroup(name) {\n return this.groups.get(name);\n }\n\n /**\n * Check if a field is visible\n */\n isFieldVisible(name) {\n const field = this.fields.get(name);\n if (!field) return false;\n\n // visible:false wins outright \u2014 a showWhen that happens to return true\n // must not resurrect a field the caller switched off.\n if (field.visible === false) return false;\n\n // Support both showWhen and showIf\n const showCondition = field.showWhen || field.showIf;\n if (showCondition) {\n return showCondition(this.values);\n }\n\n return true;\n }\n\n /**\n * Reset form\n */\n reset() {\n // Reset to initial values or empty strings\n this.values = {};\n for (const [name, field] of this.fields) {\n if (field.defaultValue !== undefined) {\n this.values[name] = field.defaultValue;\n } else {\n this.values[name] = '';\n }\n }\n this.errors = {};\n this.touched = {};\n this._isSubmitting = false;\n return this;\n }\n}\n\n/**\n * Create a form builder\n */\nexport function createFormBuilder(options = {}) {\n const form = new FormBuilder(options);\n\n // Add fields if provided\n if (options.fields) {\n options.fields.forEach(fieldConfig => {\n form.addField(fieldConfig.name, fieldConfig);\n });\n }\n\n return form;\n}\n\n/**\n * Build a form component from a configuration object.\n *\n * Returns a renderable component, as the type declarations have always said.\n * It previously returned the FormBuilder itself, so `render(buildForm(...))`\n * threw \"Invalid component structure\". Use createFormBuilder() when you want\n * the builder.\n *\n * @param {Object} config - Form configuration; `fields` may be an array of\n * field objects or an object keyed by field name. Remaining keys (action,\n * method, name, className, enctype, submitText) configure the form element.\n * @returns {Object} A Coherent.js component\n */\nexport function buildForm(config = {}) {\n const { fields = [], ...options } = Array.isArray(config) ? { fields: config } : config;\n const builder = new FormBuilder(options);\n\n if (Array.isArray(fields)) {\n for (const field of fields) {\n if (field && field.name) builder.field(field.name, field);\n }\n } else {\n for (const [name, field] of Object.entries(fields)) {\n builder.field(name, field);\n }\n }\n\n return builder.buildForm(options);\n}\n\nexport default {\n FormBuilder,\n createFormBuilder,\n buildForm\n};\n"],
5
+ "mappings": ";AAQA,SAAS,UAAU,oBAAoB;AAOhC,IAAM,sBAAsB;AAAA;AAAA,EAEjC,OAAO;AAAA,EACP,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,OAAO;AAAA,EACP,QAAQ;AACV;AAOA,IAAM,uBAAuB;AAQ7B,IAAM,qBAAqB;AAE3B,SAAS,eAAe,YAAY;AAClC,MAAI,CAAC,cAAc,OAAO,eAAe,SAAU,QAAO,CAAC;AAE3D,QAAM,OAAO,CAAC;AACd,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACtD,QAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG;AACpC,cAAQ,KAAK,wDAAwD,KAAK,UAAU,IAAI,CAAC,EAAE;AAC3F;AAAA,IACF;AACA,QAAI,mBAAmB,KAAK,IAAI,GAAG;AACjC,cAAQ;AAAA,QACN,sDAAsD,IAAI;AAAA,MAE5D;AACA;AAAA,IACF;AACA,SAAK,IAAI,IAAI;AAAA,EACf;AACA,SAAO;AACT;AAGA,SAAS,eAAe,OAAO;AAC7B,SAAO,MAAM,OAAO,OAAO,EAAE,KAAK,GAAG;AACvC;AAMO,IAAM,cAAN,MAAkB;AAAA,EACvB,YAAY,UAAU,CAAC,GAAG;AACxB,SAAK,UAAU;AAAA,MACb,kBAAkB;AAAA,MAClB,gBAAgB;AAAA,MAChB,MAAM,QAAQ,QAAQ;AAAA,MACtB,GAAG;AAAA,IACL;AAEA,SAAK,SAAS,oBAAI,IAAI;AACtB,SAAK,SAAS,oBAAI,IAAI;AACtB,SAAK,SAAS,CAAC;AACf,SAAK,SAAS,CAAC;AACf,SAAK,UAAU,CAAC;AAChB,SAAK,gBAAgB,CAAC;AACtB,SAAK,gBAAgB;AACrB,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAM,SAAS,CAAC,GAAG;AAC1B,WAAO,KAAK,MAAM,MAAM,MAAM;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,SAAS,CAAC,GAAG;AACvB,UAAM,cAAc;AAAA,MAClB;AAAA,MACA,MAAM,OAAO,QAAQ;AAAA,MACrB,OAAO,OAAO,SAAS;AAAA,MACvB,aAAa,OAAO,eAAe;AAAA,MACnC,cAAc,OAAO,gBAAgB;AAAA,MACrC,YAAY,OAAO,cAAc,CAAC;AAAA,MAClC,UAAU,OAAO,YAAY;AAAA,MAC7B,SAAS,OAAO,YAAY;AAAA,MAC5B,UAAU,OAAO;AAAA,MACjB,GAAG;AAAA,IACL;AAEA,SAAK,OAAO,IAAI,MAAM,WAAW;AAGjC,QAAI,OAAO,iBAAiB,QAAW;AACrC,WAAK,OAAO,IAAI,IAAI,OAAO;AAC3B,WAAK,cAAc,IAAI,IAAI,OAAO;AAAA,IACpC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,MAAM;AAChB,SAAK,OAAO,OAAO,IAAI;AACvB,WAAO,KAAK,OAAO,IAAI;AACvB,WAAO,KAAK,OAAO,IAAI;AACvB,WAAO,KAAK,QAAQ,IAAI;AACxB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,MAAM,QAAQ;AACxB,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,OAAO;AACT,WAAK,OAAO,IAAI,MAAM,EAAE,GAAG,OAAO,GAAG,OAAO,CAAC;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY;AACV,WAAO,MAAM,KAAK,KAAK,OAAO,OAAO,CAAC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAM,SAAS,CAAC,GAAG;AAC1B,SAAK,OAAO,IAAI,MAAM;AAAA,MACpB;AAAA,MACA,OAAO,OAAO,SAAS;AAAA,MACvB,QAAQ,OAAO,UAAU,CAAC;AAAA,MAC1B,GAAG;AAAA,IACL,CAAC;AAGD,QAAI,OAAO,QAAQ;AACjB,aAAO,OAAO,QAAQ,iBAAe;AACnC,aAAK,SAAS,YAAY,MAAM,WAAW;AAAA,MAC7C,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAM;AACb,WAAO,KAAK,OAAO,IAAI,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAM,OAAO;AACpB,SAAK,OAAO,IAAI,IAAI;AACpB,SAAK,QAAQ,IAAI,IAAI;AAErB,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,UAAU,MAAM,oBAAoB,KAAK,QAAQ,mBAAmB;AACtE,WAAK,cAAc,IAAI;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAAQ;AAChB,WAAO,OAAO,KAAK,QAAQ,MAAM;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAM;AACb,WAAO,KAAK,OAAO,IAAI;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY;AACV,WAAO,EAAE,GAAG,KAAK,OAAO;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,MAAM;AAClB,WAAO,KAAK,OAAO,IAAI;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY;AACV,WAAO,OAAO,KAAK,KAAK,MAAM,EAAE,SAAS;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc;AACZ,SAAK,SAAS,CAAC;AACf,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU;AACR,WAAO,OAAO,KAAK,KAAK,MAAM,EAAE,KAAK,SAAO;AAC1C,aAAO,KAAK,OAAO,GAAG,MAAM,KAAK,cAAc,GAAG;AAAA,IACpD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU;AACR,UAAM,SAAS,KAAK,SAAS;AAC7B,WAAO,OAAO,KAAK,MAAM,EAAE,WAAW;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,MAAM;AAClB,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,CAAC,MAAO,QAAO;AAGnB,UAAM,gBAAgB,MAAM,YAAY,MAAM;AAC9C,QAAI,iBAAiB,CAAC,cAAc,KAAK,MAAM,GAAG;AAChD,aAAO,KAAK,OAAO,IAAI;AACvB,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,KAAK,OAAO,IAAI;AAG9B,QAAI,MAAM,aAAa,UAAU,UAAa,UAAU,QAAQ,UAAU,KAAK;AAC7E,YAAM,QAAQ;AACd,WAAK,OAAO,IAAI,IAAI;AACpB,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,SAAS,CAAC,MAAM,UAAU;AAC7B,aAAO,KAAK,OAAO,IAAI;AACvB,aAAO;AAAA,IACT;AAGA,QAAI,OAAO;AACT,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,aAAa;AACnB,YAAI,CAAC,WAAW,KAAK,KAAK,GAAG;AAC3B,gBAAM,QAAQ;AACd,eAAK,OAAO,IAAI,IAAI;AACpB,iBAAO;AAAA,QACT;AAAA,MACF,WAAW,MAAM,SAAS,OAAO;AAC/B,YAAI;AACF,cAAI,IAAI,KAAK;AAAA,QACf,QAAQ;AACN,gBAAM,QAAQ;AACd,eAAK,OAAO,IAAI,IAAI;AACpB,iBAAO;AAAA,QACT;AAAA,MACF,WAAW,MAAM,SAAS,UAAU;AAClC,YAAI,MAAM,OAAO,KAAK,CAAC,GAAG;AACxB,gBAAM,QAAQ;AACd,eAAK,OAAO,IAAI,IAAI;AACpB,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAGA,QAAI,MAAM,UAAU;AAClB,YAAM,QAAQ,MAAM,SAAS,OAAO,KAAK,MAAM;AAC/C,UAAI,OAAO;AACT,aAAK,OAAO,IAAI,IAAI;AACpB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,eAAW,aAAa,MAAM,cAAc,CAAC,GAAG;AAC9C,YAAM,QAAQ,UAAU,OAAO,KAAK,MAAM;AAC1C,UAAI,OAAO;AACT,aAAK,OAAO,IAAI,IAAI;AACpB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,KAAK,OAAO,IAAI;AACvB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW;AACT,UAAM,SAAS,CAAC;AAEhB,eAAW,CAAC,IAAI,KAAK,KAAK,QAAQ;AAGhC,UAAI,CAAC,KAAK,eAAe,IAAI,EAAG;AAEhC,YAAM,QAAQ,KAAK,cAAc,IAAI;AACrC,UAAI,OAAO;AACT,eAAO,IAAI,IAAI;AAAA,MACjB;AAAA,IACF;AAEA,SAAK,SAAS;AACd,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAAS;AAChB,SAAK,gBAAgB;AACrB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,SAAS;AACf,SAAK,eAAe;AACpB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS;AACb,UAAM,SAAS,KAAK,SAAS;AAE7B,QAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAAG;AAClC,aAAO,EAAE,SAAS,OAAO,OAAO;AAAA,IAClC;AAEA,QAAI,CAAC,KAAK,eAAe;AACvB,aAAO,EAAE,SAAS,MAAM,MAAM,KAAK,OAAO;AAAA,IAC5C;AAEA,SAAK,gBAAgB;AAErB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,cAAc,KAAK,MAAM;AACnD,WAAK,gBAAgB;AACrB,aAAO,EAAE,SAAS,MAAM,MAAM,OAAO;AAAA,IACvC,SAAS,OAAO;AACd,WAAK,gBAAgB;AACrB,UAAI,KAAK,cAAc;AACrB,aAAK,aAAa,KAAK;AAAA,MACzB;AACA,aAAO,EAAE,SAAS,OAAO,MAAM;AAAA,IACjC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY;AACV,WAAO,EAAE,GAAG,KAAK,OAAO;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,UAAU,CAAC,GAAG;AAInB,WAAO,aAAa,KAAK,UAAU,OAAO,CAAC;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM;AACV,SAAK,QAAQ,IAAI,IAAI;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAM,aAAa,KAAK,kBAAkB,GAAG;AACtD,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,QAAQ,KAAK,OAAO,IAAI,KAAK;AACnC,UAAM,QAAQ,KAAK,OAAO,IAAI;AAC9B,UAAM,YAAY,KAAK,QAAQ,IAAI;AAGnC,UAAM,iBAAiB,MAAM,WAC1B,IAAI,OAAK;AACR,UAAI,OAAO,MAAM,WAAY,QAAO,EAAE,QAAQ;AAC9C,UAAI,OAAO,MAAM,SAAU,QAAO;AAClC,aAAO;AAAA,IACT,CAAC,EACA,OAAO,OAAO,EACd,KAAK,GAAG;AAEX,UAAM,eAAe;AAAA,MACnB,WAAW;AAAA,MACX,MAAM;AAAA,MACN,SAAS,YAAY,WAAW,UAAU;AAAA,IAC5C;AAEA,UAAM,aAAa;AAAA;AAAA;AAAA,MAGjB,GAAG,eAAe,MAAM,UAAU;AAAA,MAClC,MAAM,MAAM;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,IAAI,MAAM;AAAA,MACV;AAAA,MACA,gBAAgB,QAAQ,SAAS;AAAA,MACjC,oBAAoB,QAAQ,GAAG,IAAI,WAAW;AAAA,IAChD;AAEA,QAAI,MAAM,YAAa,YAAW,cAAc,MAAM;AACtD,QAAI,aAAc,YAAW,YAAY;AACzC,QAAI,MAAM,SAAU,YAAW,WAAW;AAC1C,QAAI,MAAM,SAAU,YAAW,WAAW;AAG1C,QAAI,MAAM,UAAU;AAClB,iBAAW,WAAW;AACtB,iBAAW,eAAe,IAAI;AAAA,IAChC;AAEA,QAAI,gBAAgB;AAClB,iBAAW,iBAAiB,IAAI;AAAA,IAClC;AAOA,QAAI,MAAM,SAAS,cAAc,MAAM,SAAS,UAAU;AACxD,YAAM,EAAE,MAAM,OAAO,OAAO,QAAQ,GAAG,KAAK,IAAI;AAChD,YAAM,QAAQ,EAAE,GAAG,KAAK;AAExB,UAAI,MAAM,SAAS,YAAY;AAC7B,eAAO,EAAE,UAAU,EAAE,GAAG,OAAO,MAAM,OAAO,KAAK,EAAE,EAAE;AAAA,MACvD;AAEA,YAAM,EAAE,aAAa,cAAc,GAAG,YAAY,IAAI;AAEtD,aAAO;AAAA,QACL,QAAQ;AAAA,UACN,GAAG;AAAA,UACH,WAAW,MAAM,WAAW,CAAC,GAAG,IAAI,YAAU;AAC5C,kBAAM,EAAE,OAAO,aAAa,QAAQ,YAAY,IAC9C,OAAO,WAAW,YAAY,WAAW,OAAO,SAAS,EAAE,OAAO,OAAO;AAE3E,mBAAO;AAAA,cACL,QAAQ;AAAA,gBACN,OAAO;AAAA,gBACP,UAAU,OAAO,WAAW,MAAM,OAAO,KAAK,KAAK;AAAA,gBACnD,MAAM,OAAO,KAAK;AAAA,cACpB;AAAA,YACF;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAM,aAAa,KAAK,kBAAkB,GAAG;AACtD,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,QAAQ,EAAE,KAAK,MAAM,MAAM,MAAM,MAAM,MAAM;AACnD,QAAI,WAAW,MAAO,OAAM,YAAY,WAAW;AAEnD,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAM,aAAa,KAAK,kBAAkB,GAAG;AACtD,UAAM,QAAQ,KAAK,OAAO,IAAI;AAC9B,UAAM,YAAY,KAAK,QAAQ,IAAI;AAEnC,QAAI,CAAC,SAAS,CAAC,UAAW,QAAO;AAEjC,UAAM,QAAQ,EAAE,IAAI,GAAG,IAAI,UAAU,MAAM,SAAS,MAAM,MAAM;AAChE,QAAI,WAAW,MAAO,OAAM,YAAY,WAAW;AAEnD,WAAO,EAAE,KAAK,MAAM;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB,YAAY,CAAC,GAAG;AAChC,WAAO,EAAE,GAAG,qBAAqB,GAAG,KAAK,QAAQ,YAAY,GAAG,UAAU;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAM,aAAa,KAAK,kBAAkB,GAAG;AACtD,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,WAAW;AAAA,MACf,KAAK,WAAW,MAAM,UAAU;AAAA,MAChC,KAAK,WAAW,MAAM,UAAU;AAAA,IAClC;AAEA,UAAM,QAAQ,KAAK,WAAW,MAAM,UAAU;AAC9C,QAAI,OAAO;AACT,eAAS,KAAK,KAAK;AAAA,IACrB;AAIA,UAAM,QAAQ,EAAE,cAAc,MAAM,SAAS;AAC7C,QAAI,WAAW,MAAO,OAAM,YAAY,WAAW;AAEnD,WAAO,EAAE,KAAK,MAAM;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,UAAU,CAAC,GAAG;AACtB,UAAM,WAAW,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ;AAC/C,UAAM,aAAa,KAAK,kBAAkB,QAAQ,UAAU;AAC5D,UAAM,SAAS,CAAC;AAEhB,eAAW,CAAC,IAAI,KAAK,KAAK,QAAQ;AAGhC,UAAI,CAAC,KAAK,eAAe,IAAI,EAAG;AAChC,aAAO,KAAK,KAAK,WAAW,MAAM,UAAU,CAAC;AAAA,IAC/C;AAEA,QAAI,SAAS,iBAAiB,OAAO;AACnC,YAAM,SAAS,EAAE,MAAM,UAAU,MAAM,SAAS,cAAc,SAAS;AACvE,UAAI,WAAW,OAAQ,QAAO,YAAY,WAAW;AACrD,aAAO,KAAK,EAAE,OAAO,CAAC;AAAA,IACxB;AAEA,UAAM,OAAO,CAAC;AAEd,QAAI,SAAS,OAAQ,MAAK,SAAS,SAAS;AAC5C,QAAI,SAAS,OAAQ,MAAK,SAAS,SAAS;AAC5C,QAAI,SAAS,KAAM,MAAK,OAAO,SAAS;AACxC,QAAI,SAAS,GAAI,MAAK,KAAK,SAAS;AACpC,QAAI,SAAS,UAAW,MAAK,YAAY,SAAS;AAClD,QAAI,SAAS,QAAS,MAAK,UAAU,SAAS;AAM9C,QAAI,SAAS,SAAS;AACpB,WAAK,WAAW,OAAO,SAAS,YAAY,WACxC,SAAS,UACT;AAAA,IACN;AAEA,QAAI,SAAS,eAAe,KAAM,MAAK,aAAa;AAEpD,SAAK,WAAW;AAEhB,WAAO,EAAE,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAAQ;AAChB,SAAK,QAAQ,SAAS;AACtB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAAQ;AAChB,SAAK,QAAQ,SAAS;AACtB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,CAAC,GAAG;AAClB,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,UAAU,CAAC,GAAG;AACnB,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe;AACb,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAM;AACb,WAAO,KAAK,OAAO,IAAI,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,MAAM;AACnB,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,CAAC,MAAO,QAAO;AAInB,QAAI,MAAM,YAAY,MAAO,QAAO;AAGpC,UAAM,gBAAgB,MAAM,YAAY,MAAM;AAC9C,QAAI,eAAe;AACjB,aAAO,cAAc,KAAK,MAAM;AAAA,IAClC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ;AAEN,SAAK,SAAS,CAAC;AACf,eAAW,CAAC,MAAM,KAAK,KAAK,KAAK,QAAQ;AACvC,UAAI,MAAM,iBAAiB,QAAW;AACpC,aAAK,OAAO,IAAI,IAAI,MAAM;AAAA,MAC5B,OAAO;AACL,aAAK,OAAO,IAAI,IAAI;AAAA,MACtB;AAAA,IACF;AACA,SAAK,SAAS,CAAC;AACf,SAAK,UAAU,CAAC;AAChB,SAAK,gBAAgB;AACrB,WAAO;AAAA,EACT;AACF;AAKO,SAAS,kBAAkB,UAAU,CAAC,GAAG;AAC9C,QAAM,OAAO,IAAI,YAAY,OAAO;AAGpC,MAAI,QAAQ,QAAQ;AAClB,YAAQ,OAAO,QAAQ,iBAAe;AACpC,WAAK,SAAS,YAAY,MAAM,WAAW;AAAA,IAC7C,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAeO,SAAS,UAAU,SAAS,CAAC,GAAG;AACrC,QAAM,EAAE,SAAS,CAAC,GAAG,GAAG,QAAQ,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE,QAAQ,OAAO,IAAI;AACjF,QAAM,UAAU,IAAI,YAAY,OAAO;AAEvC,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,eAAW,SAAS,QAAQ;AAC1B,UAAI,SAAS,MAAM,KAAM,SAAQ,MAAM,MAAM,MAAM,KAAK;AAAA,IAC1D;AAAA,EACF,OAAO;AACL,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,cAAQ,MAAM,MAAM,KAAK;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO,QAAQ,UAAU,OAAO;AAClC;AAEA,IAAO,uBAAQ;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -300,6 +300,21 @@ var validators = {
300
300
  }
301
301
  };
302
302
 
303
+ // src/form-builder.js
304
+ import { render as renderToHTML } from "@coherent.js/core";
305
+ var DEFAULT_CLASS_NAMES = {
306
+ /** Wrapper around label, control and error */
307
+ field: "form-field",
308
+ label: "",
309
+ /** Base class on the control, before any per-field className */
310
+ control: "",
311
+ /** Added to the control while it has a visible error */
312
+ invalid: "error",
313
+ /** The error message element */
314
+ error: "error-message",
315
+ submit: "submit-button"
316
+ };
317
+
303
318
  // src/form-hydration.js
304
319
  function hydrateForm(formSelector, options = {}) {
305
320
  if (typeof document === "undefined") {
@@ -317,7 +332,14 @@ function hydrateForm(formSelector, options = {}) {
317
332
  validateOnSubmit: true,
318
333
  showErrorsOnTouch: true,
319
334
  debounce: 300,
320
- ...options
335
+ ...options,
336
+ // Must match whatever the server rendered, so pass the same names given to
337
+ // buildForm's `classNames` when they were customised.
338
+ classNames: {
339
+ invalid: DEFAULT_CLASS_NAMES.invalid,
340
+ error: DEFAULT_CLASS_NAMES.error,
341
+ ...options.classNames
342
+ }
321
343
  };
322
344
  const state = {
323
345
  values: {},
@@ -361,10 +383,10 @@ function hydrateForm(formSelector, options = {}) {
361
383
  function createErrorElement(name, inputElement) {
362
384
  const errorDiv = document.createElement("div");
363
385
  errorDiv.id = `${name}-error`;
364
- errorDiv.className = "error-message";
386
+ if (opts.classNames.error) errorDiv.className = opts.classNames.error;
365
387
  errorDiv.setAttribute("role", "alert");
366
388
  errorDiv.style.display = "none";
367
- const fieldWrapper = inputElement.closest(".form-field") || inputElement.parentElement;
389
+ const fieldWrapper = inputElement.closest("[data-field]") || inputElement.parentElement;
368
390
  fieldWrapper.appendChild(errorDiv);
369
391
  return errorDiv;
370
392
  }
@@ -413,6 +435,9 @@ function hydrateForm(formSelector, options = {}) {
413
435
  displayError(name, error);
414
436
  return !error;
415
437
  }
438
+ function invalidClasses() {
439
+ return opts.classNames.invalid ? opts.classNames.invalid.trim().split(/\s+/) : [];
440
+ }
416
441
  function displayError(name, error) {
417
442
  const field = state.fields.get(name);
418
443
  if (!field) return;
@@ -421,12 +446,12 @@ function hydrateForm(formSelector, options = {}) {
421
446
  errorElement.textContent = error;
422
447
  errorElement.style.display = "block";
423
448
  element.setAttribute("aria-invalid", "true");
424
- element.classList.add("error");
449
+ invalidClasses().forEach((name2) => element.classList.add(name2));
425
450
  } else {
426
451
  errorElement.textContent = "";
427
452
  errorElement.style.display = "none";
428
453
  element.setAttribute("aria-invalid", "false");
429
- element.classList.remove("error");
454
+ invalidClasses().forEach((name2) => element.classList.remove(name2));
430
455
  }
431
456
  }
432
457
  function validateForm() {