# DOM Helpers Form Module — Production-Grade Fixes (v1.1.1)

This package contains a fixed version of `01_dh-form.js` and
`02_dh-form-enhance.js`, plus a rebuilt drop-in replacement for
`dom-helpers.full-spa.min.js`. Everything here was found by reading both
files in full, fixed in the source, and verified with an automated jsdom
test suite (13/13 passing) — not just inspected by eye.

## Files in this package

| File | What it is |
|---|---|
| `01_dh-form.js` | Fixed base form module (v1.1.1) |
| `02_dh-form-enhance.js` | Fixed enhancement module (v1.1.1) |
| `dom-helpers.full-spa.fixed.js` | Full rebuilt bundle, unminified — drop-in replacement for your CDN `<script>` |
| `dom-helpers.full-spa.fixed.min.js` | Same, minified — this is the one to actually deploy |
| `concat-build.js` | The build script used to produce the bundles (see "A build-process bug" below) |
| `test.js` | The automated test suite that verifies every fix (needs `npm install jsdom` to run) |

## How to use it

Replace your CDN script tag:
```html
<!-- Before -->
<script src="https://cdn.jsdelivr.net/npm/dom-helpers-js@2.10.0/dist/dom-helpers.full-spa.min.js"></script>

<!-- After -->
<script src="./dom-helpers.full-spa.fixed.min.js"></script>
```
Nothing else changes — every existing call (`Forms.myForm`, `.values`,
`.validate()`, `.submitData()`, etc.) works exactly the same; it just no
longer crashes or silently drops data in the situations below.

---

## Bugs fixed

### 1. The crash you hit — `form.update = ...` on a locked property
`01_dh-core.js` locks `update` as non-writable when it first enhances an
element. The form module then tried to **overwrite** it with a plain
assignment, which throws under `'use strict'`. Fixed with a `_defineSafe()`
helper that uses `Object.defineProperty` instead.

### 2. Silent file loss on form submit
Both `submitData()` (base file) and `enhancedSubmit()`'s default handler
(enhance file) always sent `JSON.stringify(values)`. A `File` object
serializes to `{}` under JSON — so any form with `<input type="file">`
silently lost the file on submit, with no error. Both now detect File/Blob
values automatically and switch to a real multipart `FormData` body instead.

### 3. CSS-selector injection via unescaped field names
`getFormField()`, `_setFormField()`, `_markFieldInvalid()`, and
`connectReactiveForm()`'s two lookups all built selectors like
`[name="${name}"]` via string interpolation — a field name containing a
quote character (or coming from untrusted dynamic data) could break the
selector or behave unexpectedly. Now uses `form.elements` (the native,
injection-proof way to look up form fields by name) wherever possible, and
`CSS.escape()` consistently everywhere a real selector is still needed.

### 4. Global config silently never reaching already-touched forms
`Forms.enhance.configure({...})` is documented as setting library-wide
defaults, but the whole global config was **snapshotted** into a form's
internal state the first time that form was accessed — so calling
`configure()` *after* any interaction with a form (very easy to do
accidentally) had no effect on it. Per-form state now stores only explicit
overrides; the effective config is resolved fresh (`globals < overrides <
call-site opts`) on every read.

### 5. A second, more serious Proxy bug — `Forms.enhance`/`.validators` were always `null`
While fixing #4, I found this one wasn't caused by application code — it's a
gap in the `Forms` Proxy itself. The `get` trap only let a hardcoded
allowlist of names (`'helper'`, `'addEnhancer'`) or *functions* pass through
to the real object; anything else — including `Forms.enhance`,
`.enhancements`, `.validators`, and `.v`, all of which are **plain objects**,
not functions — silently fell through to the form-lookup path and resolved
to `null`, since none of those names match a real `<form id="...">` on the
page. In effect, the entire `Forms.enhance.*`/`Forms.validators.*` public API
was unusable through the `Forms` namespace (only the standalone
`FormEnhancements` global worked).

