{"_id":"@cassets/form-pipeline","_rev":"2-445b6ba737ecda02fa096c174134108f","name":"@cassets/form-pipeline","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@cassets/form-pipeline","version":"0.1.0","keywords":["react","form","validation","pipeline","typescript"],"author":{"name":"Vivek Rao Bhosale"},"license":"MIT","_id":"@cassets/form-pipeline@0.1.0","maintainers":[{"name":"clusterassets","email":"clusterassets@gmail.com"}],"dist":{"shasum":"9dab62b3f25a85edd4191bfbe61293567787e786","tarball":"https://registry.npmjs.org/@cassets/form-pipeline/-/form-pipeline-0.1.0.tgz","fileCount":8,"integrity":"sha512-RwPK/zOLX+NtkirACkgc3i9dadXQRm5L76XnPATJyFBaCxxWIwHKcJ6WdY3tdAPl70Yb7r04vimZnufPEqa6tQ==","signatures":[{"sig":"MEQCIC+W6KGoBxYiE5KtSqk0RaSPrXqTtl20G6V4hGkfD04LAiAaWyxHwp5yGL7RifAj8MjPQQc0RCfgu+daKolRCHLD2A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91718},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"clusterassets","email":"clusterassets@gmail.com"},"_npmVersion":"11.16.0","description":"Zero-config form pipeline for React with automatic DOM reading, coercion, validation, and file handling.","directories":{},"_nodeVersion":"24.18.0","_hasShrinkwrap":false,"devDependencies":{"zod":"^3","tsup":"^8.5.1","react":"^18","esbuild":"^0.28.2","typescript":"^5","@types/react":"^18"},"peerDependencies":{"zod":">=3","react":">=17"},"peerDependenciesMeta":{"zod":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/form-pipeline_0.1.0_1786607872262_0.9854252637482508","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@cassets/form-pipeline","version":"0.1.1","description":"DOM-first form-to-payload pipeline for React and browser apps with native validation, coercion, nested payloads, files, and optional Zod validation.","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"build":"tsup","prepublishOnly":"npm run build","typecheck":"tsc --noEmit"},"peerDependencies":{"react":">=17"},"devDependencies":{"@types/react":"^18","react":"^18","tsup":"^8.5.1","typescript":"^5","zod":"^3"},"keywords":["cassets","react","form","form-validation","typescript","zod","payload","formdata","html-form","uncontrolled-form"],"author":{"name":"Vivek Rao Bhosale","url":"https://github.com/diskhacker"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/clusterassets/form-pipeline.git"},"homepage":"https://github.com/clusterassets/form-pipeline#readme","bugs":{"url":"https://github.com/clusterassets/form-pipeline/issues"},"publishConfig":{"access":"public","provenance":true},"sideEffects":false,"gitHead":"81622f7c4cd3506d89a2171cc37dd8343668be72","_id":"@cassets/form-pipeline@0.1.1","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-3bT03q6Da9lVHca7GMOYIdeX6Js8dFa7zsOr8Fcp8ESqCDBfLuvgKZJWHNWODvO6dlK7z9QDwlTM7KzIGMajDw==","shasum":"63434f324e66743c4d9da9d5f284f86cd2828c0b","tarball":"https://registry.npmjs.org/@cassets/form-pipeline/-/form-pipeline-0.1.1.tgz","fileCount":9,"unpackedSize":104105,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cassets%2fform-pipeline@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCvZypXdQvNT4/nyyXKUwMXBPP9GbbhveYMGnqPeQ4yAgIhAN+Swb+m0mJScXJuVDqXX8P598Lmlz0/toNC2JychMKd"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b8dd5224-c95f-4e23-8b35-bc2a49781d8e"}},"directories":{},"maintainers":[{"name":"clusterassets","email":"clusterassets@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/form-pipeline_0.1.1_1787580239190_0.3900288224229198"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-13T07:57:52.015Z","modified":"2026-08-24T14:03:59.722Z","0.1.0":"2026-08-13T07:57:52.390Z","0.1.1":"2026-08-24T14:03:59.352Z"},"author":{"name":"Vivek Rao Bhosale","url":"https://github.com/diskhacker"},"license":"MIT","keywords":["cassets","react","form","form-validation","typescript","zod","payload","formdata","html-form","uncontrolled-form"],"description":"DOM-first form-to-payload pipeline for React and browser apps with native validation, coercion, nested payloads, files, and optional Zod validation.","maintainers":[{"name":"clusterassets","email":"clusterassets@gmail.com"}],"readme":"# @cassets/form-pipeline\n\nDOM-first form-to-payload processing for React and browser applications.\n\n`@cassets/form-pipeline` reads the form you already rendered, applies native HTML validation, normalizes and coerces values, builds nested payloads, handles files, and optionally validates with Zod. It is designed for uncontrolled or lightly controlled forms where the DOM is the source of truth.\n\n[![npm](https://img.shields.io/npm/v/@cassets/form-pipeline.svg)](https://www.npmjs.com/package/@cassets/form-pipeline)\n[![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n\n## Why this package?\n\nTraditional React forms often duplicate browser state in `useState`, repeat validation rules in multiple layers, and hand-build API payloads. This package uses the semantics already present in `HTMLFormElement` controls.\n\n```text\nHTMLFormElement\n  -> discover controls\n  -> read raw values\n  -> normalize\n  -> coerce from HTML semantics\n  -> native validation\n  -> transforms\n  -> nested builder\n  -> optional Zod validation\n  -> JSON object or FormData\n```\n\nThe package can build a payload only, or the React hook can optionally call your submit function / URL. For larger applications, keeping network submission in your API layer is usually the cleanest boundary.\n\n## Installation\n\n```bash\nnpm install @cassets/form-pipeline react\n```\n\nOptional Zod validation (any compatible `safeParse` schema can be used):\n\n```bash\nnpm install zod\n```\n\nReact `>=17` is a peer dependency. Zod `>=3` is optional.\n\n## Quick start: payload only\n\n```tsx\nimport { useRef } from 'react';\nimport { formPipe } from '@cassets/form-pipeline';\n\nexport function ProfileForm() {\n  const ref = useRef<HTMLFormElement>(null);\n\n  return (\n    <form\n      ref={ref}\n      onSubmit={(event) => {\n        event.preventDefault();\n        if (!ref.current) return;\n\n        const result = formPipe(ref.current);\n        if (!result.success) {\n          console.error(result.errors);\n          return;\n        }\n\n        console.log(result.payload);\n      }}\n    >\n      <input name=\"profile.name\" required />\n      <input name=\"profile.age\" type=\"number\" min={1} />\n      <button>Save</button>\n    </form>\n  );\n}\n```\n\nFor input values `profile.name=\"Vivek\"` and `profile.age=\"37\"`, the payload is:\n\n```json\n{\n  \"profile\": {\n    \"name\": \"Vivek\",\n    \"age\": 37\n  }\n}\n```\n\n## React hook\n\n```tsx\nimport { useRef } from 'react';\nimport { useForm } from '@cassets/form-pipeline';\n\nexport function LoginForm() {\n  const formRef = useRef<HTMLFormElement>(null);\n  const { handleSubmit, errors, isSubmitting, isSuccess } = useForm({\n    formRef,\n    submit: async (payload) => {\n      const response = await fetch('/api/login', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(payload),\n      });\n      if (!response.ok) throw new Error(`HTTP ${response.status}`);\n      return response.json();\n    },\n  });\n\n  return (\n    <form ref={formRef} onSubmit={handleSubmit}>\n      <input name=\"email\" type=\"email\" required />\n      {errors?.email && <p>{errors.email}</p>}\n\n      <input name=\"password\" type=\"password\" required minLength={8} />\n      {errors?.password && <p>{errors.password}</p>}\n\n      <button disabled={isSubmitting}>\n        {isSubmitting ? 'Signing in...' : 'Sign in'}\n      </button>\n      {isSuccess && <p>Signed in</p>}\n    </form>\n  );\n}\n```\n\nYou can use either a React ref or a selector:\n\n```ts\nuseForm({ formRef: 'form#account' });\n```\n\n## Core API\n\n### `formPipe(form, options?, schema?)`\n\nRuns the full pipeline synchronously.\n\n```ts\nconst result = formPipe<MyPayload>(formElement, options, schema);\n```\n\nResult:\n\n```ts\ninterface PipelineResult<T> {\n  success: boolean;\n  payload: T;\n  errors: Record<string, string> | null;\n  rawValues: Record<string, unknown>;\n}\n```\n\n`rawValues` contains values as read from controls before normalization/coercion/transforms. `payload` is the final value.\n\n### `toFormData(payload)`\n\nConverts a nested payload into `FormData` using dotted keys.\n\n```ts\nimport { toFormData } from '@cassets/form-pipeline';\n\nconst formData = toFormData({\n  profile: { name: 'Vivek' },\n  avatar: file,\n});\n\n// profile.name -> Vivek\n// avatar       -> File\n```\n\n### `useForm(config)`\n\n```ts\nconst {\n  handleSubmit,\n  getPayload,\n  reset,\n  setFieldError,\n  clearErrors,\n  payload,\n  errors,\n  isSubmitting,\n  isSuccess,\n  isError,\n} = useForm({\n  formRef,\n  schema,\n  submit,\n  url,\n  method,\n  submitAs,\n  options,\n  onSuccess,\n  onError,\n  resetOnSuccess,\n});\n```\n\n`getPayload()` validates the current form and returns either the payload, `FormData`, or `null` when validation fails.\n\n## HTML control behavior\n\n### Text, email, password, textarea\n\n```html\n<input name=\"name\" required />\n<input name=\"email\" type=\"email\" required />\n<textarea name=\"bio\" maxlength=\"500\"></textarea>\n```\n\nStrings are trimmed by default. Empty strings become `undefined` by default.\n\n### Number and range\n\n```html\n<input name=\"quantity\" type=\"number\" min=\"1\" max=\"99\" />\n<input name=\"score\" type=\"range\" min=\"0\" max=\"10\" />\n```\n\n`number` and `range` values are converted to JavaScript numbers by default. Numeric-looking text fields remain strings.\n\n### Single checkbox\n\n```html\n<input name=\"termsAccepted\" type=\"checkbox\" required />\n```\n\nProduces a boolean:\n\n```json\n{ \"termsAccepted\": true }\n```\n\n### Checkbox group\n\n```html\n<label><input type=\"checkbox\" name=\"roles\" value=\"admin\" /> Admin</label>\n<label><input type=\"checkbox\" name=\"roles\" value=\"editor\" /> Editor</label>\n<label><input type=\"checkbox\" name=\"roles\" value=\"viewer\" /> Viewer</label>\n```\n\nChecked values become an array:\n\n```json\n{ \"roles\": [\"admin\", \"viewer\"] }\n```\n\n### Radio group\n\n```html\n<label><input type=\"radio\" name=\"plan\" value=\"free\" /> Free</label>\n<label><input type=\"radio\" name=\"plan\" value=\"pro\" /> Pro</label>\n```\n\nOnly the checked radio contributes a value.\n\n### Select and multi-select\n\n```html\n<select name=\"country\" required>\n  <option value=\"IN\">India</option>\n  <option value=\"GB\">United Kingdom</option>\n</select>\n\n<select name=\"skills\" multiple>\n  <option value=\"typescript\">TypeScript</option>\n  <option value=\"react\">React</option>\n  <option value=\"node\">Node.js</option>\n</select>\n```\n\nA multi-select produces a string array.\n\n### Date controls\n\nDates remain strings by default, which is usually safest for APIs.\n\n```html\n<input name=\"startDate\" type=\"date\" />\n```\n\nTo produce `Date` objects:\n\n```ts\nformPipe(form, {\n  coerce: { dates: 'Date' },\n});\n```\n\n### File and multiple files\n\n```html\n<input name=\"avatar\" type=\"file\" accept=\"image/*\" />\n<input name=\"documents\" type=\"file\" multiple />\n```\n\nSingle-file controls produce `File`; multiple-file controls produce `File[]`.\n\nFor transport use `FormData`:\n\n```ts\nconst { handleSubmit } = useForm({\n  formRef,\n  submitAs: 'formdata',\n  submit: async (body) => fetch('/api/profile', { method: 'POST', body }),\n});\n```\n\nDo not manually set the multipart `Content-Type` header; the browser adds its boundary.\n\n## Nested objects and arrays\n\nDot and bracket notation are supported:\n\n```html\n<input name=\"user.name\" value=\"Vivek\" />\n<input name=\"user.address.city\" value=\"Chennai\" />\n<input name=\"items[0].sku\" value=\"A-100\" />\n<input name=\"items[0].quantity\" type=\"number\" value=\"2\" />\n<input name=\"items[1].sku\" value=\"B-200\" />\n```\n\nProduces:\n\n```json\n{\n  \"user\": {\n    \"name\": \"Vivek\",\n    \"address\": { \"city\": \"Chennai\" }\n  },\n  \"items\": [\n    { \"sku\": \"A-100\", \"quantity\": 2 },\n    { \"sku\": \"B-200\" }\n  ]\n}\n```\n\nDangerous object path segments such as `__proto__`, `constructor`, and `prototype` are rejected by the nested builder.\n\n## Native HTML validation\n\nThe browser remains the first validation layer:\n\n```html\n<input name=\"username\" required minlength=\"3\" maxlength=\"30\" />\n<input name=\"email\" type=\"email\" required />\n<input name=\"age\" type=\"number\" min=\"18\" max=\"120\" />\n<input name=\"code\" pattern=\"[A-Z]{3}-[0-9]{4}\" />\n```\n\nInvalid controls are returned in `errors` using the browser's `validationMessage`.\n\n## Zod validation\n\n```tsx\nimport { z } from 'zod';\nimport { useForm } from '@cassets/form-pipeline';\n\nconst schema = z.object({\n  email: z.string().email(),\n  age: z.number().int().min(18),\n});\n\nconst form = useForm<z.infer<typeof schema>>({\n  formRef,\n  schema,\n});\n```\n\nNative validation runs first at control level; Zod validates the assembled payload.\n\n## Normalization and coercion\n\nDefaults:\n\n```ts\n{\n  normalize: {\n    trim: true,\n    emptyToUndefined: true,\n    emptyToNull: false,\n  },\n  coerce: {\n    numbers: true,\n    booleans: true,\n    dates: 'string',\n  },\n  nested: true,\n  skipUnderscore: true,\n  skipAttributes: ['data-skip'],\n  skipDisabled: true,\n  stripEmpty: true,\n}\n```\n\nDisable behavior explicitly:\n\n```ts\nconst result = formPipe(form, {\n  normalize: { trim: false, emptyToUndefined: false },\n  coerce: { numbers: false },\n  nested: false,\n  stripEmpty: false,\n});\n```\n\n## Transform fields\n\n```ts\nconst result = formPipe(form, {\n  transform: {\n    email: (value) => String(value).toLowerCase(),\n  },\n  fields: {\n    amount: {\n      transform: (value) => Math.round(Number(value) * 100),\n    },\n  },\n});\n```\n\nField-specific transforms run before the global transform map for the same field.\n\n## Excluding controls\n\n### `data-skip`\n\n```html\n<input name=\"uiSearch\" data-skip />\n```\n\n### Leading underscore\n\n```html\n<input name=\"_csrf_display_only\" />\n```\n\nNames beginning with `_` are skipped by default.\n\n### Ignore exact names or wildcard patterns\n\n```ts\nformPipe(form, {\n  ignoreFields: ['debug', 'internal.*', 'items.*.temporary'],\n});\n```\n\n### Per-field skip\n\n```ts\nformPipe(form, {\n  fields: {\n    internalToken: { skip: true },\n  },\n});\n```\n\n### Custom inclusion logic\n\n```ts\nformPipe(form, {\n  shouldInclude: ({ fieldName, element, rawValue }) => {\n    return !element.closest('[data-disabled-section]');\n  },\n});\n```\n\n## Direct URL submission\n\nFor small applications, `useForm` can own the final `fetch` call:\n\n```ts\nconst form = useForm({\n  formRef,\n  url: '/api/users',\n  method: 'POST',\n  onSuccess: (response) => console.log(response),\n  onError: (error) => console.error(error),\n  resetOnSuccess: true,\n});\n```\n\nFor authentication, retries, caching, request cancellation, or application-wide error handling, prefer the `submit` callback and your own HTTP layer.\n\n## Application-defined server errors\n\n```ts\nconst { setFieldError, clearErrors } = useForm({ formRef });\n\nsetFieldError('email', 'This email is already registered');\nclearErrors();\n```\n\n## Reset\n\n`reset()` calls the native form `reset()` and clears hook state. `resetOnSuccess: true` does the same after a successful hook-owned submission.\n\n## Browser and SSR notes\n\nThe processing APIs depend on browser DOM types such as `HTMLFormElement`, `File`, and `FormData`. Run them client-side. Importing types is safe, but do not execute the form pipeline during server rendering.\n\n## Security model\n\n- The package does not send credentials unless you configure `useForm` submission.\n- Native browser validation is a UX/client-side guard, not a server security boundary.\n- Always validate and authorize again on the server.\n- Nested path construction blocks prototype-pollution path segments.\n- File `accept` is advisory; validate file type, size, and content on the server.\n\n## Package family\n\n- [`@cassets/http-client`](https://github.com/clusterassets/http-client) — HTTP requests and chunked upload orchestration.\n- [`@cassets/cloud`](https://github.com/clusterassets/cloud) — signed-upload URL adapters for cloud storage.\n\n## Project links\n\n- ClusterAssets GitHub: https://github.com/clusterassets\n- ClusterAssets LinkedIn: https://www.linkedin.com/company/clusterassets\n- Creator GitHub: https://github.com/diskhacker\n- Creator LinkedIn: https://www.linkedin.com/in/kp-vivek-rao-bhosale/\n\n## Contributing and security\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) and [SECURITY.md](./SECURITY.md).\n\n## License\n\nMIT © Vivek Rao Bhosale / ClusterAssets.\n","readmeFilename":"README.md","homepage":"https://github.com/clusterassets/form-pipeline#readme","repository":{"type":"git","url":"git+https://github.com/clusterassets/form-pipeline.git"},"bugs":{"url":"https://github.com/clusterassets/form-pipeline/issues"}}