{"_id":"@adexdsamson/forge-validation","name":"@adexdsamson/forge-validation","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@adexdsamson/forge-validation","version":"0.1.0","description":"Opt-in validation strategy companion package for @adexdsamson/forge — strategies, debounced async validation with abort, and submit gating.","main":"dist/index.cjs.js","module":"dist/index.esm.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"}},"scripts":{"prepare":"npm run build","build":"rollup -c","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"eslint src/ && prettier --check src/","lint:fix":"eslint src/ --fix && prettier --write src/"},"repository":{"type":"git","url":"git+https://github.com/adexdsamson/forge-validation.git"},"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"keywords":["react","react-native","form","forms","validation","react-hook-form","forge"],"author":{"name":"adexdsamson","url":"https://github.com/adexdsamson"},"license":"MIT","sideEffects":false,"engines":{"node":">=18"},"bugs":{"url":"https://github.com/adexdsamson/forge-validation/issues"},"homepage":"https://github.com/adexdsamson/forge-validation#readme","peerDependencies":{"@adexdsamson/forge":">=1.1.0","react":">=18","react-hook-form":"^7.34.0"},"devDependencies":{"@adexdsamson/forge":"^1.1.0","@eslint/js":"^10.0.1","@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-node-resolve":"^15.2.3","@rollup/plugin-typescript":"^11.1.6","@testing-library/jest-dom":"^6.9.1","@testing-library/react":"^16.3.2","@types/react":"^18.2.55","eslint":"^10.4.1","eslint-config-prettier":"^10.1.8","eslint-plugin-react-hooks":"^7.1.1","jsdom":"^29.1.1","prettier":"^3.8.3","react":"^18.2.0","react-dom":"^18.2.0","react-hook-form":"^7.50.1","rollup":"^4.12.0","rollup-plugin-dts":"^6.1.0","tslib":"^2.6.2","typescript":"^5.3.3","typescript-eslint":"^8.60.0","vitest":"^4.1.7"},"_id":"@adexdsamson/forge-validation@0.1.0","gitHead":"59d5aa5cc4c39960b94791f080e64288d7c533a4","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-8T1JgAv6TtYpIDEgQu9O+zHmgdAAlVYUnbpQXsQas6rraUglqV6CAYLWRjPyxYami6IPNVUywNYOnY5I8E1/Sg==","shasum":"acea81c2787ca2af61da02c78583fb8ac3d0488c","tarball":"https://registry.npmjs.org/@adexdsamson/forge-validation/-/forge-validation-0.1.0.tgz","fileCount":8,"unpackedSize":70216,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@adexdsamson%2fforge-validation@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEqubE02BO7Keazy5RHCcykEIn1peJyki7HPGE+ymn2uAiB71oBag5BgSxrpBYceV3YZiWZwAOtrc4bZ1oVmG96aHw=="}]},"_npmUser":{"name":"adexdsamson","email":"adexdsamson@gmail.com"},"directories":{},"maintainers":[{"name":"adexdsamson","email":"adexdsamson@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/forge-validation_0.1.0_1782853707065_0.5554683601787731"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-30T21:08:26.941Z","0.1.0":"2026-06-30T21:08:27.200Z","modified":"2026-06-30T21:08:27.574Z"},"maintainers":[{"name":"adexdsamson","email":"adexdsamson@gmail.com"}],"description":"Opt-in validation strategy companion package for @adexdsamson/forge — strategies, debounced async validation with abort, and submit gating.","homepage":"https://github.com/adexdsamson/forge-validation#readme","keywords":["react","react-native","form","forms","validation","react-hook-form","forge"],"repository":{"type":"git","url":"git+https://github.com/adexdsamson/forge-validation.git"},"author":{"name":"adexdsamson","url":"https://github.com/adexdsamson"},"bugs":{"url":"https://github.com/adexdsamson/forge-validation/issues"},"license":"MIT","readme":"# @adexdsamson/forge-validation\n\nOpt-in validation strategy companion package for [`@adexdsamson/forge`](https://github.com/adexdsamson/Forge). Wraps RHF's validation surface with three concrete pieces:\n\n- **Strategies** — `progressive`, `lenient`, `strict`, `standard` as plain predicate objects. Override one predicate by spreading.\n- **`<ForgeSubmit>`** — submit gating that reads `canSubmit` from the active strategy. Web + React Native. `asChild`, render-prop, or default `<button>`.\n- **`createAsyncValidator()`** — debounced async field validation with `AbortSignal` threading, `AbortError` swallow, and a generation-counter guard for validators that ignore the signal.\n\n> **Status:** Stable surface, all five engineering milestones merged (M0–M4). Implementation tracked in [`adexdsamson/Forge#5`](https://github.com/adexdsamson/Forge/issues/5).\n\n## Install\n\n```bash\nnpm install @adexdsamson/forge-validation\n```\n\nPeer dependencies:\n\n```bash\nnpm install @adexdsamson/forge react react-hook-form\n```\n\nMinimum versions: `@adexdsamson/forge` `>= 1.1.0`, `react` `>= 18`, `react-hook-form` `^7.34.0`.\n\n## Quickstart\n\n```tsx\nimport { useForge, Forge, Forger } from \"@adexdsamson/forge\";\nimport {\n  ForgeValidation,\n  ForgeSubmit,\n  createAsyncValidator,\n} from \"@adexdsamson/forge-validation\";\n\nconst checkUsername = createAsyncValidator(\n  async (value, ctx) => {\n    const res = await fetch(`/api/check?name=${value}`, { signal: ctx.signal });\n    const data = await res.json();\n    return data.available ? undefined : \"Username already taken\";\n  },\n  { debounceMs: 400, field: \"username\" }\n);\n\nfunction SignUpForm() {\n  const { control } = useForge({ defaultValues: { username: \"\", password: \"\" } });\n\n  return (\n    <ForgeValidation strategy=\"progressive\">\n      <Forge control={control} onSubmit={(data) => console.log(data)}>\n        <Forger\n          name=\"username\"\n          component={TextInput}\n          rules={{ required: \"Username is required\", validate: checkUsername }}\n        />\n        <Forger\n          name=\"password\"\n          component={TextInput}\n          rules={{ required: \"Password is required\", minLength: 8 }}\n        />\n        <ForgeSubmit asChild>\n          <Button>Sign up</Button>\n        </ForgeSubmit>\n      </Forge>\n    </ForgeValidation>\n  );\n}\n```\n\nThe four pieces:\n\n1. **`strategy=\"progressive\"`** — submit is enabled until a *touched* field has an error. Errors only render after the user has blurred the field.\n2. **`createAsyncValidator(fn, opts)`** — wraps your async check with debouncing (400ms here) and abort-on-new-keystroke. The `ctx.signal` is threaded into your `fetch` call so cancelled requests don't hit your backend twice.\n3. **`<ForgeSubmit asChild>`** — clones `<Button>` and injects `type=\"submit\"` + a `disabled` value derived from the strategy's `canSubmit`. Web-only here; the same component works on React Native — see [`docs/REACT_NATIVE.md`](./docs/REACT_NATIVE.md).\n4. **`Forge` / `Forger` / `useForge`** — the core package. This companion doesn't reinvent any of that.\n\n## API\n\n### `<ForgeValidation>`\n\nProvider that exposes a resolved `Strategy` to descendants.\n\n```tsx\n<ForgeValidation strategy={\"progressive\" | \"lenient\" | \"strict\" | \"standard\"}>\n  {children}\n</ForgeValidation>\n\n// or with a fully-specified Strategy object\n<ForgeValidation strategy={strategies.progressive}>{children}</ForgeValidation>\n\n// or with an override\n<ForgeValidation strategy={{\n  ...strategies.progressive,\n  canSubmit: (state) => state.errors.email === undefined,\n}}>\n  {children}\n</ForgeValidation>\n```\n\n| Prop | Type | Default |\n|---|---|---|\n| `strategy` | `StrategyName \\| Strategy \\| undefined` | `undefined` (no gating) |\n| `children` | `ReactNode` | required |\n\n### `useForgeValidation()`\n\n```ts\nconst ctx = useForgeValidation();\n// → { strategy: Strategy | null } | null\n```\n\nReturns `null` outside a `<ForgeValidation>`. Use this in custom field components if you want to gate error rendering or per-field UI on `ctx.strategy.shouldShowError(...)`.\n\n### `strategies`\n\nBuilt-in predicate objects. Read the source for exact semantics — no mystery enums.\n\n| Preset | `canSubmit` blocks on | `shouldShowError` after | `shouldValidate` on |\n|---|---|---|---|\n| `strict` | any error · submitting · in-flight async | error exists | every trigger |\n| `standard` | any error · submitting | touch OR submit attempt | blur, submit, manual (no change) |\n| `progressive` | error on a *touched* field · submitting | touch | blur, submit, manual + change *only when field already errors* |\n| `lenient` | submitting | submit attempt | submit, manual only |\n\n```ts\nimport { strategies } from \"@adexdsamson/forge-validation\";\n\nstrategies.progressive; // → Strategy object\n```\n\n### `<ForgeSubmit>`\n\nSubmit gating that reads `canSubmit` from the active strategy.\n\n```tsx\n// 1. Default — internal <button type=\"submit\">\n<ForgeSubmit>Sign up</ForgeSubmit>\n\n// 2. asChild — clones the child and injects type/disabled (web) or\n//    disabled/accessibilityState.disabled/enabled (RN)\n<ForgeSubmit asChild>\n  <Button isLoading={isPending}>Sign up</Button>\n</ForgeSubmit>\n\n// 3. Render-prop — for hairy composition\n<ForgeSubmit>\n  {({ disabled, type }) => <FancyButton disabled={disabled} type={type} />}\n</ForgeSubmit>\n```\n\n**Composition rules:**\n\n- `asChild` **OR**s the child's existing `disabled` prop. Never overrides. `<ForgeSubmit asChild><Button disabled={isPending} /></ForgeSubmit>` disables on `isPending || !canSubmit`.\n- Same rule for `accessibilityState.disabled` and gesture-handler's `enabled={false}` on React Native — see [`docs/REACT_NATIVE.md`](./docs/REACT_NATIVE.md).\n- Outside a `<ForgeValidation>` (or with no strategy), and outside an RHF `<FormProvider>`, submit is never gated — `disabled` defaults to `false`. No throws.\n\n### `createAsyncValidator(fn, opts)`\n\nWrap an async field validator so RHF gets a debounced, `AbortSignal`-threading function.\n\n```ts\nconst checkUsername = createAsyncValidator(\n  async (value: string, ctx) => {\n    const res = await fetch(`/api/check?name=${value}`, { signal: ctx.signal });\n    const data = await res.json();\n    return data.available ? undefined : \"Username already taken\";\n  },\n  { debounceMs: 400, field: \"username\" }\n);\n\n// Plugs into RHF's validate option as-is:\n<Forger name=\"username\" rules={{ validate: checkUsername }} ... />\n```\n\n`opts.debounceMs` (default `0`) — milliseconds of no-new-trigger to wait before invoking. `0` still aborts in-flight on new triggers.\n\n`opts.field` (default `\"\"`) — field name threaded into `ctx.field`.\n\n**Behavior:**\n\n| Event | Outcome |\n|---|---|\n| Rapid invocations within debounce window | All but the latest are settled with `undefined`; the latest runs the validator. |\n| In-flight `fetch` superseded | `controller.abort()` fires. Caller must forward `ctx.signal`. |\n| Inner validator throws `AbortError` | Swallowed. Resolves to `undefined`. |\n| Inner validator throws any other error | Surfaced as a string (`err.message` or `\"Validation failed\"`). Logged in dev mode. |\n| Validator ignores `ctx.signal` and resolves anyway after abort | Stale result dropped. Dev-mode warning: *\"validator resolved after abort — thread ctx.signal into your fetch call\"*. Production silent. |\n\nSee [`docs/SSR.md`](./docs/SSR.md) for the constraints on what strategies and async validators may read.\n\n## Documentation\n\n- [SSR contract](./docs/SSR.md) — pure-of-state rules, hydration behavior, hard \"no storage in strategies\" constraint.\n- [React Native composition](./docs/REACT_NATIVE.md) — `<ForgeSubmit asChild>` with RN core `Pressable` vs `react-native-gesture-handler`, OR-merge details.\n- *Migration from a private validation fork* — coming once a representative private API surfaces; see RFC §5 of [`adexdsamson/Forge#5`](https://github.com/adexdsamson/Forge/issues/5).\n\n## TypeScript\n\nAll exports ship with `.d.ts`. Strategies are generic in `TValues extends FieldValues` — passing your form's value type into `useForge<MyValues>()` propagates to the `Strategy<MyValues>` consumed by `<ForgeValidation>`.\n\n```ts\nimport type {\n  Strategy,\n  StrategyName,\n  ForgeState,\n  FieldState,\n  ValidationTrigger,\n  AsyncValidator,\n} from \"@adexdsamson/forge-validation\";\n```\n\n## License\n\nMIT © adexdsamson\n","readmeFilename":"README.md","_rev":"1-f634c159b4ba969c43fa17700dec7d66"}