feat: form state hooks, schema adapters, wizard, DevTools + CI hardening - #31
Merged
vannt-dev merged 20 commits intoAug 5, 2026
Merged
Conversation
…ult renderers map
… engine, and DevTools
Why:
zodValidator checked safeParseAsync before safeParse. Real Zod schemas expose
both, so the async branch always won and the validator returned a Promise.
validateField treats a Promise as "no errors", and useDynamicForm only ever
calls the synchronous validateFields - so a Zod-validated form silently
reported valid for invalid data.
yupValidator had a related bug: Yup throws a plain Error (not a
ValidationError) when a test is async, and that internal message
"Validation test of type ... returned a Promise" was surfaced to the user as
a form error.
The payload rule was also a guess - `value !== undefined ? value : data` -
which picked the field value for a form-level schema, and differed from
standardSchemaValidator.
What:
- Prefer sync parsing (safeParse / validateSync), falling back to the async
path only when the schema genuinely requires it.
- Distinguish a Yup ValidationError from its async-test error, and retry
asynchronously instead of leaking the internal message.
- Replace the payload heuristic with an explicit SchemaValidatorOptions
`{ field, target: 'form' | 'field' }`, applied identically by all three
adapters. The `zodValidator(schema, 'email')` shorthand still works.
- Add zod and yup as core devDependencies; the adapters were previously only
exercised against hand-rolled fakes that hid both bugs.
How to test:
npm run test --workspace=@dynamic-field-kit/core
Why: The three form-state APIs had drifted. React was missing isSubmitting and isSubmitted, which Vue and Angular both exposed, so a React consumer had no way to disable a submit button while a submission was in flight. Angular's handleSubmit executed immediately instead of returning a handler like React and Vue, and never called preventDefault - so it could not be bound to a native form submit at all. What: - React: add isSubmitting / isSubmitted, set around handleSubmit in a try/finally so a throwing onValid still clears the flag, and reset both. - Angular: handleSubmit(onValid, onInvalid) now returns an async handler that calls preventDefault, matching React and Vue. These APIs are unreleased (published angular is 1.4.0 without the store), so no published consumer is affected. How to test: npm run test --workspace=@dynamic-field-kit/react npm run test --workspace=@dynamic-field-kit/angular
Why: The new feature work landed largely untested and pushed every package below its coverage floor, which is a hard CI gate: core 68/63/79/68, react functions 77.7, vue 84/74/72/84, angular 83/71/85/83. On develop core sat at 88/85/100/88, so this was a regression introduced by the feature branch. What: Tests for the code that shipped without any - group array helpers (move/swap/insert plus focusFirstInvalidField under jsdom), wizard index clamping and the empty-steps case, the DevTools overlay across all four tabs in all three frameworks, and the extended HTML5 renderers (date/time/ datetime-local/switch/file, option id/name fallbacks, range). Coverage is now core 89.95/85.04/96.96/90.33, react 97.34/86.95/95.65/97.34, vue 98.87/87.01/94.02/98.87, angular 91.30/75.56/92.40/91.22 - all above their floors. Suite grew from 249 to 394 tests. How to test: npm run test --workspace=@dynamic-field-kit/<pkg> -- --coverage
Why: The three feature commits on this branch added public API to all four packages without a changeset, so the work would have been merged and never released. The README also promised "Integrated zodValidator" without saying that adapters parse synchronously, which is the difference between a form that validates and one that silently passes. What: - Add a minor changeset covering core, react, vue and angular. - Document the schema adapter contract: form vs field target, the field-name shorthand, and when a schema forces validateFieldsAsync. - Note that all three frameworks share one hook surface. How to test: npx changeset status
Why: Both the react and vue defaultRenderersMap register a `switch` renderer, but FieldTypeMap had no `switch` entry - so `type: 'switch'` failed to typecheck and the shipped renderer was unreachable from TypeScript. The example app hit exactly this: `Type '"switch"' is not assignable to type 'FieldTypeKey'`. What: Add `switch: boolean` to FieldTypeMap, and a type test asserting every key the default renderer maps register is a usable field type, so the two cannot drift apart again. How to test: npm run test:types --workspace=@dynamic-field-kit/core
Why: Two problems that CI cannot currently see, because the example apps are not workspaces and are not referenced by any workflow, and because the angular package has no `files` field. The example passed `onBlurField` to MultiFieldInput, which has no such prop - it manages blur internally. The page therefore did not compile. The angular tarball shipped src/ and test/ alongside dist/ - 61 files where react ships 8. What: - example: remove the `onBlurField` prop so the page compiles. - angular: add `files: ["dist"]`. Tarball drops 61 -> 33 files, dist/ only. Note: useDynamicForm's handleBlur/touched cannot currently be wired into MultiFieldInput at all. Worth a follow-up on whether the component should expose a blur hook. How to test: cd example/react-app && npx next build cd packages/angular && npm pack --dry-run
…not see Why: Publishing was effectively ungated. Release triggered on push to master, did not depend on CI, and ran a thinner set of checks - no lint, no format, no coverage floors, no verify scripts. The two workflows are independent, as the 2026-08-04 history shows: CI succeeded on master at 16:13:10 while Release failed at 16:13:09 on the same commit. The reverse - publishing while CI is red - was equally possible. Three more blind spots: - The example apps are not workspaces and were referenced by no workflow, so nothing compiled them. Both bugs fixed in the previous two commits (a missing FieldTypeMap entry and a non-existent prop) were sitting in an example that had never been built in CI. - No check that a PR touching a package adds a changeset. The three feature commits on this branch had none. - No dependency audit. postcss reached production deps through vue. What: - Extract every gate into a reusable quality-gates.yml (workflow_call). ci.yml and release.yml both call it, so the two can no longer drift and release blocks on `needs: [gates]`. - Add an examples job building all three demo apps against the built dist. - Add a changeset job on pull_request, and an npm audit step for production dependencies. - Pin every job to .nvmrc via node-version-file. The workflows hardcoded Node 22 while .nvmrc said 24. - Add CODEOWNERS - master has require_code_owner_reviews enabled, which does nothing without this file - and a PR template. - Widen lint-staged globs to cover .cjs/.mjs/.yaml so pre-commit stops letting through files that `prettier --check .` then fails on in CI. - Configure commit.template from `prepare`; the template file existed but was never wired up. - Override postcss to ^8.5.25. `npm audit fix` cannot resolve it here because of the known ng-packagr peer conflict. Production audit is now clean, so the new audit step passes. - changesets baseBranch master -> develop, matching where PRs actually land. How to test: npm run lint && npm run format-check && npm run typecheck npm audit --omit=dev --audit-level=high npx changeset status --since=origin/develop cd example/<app> && npm ci && npm run build
Why: The override broke `npm ci` on Linux: "Missing: yaml@2.9.0 from lock file". A bare `postcss` key rewrites every postcss in the tree, including @angular-devkit/build-angular's nested copy, whose subtree resolves differently on Linux than on Windows. npm then had to re-resolve packages the lockfile had no entries for, so every CI job failed at install. The override was also unnecessary. vue's compiler-sfc asks for postcss ^8.5.8, and the advisories cover <=8.5.22 - so 8.5.25 was always inside the range npm was allowed to pick. The vulnerability existed only because the lockfile pinned 8.5.12. Refreshing that pin is the whole fix; no override is needed to hold it there. What: Remove the overrides block. The lockfile keeps postcss 8.5.25, which is a resolution npm reaches on its own, and `npm audit --omit=dev` reports zero vulnerabilities. How to test: npm ci && npm audit --omit=dev --audit-level=high
Why: Pointing the workflows at .nvmrc was meant to remove drift, but .nvmrc said 24 while every workflow had hardcoded 22 - so the "fix" silently upgraded CI by a major version, and install broke everywhere with: npm error Missing: yaml@2.9.0 from lock file Node 22 ships npm 10.x; Node 24.18 ships npm 11.16. lint-staged declares `yaml` as an optionalDependency, and npm 11.16 requires an entry for it in the lockfile where npm 10 does not. develop's lockfile has never had that entry, so this was latent, not caused by anything in this branch. Regenerating the lockfile on Windows is not the fix: npm 11.16 prunes every other platform's optional binaries while doing it, dropping 26 entries including @lmdb/lmdb-linux-x64 and @napi-rs/nice-linux-x64-gnu, which is exactly what Linux CI needs. What: Set .nvmrc to 22 so the declared version matches the one that is actually tested and that the committed lockfile supports. The workflows keep reading .nvmrc, so there is still a single source of truth. Moving to Node 24 is a real upgrade and needs its own PR: the lockfile has to be regenerated on Linux under npm 11.16 so it keeps the cross-platform optional binaries and gains the yaml entry. How to test: npm ci
Why:
Two failures in the new job.
example/angular-app gitignores its package-lock.json, so in a fresh checkout
there is no lockfile: `cache-dependency-path` could not resolve it ("Some
specified paths were not resolved") and `npm ci` had nothing to install from.
example/react-app failed with module-not-found on
@dynamic-field-kit/core from packages/react/dist/index.mjs. The `file:` deps
resolve to the real packages/ directories, so Node walks up from there to the
workspace root looking for core - and the job never ran a root install, so it
was not there. It passed locally only because a root node_modules already
existed.
What:
- Run `npm ci` at the workspace root before installing each example.
- Use `npm install` for the examples so the lockfile-less angular app works;
react and vue still honour their committed lockfiles.
- Drop cache-dependency-path so the cache keys off the root lockfile, which
always exists.
How to test:
rm -rf example/react-app/node_modules
npm ci && cd example/react-app && npm install && npm run build
The release PR consumes changesets and bumps versions, so it changes packages while correctly having no changeset left. Requiring the check without this would deadlock every release.
Why: Releases were being cut by hand - #25 edited versions directly and #27 then had to delete consumed changeset files manually. Changesets was installed but half-used, so the version bump was manual work followed by a commit. What: Release is now workflow_dispatch only, with inputs: bump patch | minor | major packages core,react,vue,angular (empty = all) message the CHANGELOG entry dry_run version and print, publish nothing scripts/create-changeset.js turns those inputs into a real changeset, so the run goes: quality gates -> changeset version -> lockfile sync -> build -> commit -> changeset publish. No version is edited by hand. Packages stay independently versioned, and changesets already committed are consumed in the same run with the largest bump per package winning. The push-to-master trigger is gone. It opened a "version packages" PR through changesets/action, which is a second, competing release path - the same overlap that produced the manual cleanup in #27. Run it on develop, not master: required status checks apply to direct pushes, so the Actions bot cannot push the release commit to master. Versions reach master through the usual develop -> master PR. Verified locally against the real changeset on this branch: core 1.3.0 -> 1.4.0, react/vue/angular 1.4.0 -> 1.5.0, internal deps rewritten to ^1.4.0, CHANGELOGs generated. The lockfile sync step was checked under npm 10 (what Node 22 ships, per .nvmrc) and keeps every platform's optional binaries - npm 11 prunes them, which is what broke install earlier on this branch. How to test: Actions > Release > Run workflow, on develop, with dry_run enabled.
Why: The engine shipped canGoNext and canGoPrev - it could say whether moving was allowed, but there was no function to move. Callers had to rebuild state with createWizardState(steps, i + 1), which resets completedSteps to [] every time. completedSteps was written in exactly one place, its initialiser, and read nowhere: declared state that nothing maintained. The README advertises a "Multi-Step Form Wizard Engine", so this was the gap between the claim and what the code could do. What: - goNext / goPrev / goToStep, all returning new state and leaving the input untouched. goNext records the step it leaves in completedSteps, so the set is now maintained rather than decorative. - markStepCompleted and isStepCompleted for driving a step indicator. - goNext at the last step and goPrev at the first return the *same* state object, so callers can compare identity to detect a no-op. goNext does not validate. Validation stays explicit through validateStep, so a wizard can allow moving on from an incomplete step if it wants to. How to test: npm run test --workspace=@dynamic-field-kit/core
The react and vue overlays put a red count badge on the collapsed button and render the errors tab as "errors (N)". Angular had neither, so the one framework where you cannot see errors at a glance was the one whose overlay looked identical otherwise.
Why:
useDynamicForm ships handleBlur, touched and validateOnBlur, but there was no
way to connect them to MultiFieldInput - the component that actually renders
the form. The example app tried, with onBlurField, and did not compile.
Worse, blur plumbing existed only in react. Vue and Angular had none at any
level: their FieldInput never passed onBlur down, and Vue's DynamicInput did
not declare it - so the vue default renderers accepted an onBlur prop that
could never arrive.
What:
- react: MultiFieldInput takes an optional onBlurField, called alongside the
touched tracking it already did internally.
- vue: onBlur and touched threaded through DynamicInput -> FieldInput ->
MultiFieldInput, plus internal touched tracking to match react.
- angular: FieldInput emits onBlurField from a `focusout` listener - it
bubbles, so any renderer works without declaring a blur output of its own.
MultiFieldInput re-emits it and exposes isTouched().
- example: restore onBlurField, now that the prop exists.
How to test:
npm run test --workspace=@dynamic-field-kit/{react,vue,angular}
cd example/react-app && npm run build
Why: The v1.4 features were a bullet list with no examples, and several shipped APIs appeared nowhere in the README at all - the wizard navigation, the group array helpers, defaultRenderersMap/getDefaultRenderer, and the blur wiring. The built-in renderer list was also stale: it named 7 types when the packages ship 14. What: - Sections with real examples for form state, the wizard, DevTools and the group array helpers, each with a table of the exported surface. - Correct the renderer list, and document what each default emits. - A "Runnable Examples" section mapping each demo page to what it shows. - New /wizard page in the react example: step indicator driven by completedSteps, per-step validateStep, goNext/goPrev. Linked from the other two pages. Every identifier named in the README was checked against the built dist, and the example app compiles in CI. How to test: cd example/react-app && npm run dev # then visit /wizard
Why: There was no link to hand someone who wants to see the library work. The example apps only ran locally, and running them means cloning the repo and building four packages first. What: - deploy-pages.yml builds all three demos and publishes them under one site: /react, /vue, /angular, plus a landing page. It runs on develop and on demand, and only when packages/ or example/ changed. - Each app takes its base path from an env var, so local dev is untouched: Next reads PAGES_BASE_PATH, Vite reads it as `base`, Angular gets --base-href on the command line. - Next now emits a static export with trailingSlash, so /wizard resolves to wizard/index.html on a plain file host. A .nojekyll file stops Pages from stripping _next. - README gains per-package npm badges and demo links. The repo's About link now points at the demos, so npm needed a home in the README. Also deletes example/react-app/next.config.ts. Next resolves next.config.js first, so the .ts file - and the `reactCompiler: true` in it - had never taken effect. Verified by adding output:'export' to the .js and watching out/ appear. And ignores example/ and smoke/ in .eslintignore. `npm run lint` only covers packages/*/src, so CI never linted them, but the pre-commit hook did - against a config written for library source, which rejects a `require` in a Next config and cannot resolve an example's own dependencies. Live at https://vannt-dev.github.io/dynamic-field-kit/ once this is on develop. How to test: Actions > Deploy demos to Pages > Run workflow
vannt-dev
deleted the
feature/form-state-hooks-and-advanced-enhancements
branch
August 5, 2026 16:33
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Completes the enterprise feature branch and hardens the release pipeline.
Bug fixes in the feature work
zodValidatorreturned a Promise for every real Zod schema (it checkedsafeParseAsyncfirst, and Zod exposes both).validateFieldtreats a Promise as "no errors" anduseDynamicFormonly calls the synchronousvalidateFields— so a Zod-validated form silently reported valid for invalid data. Adapters now parse synchronously, falling back to async only when the schema genuinely requires it.yupValidatorsurfaced Yup's internal"Validation test of type ... returned a Promise"to users as a form error.value !== undefined ? value : datapayload guess with an explicit{ target: 'form' | 'field' }option, applied identically by all three adapters.switchhad a shipped renderer in react and vue but noFieldTypeMapentry, sotype: 'switch'did not typecheck.useDynamicFormwas missingisSubmitting/isSubmitted; Angular'shandleSubmitexecuted immediately instead of returning a handler and never calledpreventDefault.Coverage
Every package was below its floor — a hard CI gate. Core had regressed from 88/85/100/88 on develop to 68/63/79/68. Now core 89.95/85.04/96.96/90.33, react 97.34/86.95/95.65/97.34, vue 98.87/87.01/94.02/98.87, angular 91.30/75.56/92.40/91.22. Suite 249 → 394 tests.
Pipeline
quality-gates.ymlthat both CI and Release call.npm auditstep, CODEOWNERS (master enablesrequire_code_owner_reviews, which did nothing without the file), and a PR template..nvmrc; workflows said Node 22 while.nvmrcsaid 24.src/andtest/to npm — 61 files vs react's 8. Nowfiles: ["dist"], 33 files.Why
The feature branch could not merge: all four test legs failed their coverage gate, and the headline schema-adapter feature did not work with the hooks shipped alongside it.
How to test
Added after the initial description: one-click releases
Releases were half-manual — #25 edited versions by hand, #27 then deleted consumed changeset files by hand. Release is now workflow_dispatch only: pick
bump(patch/minor/major), optionally which packages, a CHANGELOG message, anddry_run.scripts/create-changeset.jsturns those inputs into a real changeset, then the run does gates →changeset version→ lockfile sync → build → commit →changeset publish. No version is edited by hand and packages stay independently versioned.The
push: mastertrigger is removed — it opened a competing "version packages" PR, which is the same overlap that caused the manual cleanup in #27.Verified locally against this branch's real changeset: core 1.3.0 → 1.4.0, react/vue/angular 1.4.0 → 1.5.0, internal deps rewritten, CHANGELOGs generated (then reverted).
Run it on
develop, notmaster— required status checks apply to direct pushes, so the Actions bot cannot push the release commit to master. Versions reach master through the usualdevelop → masterPR.