@coherent.js/forms 1.0.1 → 1.1.2

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,49 @@
1
1
  // src/form-builder.js
2
2
  import { render as renderToHTML } from "@coherent.js/core";
3
+
4
+ // src/patterns.js
5
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
6
+ var EMAIL_MAX_LENGTH = 254;
7
+ function isEmailShaped(value) {
8
+ return typeof value === "string" && value.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(value);
9
+ }
10
+
11
+ // src/form-builder.js
12
+ var DEFAULT_CLASS_NAMES = {
13
+ /** Wrapper around label, control and error */
14
+ field: "form-field",
15
+ label: "",
16
+ /** Base class on the control, before any per-field className */
17
+ control: "",
18
+ /** Added to the control while it has a visible error */
19
+ invalid: "error",
20
+ /** The error message element */
21
+ error: "error-message",
22
+ submit: "submit-button"
23
+ };
24
+ var VALID_ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;
25
+ var EVENT_HANDLER_NAME = /^on/i;
26
+ function safeAttributes(attributes) {
27
+ if (!attributes || typeof attributes !== "object") return {};
28
+ const safe = {};
29
+ for (const [name, value] of Object.entries(attributes)) {
30
+ if (!VALID_ATTRIBUTE_NAME.test(name)) {
31
+ console.warn(`[coherent.js/forms] Ignoring invalid attribute name: ${JSON.stringify(name)}`);
32
+ continue;
33
+ }
34
+ if (EVENT_HANDLER_NAME.test(name)) {
35
+ console.warn(
36
+ `[coherent.js/forms] Ignoring inline event handler "${name}". Attach handlers with hydrateForm instead.`
37
+ );
38
+ continue;
39
+ }
40
+ safe[name] = value;
41
+ }
42
+ return safe;
43
+ }
44
+ function joinClasses(...names) {
45
+ return names.filter(Boolean).join(" ");
46
+ }
3
47
  var FormBuilder = class {
4
48
  constructor(options = {}) {
5
49
  this.options = {
@@ -183,8 +227,7 @@ var FormBuilder = class {
183
227
  }
184
228
  if (value) {
185
229
  if (field.type === "email") {
186
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
187
- if (!emailRegex.test(value)) {
230
+ if (!isEmailShaped(value)) {
188
231
  const error = "Please enter a valid email address";
189
232
  this.errors[name] = error;
190
233
  return error;
@@ -227,11 +270,8 @@ var FormBuilder = class {
227
270
  */
228
271
  validate() {
229
272
  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
- }
273
+ for (const [name] of this.fields) {
274
+ if (!this.isFieldVisible(name)) continue;
235
275
  const error = this.validateField(name);
236
276
  if (error) {
237
277
  errors[name] = error;
@@ -299,7 +339,7 @@ var FormBuilder = class {
299
339
  /**
300
340
  * Build input component with validation metadata for hydration
301
341
  */
302
- buildInput(name) {
342
+ buildInput(name, classNames = this.resolveClassNames()) {
303
343
  const field = this.fields.get(name);
304
344
  if (!field) return null;
305
345
  const value = this.values[name] || "";
@@ -310,16 +350,26 @@ var FormBuilder = class {
310
350
  if (typeof v === "string") return v;
311
351
  return null;
312
352
  }).filter(Boolean).join(",");
353
+ const controlClass = joinClasses(
354
+ classNames.control,
355
+ field.className,
356
+ error && isTouched ? classNames.invalid : null
357
+ );
313
358
  const inputProps = {
359
+ // Spread first: name, id, type and the aria-* pair below are the
360
+ // builder's own invariants, and hydration keys off them.
361
+ ...safeAttributes(field.attributes),
314
362
  type: field.type,
315
363
  name: field.name,
316
364
  id: field.name,
317
365
  value,
318
- placeholder: field.placeholder,
319
366
  "aria-invalid": error ? "true" : "false",
320
- "aria-describedby": error ? `${name}-error` : void 0,
321
- className: error && isTouched ? "error" : ""
367
+ "aria-describedby": error ? `${name}-error` : void 0
322
368
  };
369
+ if (field.placeholder) inputProps.placeholder = field.placeholder;
370
+ if (controlClass) inputProps.className = controlClass;
371
+ if (field.disabled) inputProps.disabled = true;
372
+ if (field.readonly) inputProps.readonly = true;
323
373
  if (field.required) {
324
374
  inputProps.required = true;
325
375
  inputProps["data-required"] = "true";
@@ -357,71 +407,63 @@ var FormBuilder = class {
357
407
  /**
358
408
  * Build label component
359
409
  */
360
- buildLabel(name) {
410
+ buildLabel(name, classNames = this.resolveClassNames()) {
361
411
  const field = this.fields.get(name);
362
412
  if (!field) return null;
363
- return {
364
- label: {
365
- for: field.name,
366
- text: field.label
367
- }
368
- };
413
+ const props = { for: field.name, text: field.label };
414
+ if (classNames.label) props.className = classNames.label;
415
+ return { label: props };
369
416
  }
370
417
  /**
371
418
  * Build error component
372
419
  */
373
- buildError(name) {
420
+ buildError(name, classNames = this.resolveClassNames()) {
374
421
  const error = this.errors[name];
375
422
  const isTouched = this.touched[name];
376
423
  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
- };
424
+ const props = { id: `${name}-error`, role: "alert", text: error };
425
+ if (classNames.error) props.className = classNames.error;
426
+ return { div: props };
427
+ }
428
+ /**
429
+ * Merge configured class names over the defaults
430
+ */
431
+ resolveClassNames(overrides = {}) {
432
+ return { ...DEFAULT_CLASS_NAMES, ...this.options.classNames, ...overrides };
385
433
  }
386
434
  /**
387
435
  * Build complete field component
388
436
  */
389
- buildField(name) {
437
+ buildField(name, classNames = this.resolveClassNames()) {
390
438
  const field = this.fields.get(name);
391
439
  if (!field) return null;
392
440
  const children = [
393
- this.buildLabel(name),
394
- this.buildInput(name)
441
+ this.buildLabel(name, classNames),
442
+ this.buildInput(name, classNames)
395
443
  ];
396
- const error = this.buildError(name);
444
+ const error = this.buildError(name, classNames);
397
445
  if (error) {
398
446
  children.push(error);
399
447
  }
400
- return {
401
- div: {
402
- className: "form-field",
403
- "data-field": name,
404
- children
405
- }
406
- };
448
+ const props = { "data-field": name, children };
449
+ if (classNames.field) props.className = classNames.field;
450
+ return { div: props };
407
451
  }
408
452
  /**
409
453
  * Build entire form
410
454
  */
411
455
  buildForm(options = {}) {
456
+ const settings = { ...this.options, ...options };
457
+ const classNames = this.resolveClassNames(options.classNames);
412
458
  const fields = [];
413
459
  for (const [name] of this.fields) {
414
- fields.push(this.buildField(name));
460
+ if (!this.isFieldVisible(name)) continue;
461
+ fields.push(this.buildField(name, classNames));
415
462
  }
416
- const settings = { ...this.options, ...options };
417
463
  if (settings.submitButton !== false) {
418
- fields.push({
419
- button: {
420
- type: "submit",
421
- text: settings.submitText || "Submit",
422
- className: "submit-button"
423
- }
424
- });
464
+ const button = { type: "submit", text: settings.submitText || "Submit" };
465
+ if (classNames.submit) button.className = classNames.submit;
466
+ fields.push({ button });
425
467
  }
426
468
  const form = {};
427
469
  if (settings.action) form.action = settings.action;
@@ -430,8 +472,10 @@ var FormBuilder = class {
430
472
  if (settings.id) form.id = settings.id;
431
473
  if (settings.className) form.className = settings.className;
432
474
  if (settings.enctype) form.enctype = settings.enctype;
433
- form.onsubmit = "handleSubmit(event)";
434
- form.novalidate = true;
475
+ if (settings.enhance) {
476
+ form.onsubmit = typeof settings.enhance === "string" ? settings.enhance : "handleSubmit(event)";
477
+ }
478
+ if (settings.novalidate === true) form.novalidate = true;
435
479
  form.children = fields;
436
480
  return { form };
437
481
  }
@@ -479,11 +523,12 @@ var FormBuilder = class {
479
523
  isFieldVisible(name) {
480
524
  const field = this.fields.get(name);
481
525
  if (!field) return false;
526
+ if (field.visible === false) return false;
482
527
  const showCondition = field.showWhen || field.showIf;
483
528
  if (showCondition) {
484
529
  return showCondition(this.values);
485
530
  }
486
- return field.visible !== false;
531
+ return true;
487
532
  }
488
533
  /**
489
534
  * Reset form
@@ -532,6 +577,7 @@ var form_builder_default = {
532
577
  buildForm
533
578
  };
534
579
  export {
580
+ DEFAULT_CLASS_NAMES,
535
581
  FormBuilder,
536
582
  buildForm,
537
583
  createFormBuilder,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 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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",
3
+ "sources": ["../src/form-builder.js", "../src/patterns.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';\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 * 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"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,10 @@
1
+ // src/patterns.js
2
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
3
+ var EMAIL_MAX_LENGTH = 254;
4
+ function isEmailShaped(value) {
5
+ return typeof value === "string" && value.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(value);
6
+ }
7
+
1
8
  // src/validators.js
2
9
  var validators = {
3
10
  required: (value, options = {}) => {
@@ -8,8 +15,7 @@ var validators = {
8
15
  },
9
16
  email: (value) => {
10
17
  if (!value) return null;
11
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
- if (!emailRegex.test(value)) {
18
+ if (!isEmailShaped(value)) {
13
19
  return "Please enter a valid email address";
14
20
  }
15
21
  return null;
@@ -300,6 +306,21 @@ var validators = {
300
306
  }
301
307
  };
302
308
 
309
+ // src/form-builder.js
310
+ import { render as renderToHTML } from "@coherent.js/core";
311
+ var DEFAULT_CLASS_NAMES = {
312
+ /** Wrapper around label, control and error */
313
+ field: "form-field",
314
+ label: "",
315
+ /** Base class on the control, before any per-field className */
316
+ control: "",
317
+ /** Added to the control while it has a visible error */
318
+ invalid: "error",
319
+ /** The error message element */
320
+ error: "error-message",
321
+ submit: "submit-button"
322
+ };
323
+
303
324
  // src/form-hydration.js
304
325
  function hydrateForm(formSelector, options = {}) {
305
326
  if (typeof document === "undefined") {
@@ -317,7 +338,14 @@ function hydrateForm(formSelector, options = {}) {
317
338
  validateOnSubmit: true,
318
339
  showErrorsOnTouch: true,
319
340
  debounce: 300,
320
- ...options
341
+ ...options,
342
+ // Must match whatever the server rendered, so pass the same names given to
343
+ // buildForm's `classNames` when they were customised.
344
+ classNames: {
345
+ invalid: DEFAULT_CLASS_NAMES.invalid,
346
+ error: DEFAULT_CLASS_NAMES.error,
347
+ ...options.classNames
348
+ }
321
349
  };
322
350
  const state = {
323
351
  values: {},
@@ -361,10 +389,10 @@ function hydrateForm(formSelector, options = {}) {
361
389
  function createErrorElement(name, inputElement) {
362
390
  const errorDiv = document.createElement("div");
363
391
  errorDiv.id = `${name}-error`;
364
- errorDiv.className = "error-message";
392
+ if (opts.classNames.error) errorDiv.className = opts.classNames.error;
365
393
  errorDiv.setAttribute("role", "alert");
366
394
  errorDiv.style.display = "none";
367
- const fieldWrapper = inputElement.closest(".form-field") || inputElement.parentElement;
395
+ const fieldWrapper = inputElement.closest("[data-field]") || inputElement.parentElement;
368
396
  fieldWrapper.appendChild(errorDiv);
369
397
  return errorDiv;
370
398
  }
@@ -413,6 +441,9 @@ function hydrateForm(formSelector, options = {}) {
413
441
  displayError(name, error);
414
442
  return !error;
415
443
  }
444
+ function invalidClasses() {
445
+ return opts.classNames.invalid ? opts.classNames.invalid.trim().split(/\s+/) : [];
446
+ }
416
447
  function displayError(name, error) {
417
448
  const field = state.fields.get(name);
418
449
  if (!field) return;
@@ -421,12 +452,12 @@ function hydrateForm(formSelector, options = {}) {
421
452
  errorElement.textContent = error;
422
453
  errorElement.style.display = "block";
423
454
  element.setAttribute("aria-invalid", "true");
424
- element.classList.add("error");
455
+ invalidClasses().forEach((name2) => element.classList.add(name2));
425
456
  } else {
426
457
  errorElement.textContent = "";
427
458
  errorElement.style.display = "none";
428
459
  element.setAttribute("aria-invalid", "false");
429
- element.classList.remove("error");
460
+ invalidClasses().forEach((name2) => element.classList.remove(name2));
430
461
  }
431
462
  }
432
463
  function validateForm() {