Worse: because the Proxy also had **no `set` trap**, assignments like
`Forms.stats = () => ...` landed directly on the same instance object used
internally for caching — silently overwriting the *internal* `this.stats`
counters object with the public function, corrupting hit/miss tracking
(confirmed: `Forms.stats()` was returning `NaN`, which serializes as `null`
in JSON).

Fixed by giving the Proxy a real `set` trap that routes all such assignments
into a dedicated `_namespaceExtras` bag, and checking that bag first in
`get`/`has`/`ownKeys`/`getOwnPropertyDescriptor` — this fixes both problems
at once and doesn't require hardcoding every future namespace property name.

### 6. Minor cleanups
- `getStats().uptime` was mislabeled — it actually measured time since the
  last cleanup cycle, not true uptime. Now reports both correctly.
- Declarative `[data-enhanced]` forms computed their submit options once at
  wire-time and reused them forever, ignoring later attribute changes. Now
  recomputed fresh on every submit.
- 3 unconditional `console.log` lines on every page load in the enhance file
  (inconsistent with its own `enableLogging: false`-by-default design)
  condensed to one informational line, matching the base file's convention.
- Added an optional `headers` option to `submitData()` — there was
  previously no way to pass e.g. an `Authorization` header.

---

## A build-process bug I found (and corrected) along the way

Worth knowing even outside this specific fix: I originally rebuilt the bundle
with `esbuild --bundle`, the same way as an earlier bundle I gave you in this
conversation. It turns out **that approach silently breaks this codebase**.

Every file in this source tree ends with a UMD-style guard:
```js
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { Forms, ... };
} else {
  global.Forms = Forms;   // the browser path
}
```
`esbuild --bundle` scans for `module`/`exports` tokens *anywhere* in a file —
even inside a runtime-guarded branch like this — and wraps that file in its
own internal `__commonJS(...)` shim, injecting a fake `module` object. That
makes `typeof module !== 'undefined'` true **inside the bundle**, so the code
silently takes the `module.exports` branch and never sets `global.Forms` (or
`global.EnhancedUpdateUtility`, `global.asyncState`, etc.) at all.

The dangerous part: the bundle still builds without any error, and the
feature's code is still physically present in the output — so checking for
it with `grep` (which I did earlier in this conversation) gives a false
sense of confidence. I re-verified with an actual jsdom runtime test this
time and confirmed the difference directly.

**The fix:** since these files aren't real ES modules — they're plain
self-executing scripts, and the only `import`/`export` syntax lives in the
top-level entry files (`spa.js`, `form-only.js`, etc.) purely to declare load
order — the correct build step is simple concatenation in that order, not a
real bundler. `concat-build.js` in this package does exactly that: it reads
an entry file's `import './x.js'` lines, resolves them in order, and
concatenates the raw files. No CommonJS detection, no wrapping, no silent
breakage. Minification is still done with `esbuild --minify` afterward, but
in *transform* mode (no `--bundle` flag), which doesn't do module-graph
analysis and so doesn't trigger the same issue.

If you ever build other bundles from this source tree yourself, use this
same concatenation approach rather than a bundler's `--bundle` mode, unless
you first strip the `module.exports` guard blocks from the source.

---

## How I verified this (not just by reading the diff)

`test.js` spins up a jsdom environment, loads the rebuilt bundle, and runs:

1. Reproduces your exact original crash scenario (`Forms.signupForm` access) — confirms no throw.
2. Confirms `.update()` still works correctly after the fix.
3. Creates a form with a real `File` object in a file input, submits it, and confirms it survives as an actual `File`/`FormData` entry instead of `{}`.
4. Uses a field name containing a `"` character and confirms `getField()`/`setField()` still work.
5. Calls `Forms.enhance.configure()` *after* a form has already been touched, and confirms the new config actually reaches it.
6. Confirms `Forms.enhance`, `.validators`, and `.v` resolve to their real values instead of `null`.
7. Confirms `Forms.stats()` returns real numbers instead of corrupted `NaN`/`null` values.

All 13 assertions pass. Run it yourself with:
```bash
npm install jsdom
node test.js
```
