Repository: maxatwork/form2js Branch: master Commit: f9fe5aed9e47 Files: 131 Total size: 316.8 KB Directory structure: gitextract_xzm58w39/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .github/ │ └── workflows/ │ ├── ci.yml │ ├── pages.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── apps/ │ └── docs/ │ ├── astro.config.mjs │ ├── package.json │ ├── playwright.config.ts │ ├── src/ │ │ ├── components/ │ │ │ ├── api/ │ │ │ │ ├── ApiPackageNav.tsx │ │ │ │ ├── ApiPackageSummaryList.tsx │ │ │ │ └── ApiToc.tsx │ │ │ ├── landing/ │ │ │ │ ├── ApiDocsCta.astro │ │ │ │ ├── Hero.astro │ │ │ │ └── InstallSection.astro │ │ │ └── playground/ │ │ │ ├── PlaygroundShell.tsx │ │ │ ├── ReactInspectorPanel.tsx │ │ │ ├── ResultPanel.tsx │ │ │ ├── StandardResultPanel.tsx │ │ │ ├── VariantHeader.tsx │ │ │ ├── bootstrap/ │ │ │ │ └── jquery-bootstrap.ts │ │ │ ├── types.ts │ │ │ ├── variant-registry.ts │ │ │ └── variants/ │ │ │ ├── core-variant.tsx │ │ │ ├── form-data-variant.tsx │ │ │ ├── form-variant.tsx │ │ │ ├── jquery-variant.tsx │ │ │ ├── js2form-variant.tsx │ │ │ └── react-variant.tsx │ │ ├── env.d.ts │ │ ├── layouts/ │ │ │ ├── ApiDocsLayout.astro │ │ │ ├── DesignShell.astro │ │ │ └── DocsShell.astro │ │ ├── lib/ │ │ │ ├── api-docs-source.ts │ │ │ ├── api-packages.ts │ │ │ └── site-routes.ts │ │ ├── pages/ │ │ │ ├── api/ │ │ │ │ ├── [package].astro │ │ │ │ └── index.astro │ │ │ ├── index.astro │ │ │ └── migrate.astro │ │ └── styles/ │ │ ├── global.css │ │ ├── landing.css │ │ ├── playground.css │ │ └── themes/ │ │ ├── dark-brutalism.css │ │ ├── editorial.css │ │ ├── linear-dark.css │ │ ├── outrun-sunset.css │ │ └── terminal-noir.css │ ├── test/ │ │ ├── api-docs-page.test.tsx │ │ ├── api-docs-source.test.ts │ │ ├── data-variants.test.tsx │ │ ├── docs-pipeline.test.ts │ │ ├── docs-root-scripts.test.ts │ │ ├── homepage-shell.test.ts │ │ ├── playground-shell.test.tsx │ │ ├── playground-styles.test.ts │ │ ├── react-variant.test.tsx │ │ ├── site-routes.test.ts │ │ ├── standard-variants.test.tsx │ │ └── variant-registry.test.ts │ ├── test-e2e/ │ │ ├── api-docs.spec.ts │ │ └── homepage.spec.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── changeset/ │ └── config.json ├── docs/ │ ├── api-core.md │ ├── api-dom.md │ ├── api-form-data.md │ ├── api-index.md │ ├── api-jquery.md │ ├── api-js2form.md │ ├── api-react.md │ └── migrate.md ├── eslint.config.js ├── package.json ├── packages/ │ ├── core/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── test/ │ │ │ └── core.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── dom/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── standalone.ts │ │ ├── test/ │ │ │ └── dom.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── form-data/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── test/ │ │ │ └── form-data.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── jquery/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── standalone.ts │ │ ├── test/ │ │ │ └── jquery.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── js2form/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── test/ │ │ │ └── js2form.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── react/ │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ └── index.ts │ ├── test/ │ │ └── react.test.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── scripts/ │ ├── bump-version.mjs │ └── rewrite-scope.mjs ├── test/ │ └── integration/ │ ├── bump-version.test.ts │ ├── dependency-security.test.ts │ ├── form-flow.test.ts │ ├── package-metadata.test.ts │ └── workflow-node-version.test.ts ├── tsconfig.base.json └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Run `npm run changeset` to add release notes and version bumps. ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [ [ "@form2js/core", "@form2js/dom", "@form2js/form-data", "@form2js/js2form", "@form2js/jquery", "@form2js/react" ] ], "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: permissions: contents: read jobs: checks: name: checks (node ${{ matrix.node-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: [22.14.0] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - name: Install run: npm ci - name: Lint run: npm run lint - name: Typecheck run: npm run typecheck - name: Test run: npm run test:packages && npm run test:integration - name: Build run: npm run build - name: Package Dry Run run: npm run pack:dry-run docs-e2e: name: docs-e2e (node 22.14.0) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22.14.0 cache: npm - name: Install run: npm ci - name: Install Playwright Chromium run: npm -w @form2js/docs exec playwright install --with-deps chromium - name: Build workspace packages run: npm run build:packages - name: Test Docs E2E run: npm run test:docs ================================================ FILE: .github/workflows/pages.yml ================================================ name: Deploy Docs Site on: push: branches: - master paths: - "apps/docs/**" - "docs/**" - "packages/**" - ".github/workflows/pages.yml" - "package.json" - "package-lock.json" - "turbo.json" - "tsconfig.base.json" workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22.14.0 cache: npm - name: Install run: npm ci - name: Configure Pages id: pages uses: actions/configure-pages@v5 - name: Build workspace packages run: npm run build:packages - name: Build docs app run: npm -w @form2js/docs run build env: DOCS_BASE_PATH: ${{ steps.pages.outputs.base_path }} - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: path: apps/docs/dist deploy: runs-on: ubuntu-latest needs: build environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - master permissions: contents: write pull-requests: write id-token: write jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22.14.0 registry-url: https://registry.npmjs.org cache: npm - name: Use npm 11 run: npm install --global npm@11.6.2 - name: Verify toolchain run: node --version && npm --version - name: Install run: npm ci - name: Build workspace packages run: npm run build:packages - name: Create release PR or publish uses: changesets/action@v1 with: version: npx changeset version publish: npm run release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: true ================================================ FILE: .gitignore ================================================ node_modules .npm .turbo dist coverage *.tsbuildinfo .idea .hg .hgignore .astro **/.astro/ docs/triage-* .env .env.local .superpowers/brainstorm .worktrees/ .worktrees apps/docs/test-results/ apps/docs/playwright-report/ docs/superpowers/ ================================================ FILE: LICENSE ================================================ Copyright (c) 2010 Maxim Vasiliev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # form2js 🚀 **form2js is back — modernized and actively maintained.** Originally created in 2010, now rewritten for modern JavaScript, TypeScript, ESM, React, and modular usage. Legacy version is available in the [legacy branch](https://github.com/maxatwork/form2js/tree/legacy). Migrating from legacy form2js? Start with the [migration guide](https://maxatwork.github.io/form2js/migrate/). ## Description A small family of packages for turning form-shaped data into objects, and objects back into forms. It is not a serializer, not an ORM, and not a new religion. It just does this one job, does it reliably, and leaves before anyone starts a committee about it. ## Documentation - [Docs Site](https://maxatwork.github.io/form2js/) - overview, installation, unified playground, and published API reference. - [Migration Guide](https://maxatwork.github.io/form2js/migrate/) - map old `form2js` and `jquery.toObject` usage to the current package family. - [API Reference Source](docs/api-index.md) - markdown source for the published API docs page. ## Migration from Legacy If you are moving from the archived single-package version, start with the [migration guide](https://maxatwork.github.io/form2js/migrate/). Quick package map: - Legacy browser `form2js(...)` usage -> `@form2js/dom` - Legacy jQuery `$("#form").toObject()` usage -> `@form2js/jquery` - Server or pipeline `FormData` parsing -> `@form2js/form-data` - React submit handling -> `@form2js/react` - Object back into fields -> `@form2js/js2form` The current project keeps the naming rules and core parsing model, but splits the old browser-era API into environment-specific packages. ## Packages | Package | npm | Purpose | Module | Standalone | Node.js | | ------- | --- | ------- | ------ | ---------- | ------- | | [`@form2js/react`](https://www.npmjs.com/package/@form2js/react) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Freact?label=npm)](https://www.npmjs.com/package/@form2js/react) | React submit hook with parsing/validation state | Yes | No | Browser-focused | | [`@form2js/dom`](https://www.npmjs.com/package/@form2js/dom) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fdom?label=npm)](https://www.npmjs.com/package/@form2js/dom) | Extract DOM fields to object (`formToObject`, `form2js`) | Yes | Yes | With DOM shim (`jsdom`) | | [`@form2js/form-data`](https://www.npmjs.com/package/@form2js/form-data) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fform-data?label=npm)](https://www.npmjs.com/package/@form2js/form-data) | Convert `FormData`/entries to object | Yes | No | Yes | | [`@form2js/js2form`](https://www.npmjs.com/package/@form2js/js2form) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fjs2form?label=npm)](https://www.npmjs.com/package/@form2js/js2form) | Populate DOM fields from object (`objectToForm`, `js2form`) | Yes | No | With DOM shim (`jsdom`) | | [`@form2js/core`](https://www.npmjs.com/package/@form2js/core) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fcore?label=npm)](https://www.npmjs.com/package/@form2js/core) | Path parsing and object transformation engine | Yes | No | Yes | | [`@form2js/jquery`](https://www.npmjs.com/package/@form2js/jquery) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fjquery?label=npm)](https://www.npmjs.com/package/@form2js/jquery) | jQuery plugin adapter (`$.fn.toObject`) | Yes | Yes | Browser-focused | ## Installation Install only what you need: ```bash npm install @form2js/react react npm install @form2js/dom npm install @form2js/form-data npm install @form2js/js2form npm install @form2js/core npm install @form2js/jquery jquery ``` For browser standalone usage, use script builds where available: - `@form2js/dom`: `dist/standalone.global.js` - `@form2js/jquery`: `dist/standalone.global.js` ## Usage ### `@form2js/dom` HTML used in examples: ```html
``` Module: ```ts import { formToObject } from "@form2js/dom"; const result = formToObject(document.getElementById("profileForm")); // => { person: { name: { first: "Esme", last: "Weatherwax" }, tags: ["witch"] } } ``` Standalone: ```html ``` ### `@form2js/form-data` Module (browser or Node 18+): ```ts import { formDataToObject } from "@form2js/form-data"; const fd = new FormData(formElement); const result = formDataToObject(fd); ``` Node.js note: - Node 18+ has global `FormData`. - You can also pass iterable entries directly, which is handy in server pipelines: ```ts import { entriesToObject } from "@form2js/form-data"; const result = entriesToObject([ ["person.name.first", "Sam"], ["person.roles[]", "captain"], ]); // => { person: { name: { first: "Sam" }, roles: ["captain"] } } ``` With schema validation (works with Zod or any `{ parse(unknown) }` schema): ```ts import { z } from "zod"; import { formDataToObject } from "@form2js/form-data"; const PersonSchema = z.object({ person: z.object({ age: z.coerce.number().int().min(0) }) }); const result = formDataToObject([["person.age", "42"]], { schema: PersonSchema }); // => { person: { age: 42 } } ``` Standalone: - Not shipped for this package. Use module imports. ### `@form2js/react` Module: ```ts import { z } from "zod"; import { useForm2js } from "@form2js/react"; const FormDataSchema = z.object({ person: z.object({ name: z.object({ first: z.string().min(1) }) }) }); export function ProfileForm(): React.JSX.Element { const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js( async (data) => { // data is inferred from schema when schema is provided await sendFormData(data); }, { schema: FormDataSchema } ); return (
{ void onSubmit(event); }} > {isError ?

{String(error)}

: null} {isSuccess ?

Saved

: null}
); } ``` ### `@form2js/jquery` HTML used in examples: ```html
``` Module: ```ts import $ from "jquery"; import { installToObjectPlugin } from "@form2js/jquery"; installToObjectPlugin($); const data = $("#profileForm").toObject({ mode: "first" }); // => { person: { name: { first: "Sam", last: "Vimes" } } } ``` Standalone: ```html ``` ### `@form2js/js2form` HTML used in examples (before calling `objectToForm`): ```html
``` Module: ```ts import { objectToForm } from "@form2js/js2form"; objectToForm(document.getElementById("profileForm"), { person: { name: { first: "Tiffany", last: "Aching" } }, }); // fields are now populated in the form ``` Standalone: - Not shipped as a dedicated global bundle. Use module imports. ### `@form2js/core` Module: ```ts import { entriesToObject, objectToEntries } from "@form2js/core"; const data = entriesToObject([ { key: "person.name.first", value: "Vimes" }, { key: "person.tags[]", value: "watch" }, ]); const pairs = objectToEntries(data); ``` Node.js: - Fully supported (no DOM dependency). Standalone: - Not shipped for this package. Use module imports. ## Legacy behavior notes Compatibility with the old project is intentional. - Name paths define output shape (`person.name.first`). - Array and indexed syntax is preserved (`items[]`, `items[5].name`). - Rails-style names are supported (`rails[field][value]`). - DOM extraction follows native browser form submission semantics for checkbox and radio values. - Unsafe key path segments (`__proto__`, `prototype`, `constructor`) are rejected by default. - This library does data shaping, not JSON/XML serialization. ## Design boundaries and non-goals These boundaries are intentional and are used for issue triage. - Sparse indexes are compacted in first-seen order (`items[5]`, `items[8]` -> `items[0]`, `items[1]`). - Type inference is minimal by design; DOM extraction keeps native string values instead of coercing checkbox/radio fields. - Unchecked indexed controls are omitted and therefore do not reserve compacted array slots; include another submitted field when row identity matters. - `formToObject` reads successful form control values, not option labels. Disabled controls (including disabled fieldset descendants) and button-like inputs are excluded unless you explicitly opt in to disabled values. - `extractPairs`/`formToObject` support `nodeCallback`; return `SKIP_NODE` to exclude a node entirely, or `{ name|key, value }` to inject a custom entry. - Parser inputs reject unsafe path segments by default. Use `allowUnsafePathSegments: true` only with trusted inputs. - `objectToForm` supports `nodeCallback`; returning `false` skips the default assignment for that node. - `objectToForm` sets form control state and values; it does not dispatch synthetic `change` or `input` events. - Empty collections are not synthesized when no matching fields are present (for example, unchecked checkbox groups). - Dynamic key/value remapping (for example, converting `key`/`val` fields into arbitrary object keys) is application logic. - For file payloads and richer multipart semantics, use `FormData` and `@form2js/form-data`. ## Contributing ### Setup ```bash npm ci ``` ### Local checks ```bash npm run lint npm run typecheck npm run test npm run build npm run pack:dry-run ``` ### Local docs site ```bash npm run docs npm run docs:build ``` The homepage includes the unified playground for `@form2js/react`, `@form2js/dom`, `@form2js/jquery`, `@form2js/js2form`, `@form2js/core`, and `@form2js/form-data`. ### GitHub Pages docs site The published docs site is deployed by `.github/workflows/pages.yml`. - Trigger: pushes to `master` that touch `apps/docs/**`, `docs/**`, `packages/**`, `.github/workflows/pages.yml`, `package.json`, `package-lock.json`, `turbo.json`, or `tsconfig.base.json`, plus manual `workflow_dispatch`. - Output: `apps/docs/dist`. - URL: `https://maxatwork.github.io/form2js/`. In repository settings, set Pages source to `GitHub Actions` once, and then the workflow handles updates. ### Before opening a PR 1. Keep changes focused to one problem area where possible. 2. Add or update tests for behavior changes. 3. Add a changeset (`npm run changeset`) for user-visible changes. 4. Include migration notes in README if behavior or API changes. ### Filing PRs and issues Please include: - Clear expected vs actual behavior. - Minimal reproduction (HTML snippet or input entries). - Package name and version. - Environment (`node -v`, browser/version if relevant). ## Release workflow - CI runs lint, typecheck, test, build, and package dry-run. - Releases are managed with Changesets and the published `@form2js/*` packages are versioned in lockstep. ## Scope rewrite helper Default scope is `@form2js/*`. If you need to publish under another scope: ```bash npm run scope:rewrite -- --scope @your-scope --dry-run npm run scope:rewrite -- --scope @your-scope ``` This rewrites package names, internal dependencies, and import references. ## License MIT, see `LICENSE`. ================================================ FILE: apps/docs/astro.config.mjs ================================================ import { defineConfig } from "astro/config"; import react from "@astrojs/react"; const base = process.env.DOCS_BASE_PATH ?? "/"; export default defineConfig({ base, integrations: [react()], vite: { server: { fs: { allow: ["../.."] } } } }); ================================================ FILE: apps/docs/package.json ================================================ { "name": "@form2js/docs", "private": true, "type": "module", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", "test": "vitest run", "test:e2e": "playwright test", "lint": "eslint \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\" \"test-e2e/**/*.ts\"", "typecheck": "astro sync && tsc --noEmit", "clean": "rimraf dist .astro" }, "dependencies": { "@astrojs/react": "^5.0.2", "@form2js/core": "3.4.0", "@form2js/dom": "3.4.0", "@form2js/form-data": "3.4.0", "@form2js/jquery": "3.4.0", "@form2js/js2form": "3.4.0", "@form2js/react": "3.4.0", "astro": "^6.1.1", "jquery": "^3.7.1", "react": "^19.1.1", "react-dom": "^19.1.1", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "unified": "^11.0.5", "zod": "^4.1.5" }, "devDependencies": { "@playwright/test": "^1.54.2", "@types/jquery": "^3.5.33", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9" } } ================================================ FILE: apps/docs/playwright.config.ts ================================================ import { defineConfig, devices } from "@playwright/test"; const docsE2eHost = process.env.DOCS_E2E_HOST ?? "127.0.0.1"; const docsE2ePort = Number(process.env.DOCS_E2E_PORT ?? "4321"); const docsE2eUrl = `http://${docsE2eHost}:${docsE2ePort}/`; export default defineConfig({ testDir: "./test-e2e", use: { baseURL: docsE2eUrl, trace: "retain-on-failure" }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] } } ], webServer: { command: `PUBLIC_DOCS_E2E_FAULTS=1 npm -w @form2js/docs run build && npm -w @form2js/docs run preview -- --host ${docsE2eHost} --port ${docsE2ePort}`, url: docsE2eUrl, reuseExistingServer: !process.env.CI } }); ================================================ FILE: apps/docs/src/components/api/ApiPackageNav.tsx ================================================ import React from "react"; import type { ApiPackageEntry } from "../../lib/api-packages"; import { apiPackageDocsPath } from "../../lib/site-routes"; type ApiPackageNavEntry = Pick; interface ApiPackageNavProps { activeSlug?: ApiPackageNavEntry["slug"]; basePath: string; packages: ApiPackageNavEntry[]; } export function ApiPackageNav({ activeSlug, basePath, packages }: ApiPackageNavProps): React.JSX.Element { return ( ); } ================================================ FILE: apps/docs/src/components/api/ApiPackageSummaryList.tsx ================================================ import React from "react"; import type { ApiPackageEntry } from "../../lib/api-packages"; import { apiPackageDocsPath } from "../../lib/site-routes"; type ApiPackageSummaryEntry = Pick< ApiPackageEntry, "slug" | "packageName" | "summary" >; interface ApiPackageSummaryListProps { basePath: string; packages: ApiPackageSummaryEntry[]; } export function ApiPackageSummaryList({ basePath, packages }: ApiPackageSummaryListProps): React.JSX.Element { return (

Packages

{packages.map((entry) => ( ))}
); } ================================================ FILE: apps/docs/src/components/api/ApiToc.tsx ================================================ import React, { useEffect, useMemo, useState } from "react"; import type { ApiHeading } from "../../lib/api-docs-source"; interface ApiTocProps { headings: ApiHeading[]; initialActiveSlug?: string; } interface TocGroup { heading: ApiHeading; children: ApiHeading[]; } function groupHeadings(headings: ApiHeading[]): TocGroup[] { const groups: TocGroup[] = []; for (const heading of headings) { if (heading.depth === 2 || groups.length === 0) { groups.push({ heading, children: [] }); continue; } groups[groups.length - 1]?.children.push(heading); } return groups; } export function ApiToc({ headings, initialActiveSlug }: ApiTocProps): React.JSX.Element { const groups = useMemo(() => groupHeadings(headings), [headings]); const [activeSlug, setActiveSlug] = useState(initialActiveSlug ?? headings[0]?.slug ?? ""); useEffect(() => { if (typeof window === "undefined") { return; } const hashSlug = window.location.hash.replace(/^#/, ""); if (hashSlug) { setActiveSlug(hashSlug); } const observedHeadings = headings .map((heading) => document.getElementById(heading.slug)) .filter((heading): heading is HTMLElement => Boolean(heading)); if (observedHeadings.length === 0 || typeof IntersectionObserver === "undefined") { return; } const observer = new IntersectionObserver( (entries) => { const visibleEntry = entries .filter((entry) => entry.isIntersecting) .sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0]; if (visibleEntry?.target.id) { setActiveSlug(visibleEntry.target.id); } }, { rootMargin: "-20% 0px -60% 0px", threshold: [0.2, 0.6, 1] } ); for (const heading of observedHeadings) { observer.observe(heading); } const handleHashChange = (): void => { const nextHashSlug = window.location.hash.replace(/^#/, ""); if (nextHashSlug) { setActiveSlug(nextHashSlug); } }; window.addEventListener("hashchange", handleHashChange); return () => { observer.disconnect(); window.removeEventListener("hashchange", handleHashChange); }; }, [headings]); return ( ); } ================================================ FILE: apps/docs/src/components/landing/ApiDocsCta.astro ================================================ --- // apps/docs/src/components/landing/ApiDocsCta.astro import { apiDocsPath } from "../../lib/site-routes"; const basePath = import.meta.env.BASE_URL; ---

Reference

API Documentation

Exact signatures, option defaults, TypeScript types, and compatibility notes for every package — all generated from the same source of truth.

Open API Docs →
================================================ FILE: apps/docs/src/components/landing/Hero.astro ================================================ --- // apps/docs/src/components/landing/Hero.astro import { apiDocsPath } from "../../lib/site-routes"; const basePath = import.meta.env.BASE_URL; ---

form serialization library

Turn forms
into objects.

Parse browser forms into structured JavaScript objects. Six adapters — React hooks, vanilla DOM, jQuery, FormData, and more. One coherent API.

$ npm install @form2js/react react Try the playground ↓ API Docs →
================================================ FILE: apps/docs/src/components/landing/InstallSection.astro ================================================ --- // apps/docs/src/components/landing/InstallSection.astro ---

Use in your project

Install

Pick the adapter for your stack. All packages share the same path-syntax so switching is painless.

npm install @form2js/react react
{`import { useForm2js } from '@form2js/react'

const { onSubmit, isSubmitting } = useForm2js(handler)

// nested names just work:
// 
// → { user: { email: '…' } }`}
================================================ FILE: apps/docs/src/components/playground/PlaygroundShell.tsx ================================================ // apps/docs/src/components/playground/PlaygroundShell.tsx import React, { useEffect, useState } from "react"; import { VARIANT_IDS, variantsById } from "./variant-registry"; import { ResultPanel } from "./ResultPanel"; import type { ErrorInfo, OutputState, VariantDefinition, VariantId } from "./types"; import { VariantHeader } from "./VariantHeader"; function getRequestedRenderFault(): { variantId: VariantId; message: string } | null { if (typeof window === "undefined") return null; const requestedFault = new URLSearchParams(window.location.search).get("__fault"); if (!requestedFault) return null; const [requestedVariantId, source] = requestedFault.split(":"); if (source !== "render" || !VARIANT_IDS.includes(requestedVariantId as VariantId)) return null; const variantId = requestedVariantId as VariantId; return { variantId, message: `Injected render fault for ${variantsById[variantId].label}` }; } function getActiveVariantId(): VariantId { if (typeof window === "undefined") return "react"; const current = new URLSearchParams(window.location.search).get("variant"); if (current && VARIANT_IDS.includes(current as VariantId)) return current as VariantId; return "react"; } function createInitialOutputStates( variantId: VariantId ): Partial> { return { [variantId]: variantsById[variantId].createInitialOutputState() }; } function dispatchVariantChange(variantId: VariantId): void { if (typeof window === "undefined") return; const variant = variantsById[variantId]; window.dispatchEvent( new CustomEvent("form2js:variant-change", { detail: { variantId, packages: variant.packages } }) ); } interface VariantErrorBoundaryProps { children: React.ReactNode; onError: (errorInfo: ErrorInfo) => void; } interface VariantErrorBoundaryState { hasError: boolean; } class VariantErrorBoundary extends React.Component { state: VariantErrorBoundaryState = { hasError: false }; static getDerivedStateFromError(): VariantErrorBoundaryState { return { hasError: true }; } componentDidCatch(error: Error): void { this.props.onError({ message: error.message, source: "render" }); } render(): React.ReactNode { if (this.state.hasError) return null; return this.props.children; } } export function PlaygroundShell(): React.JSX.Element { const [activeVariantId, setActiveVariantId] = useState(() => getActiveVariantId()); const [mountedVariantIds, setMountedVariantIds] = useState(() => [getActiveVariantId()]); const [outputStates, setOutputStates] = useState>>(() => createInitialOutputStates(getActiveVariantId()) ); const [failedVariants, setFailedVariants] = useState>>({}); useEffect(() => { dispatchVariantChange(activeVariantId); }, []); useEffect(() => { if (!mountedVariantIds.includes(activeVariantId)) { setMountedVariantIds((current) => [...current, activeVariantId]); } setOutputStates((current) => { if (current[activeVariantId]) return current; return { ...current, [activeVariantId]: variantsById[activeVariantId].createInitialOutputState() }; }); }, [activeVariantId, mountedVariantIds]); useEffect(() => { const requestedFault = getRequestedRenderFault(); if (requestedFault?.variantId !== activeVariantId || failedVariants[activeVariantId] !== undefined) { return; } handleVariantFailure(activeVariantId, { message: requestedFault.message, source: "render" }); }, [activeVariantId, failedVariants]); const activeVariant = variantsById[activeVariantId]; const activeOutputState = outputStates[activeVariantId] ?? activeVariant.createInitialOutputState(); const variants = VARIANT_IDS.map((variantId) => variantsById[variantId]); function handleVariantFailure(variantId: VariantId, errorInfo: ErrorInfo): void { setFailedVariants((current) => ({ ...current, [variantId]: errorInfo })); setOutputStates((current) => { return Object.fromEntries( Object.entries(current).filter(([currentVariantId]) => currentVariantId !== variantId) ) as Partial>; }); } function selectVariant(variantId: VariantId): void { if (typeof window !== "undefined") { const nextUrl = new URL(window.location.href); nextUrl.searchParams.set("variant", variantId); window.history.replaceState({}, "", `${nextUrl.pathname}${nextUrl.search}`); dispatchVariantChange(variantId); } setActiveVariantId(variantId); } return (

{activeVariant.summary}

{mountedVariantIds.map((variantId) => { const variant: VariantDefinition = variantsById[variantId]; const isActive = variantId === activeVariantId; if (failedVariants[variantId]) return null; return ( ); })} {failedVariants[activeVariantId] && (

{activeVariant.label} failed to load.

{failedVariants[activeVariantId]?.message}

)}
); } ================================================ FILE: apps/docs/src/components/playground/ReactInspectorPanel.tsx ================================================ import React from "react"; import type { ReactOutputState } from "./types"; interface ReactInspectorPanelProps { outputState: ReactOutputState; } export function ReactInspectorPanel({ outputState }: ReactInspectorPanelProps): React.JSX.Element { const metaEntries = outputState.meta ? Object.entries(outputState.meta) : []; const hasParsedPayload = outputState.parsedPayload !== null; return (

Submit state

{outputState.statusMessage}

isSubmitting: {String(outputState.submitFlags.isSubmitting)}

isError: {String(outputState.submitFlags.isError)}

isSuccess: {String(outputState.submitFlags.isSuccess)}

{metaEntries.length > 0 ? (
{metaEntries.map(([key, value]) => (
{key}
{value === null ? "null" : typeof value === "boolean" ? String(value) : value}
))}
) : null} {hasParsedPayload ?
{JSON.stringify(outputState.parsedPayload, null, 2)}
: null} {outputState.error ?

{outputState.error.message}

: null}
); } ================================================ FILE: apps/docs/src/components/playground/ResultPanel.tsx ================================================ // apps/docs/src/components/playground/ResultPanel.tsx import React from "react"; import type { OutputState } from "./types"; interface ResultPanelProps { outputState: OutputState; } function formatZodErrors(error: { message: string }): string[] { // The error message from react-variant is pre-formatted as "path: msg\npath: msg" return error.message.split("\n").filter(Boolean); } export function ResultPanel({ outputState }: ResultPanelProps): React.JSX.Element { const statusClass = `status-${outputState.status}`; if (outputState.kind === "react") { const { submitFlags, error, parsedPayload, meta } = outputState; const errorLines = error ? formatZodErrors(error) : []; const metaEntries = meta ? Object.entries(meta) : []; return (

Output

Submit state

{outputState.statusMessage}
isSubmitting {String(submitFlags.isSubmitting)} isError {String(submitFlags.isError)} isSuccess {String(submitFlags.isSuccess)}
{metaEntries.length > 0 && (
{metaEntries.map(([key, value]) => (
{key}
{value === null ? "null" : typeof value === "boolean" ? String(value) : value}
))}
)} {errorLines.length > 0 && (
{errorLines.map((line, i) => (

{line}

))}
)} {parsedPayload !== null ? (
{JSON.stringify(parsedPayload, null, 2)}
) : (

{outputState.status === "idle" ? "Submit the form to see parsed output." : ""}

)}
); } // standard kind const { errorMessage, parsedPayload, statusMessage } = outputState; return (

Output

Parsed result

{statusMessage} {errorMessage && (

{errorMessage}

)} {parsedPayload !== null ? (
{JSON.stringify(parsedPayload, null, 2)}
) : (

{outputState.status === "idle" ? "Run the variant to see parsed output." : ""}

)}
); } ================================================ FILE: apps/docs/src/components/playground/StandardResultPanel.tsx ================================================ import React from "react"; import type { StandardOutputState } from "./types"; interface StandardResultPanelProps { outputState: StandardOutputState; } export function StandardResultPanel({ outputState }: StandardResultPanelProps): React.JSX.Element { const hasParsedPayload = outputState.parsedPayload !== null; return (

Parsed result

{outputState.statusMessage}

{outputState.errorMessage ?

{outputState.errorMessage}

: null} {hasParsedPayload ?
{JSON.stringify(outputState.parsedPayload, null, 2)}
: null}
); } ================================================ FILE: apps/docs/src/components/playground/VariantHeader.tsx ================================================ // apps/docs/src/components/playground/VariantHeader.tsx import React from "react"; import type { VariantDefinition, VariantId } from "./types"; interface VariantHeaderProps { activeId: VariantId; variants: VariantDefinition[]; onSelect: (variantId: VariantId) => void; } export function VariantHeader({ activeId, variants, onSelect }: VariantHeaderProps): React.JSX.Element { return (
{variants.map((variant) => ( ))}
); } ================================================ FILE: apps/docs/src/components/playground/bootstrap/jquery-bootstrap.ts ================================================ import $ from "jquery"; import { installToObjectPlugin } from "@form2js/jquery"; type JQueryWithPlugin = typeof $ & { fn: { toObject?: unknown; }; }; let installedPlugin: unknown = null; export function ensureJqueryBootstrap(): unknown { const jquery = $ as JQueryWithPlugin; if (typeof jquery.fn.toObject !== "function") { installToObjectPlugin(jquery); } installedPlugin = jquery.fn.toObject ?? null; return installedPlugin; } ================================================ FILE: apps/docs/src/components/playground/types.ts ================================================ import type { ReactNode } from "react"; export type VariantKind = "react" | "standard"; export type OutputStatus = "idle" | "running" | "success" | "error"; export type VariantId = "react" | "form" | "jquery" | "js2form" | "core" | "form-data"; export interface ErrorInfo { message: string; source: "render" | "effect" | "bootstrap" | "event" | "async"; detail?: string; } export interface ReactOutputState { kind: "react"; status: OutputStatus; statusMessage: string; submitFlags: { isSubmitting: boolean; isError: boolean; isSuccess: boolean; }; error: { message: string; detail?: string } | null; parsedPayload: unknown; meta?: Record; } export interface StandardOutputState { kind: "standard"; status: OutputStatus; statusMessage: string; errorMessage: string | null; parsedPayload: unknown; } export type OutputState = ReactOutputState | StandardOutputState; export interface VariantComponentProps { isActive: boolean; onOutputChange: (outputState: OutputState) => void; reportFatalError: (errorInfo: ErrorInfo) => void; } export interface VariantDefinition { id: VariantId; kind: VariantKind; label: string; summary: string; packages: string[]; createInitialOutputState: () => OutputState; Component: (props: VariantComponentProps) => ReactNode; } ================================================ FILE: apps/docs/src/components/playground/variant-registry.ts ================================================ import type { OutputState, VariantDefinition, VariantId } from "./types"; import { CoreVariant } from "./variants/core-variant"; import { FormDataVariant } from "./variants/form-data-variant"; import { FormVariant } from "./variants/form-variant"; import { JQueryVariant } from "./variants/jquery-variant"; import { Js2FormVariant } from "./variants/js2form-variant"; import { ReactVariant } from "./variants/react-variant"; function createStandardIdle(statusMessage: string): OutputState { return { kind: "standard", status: "idle", statusMessage, errorMessage: null, parsedPayload: null }; } function createReactIdle(statusMessage: string): OutputState { return { kind: "react", status: "idle", statusMessage, submitFlags: { isSubmitting: false, isError: false, isSuccess: false }, error: null, parsedPayload: null }; } export const variants = [ { id: "react", kind: "react", label: "React", summary: "Submit forms with schema-aware async state.", packages: ["@form2js/react"], createInitialOutputState: () => createReactIdle("Ready to submit.") }, { id: "form", kind: "standard", label: "Form", summary: "Parse a plain browser form with @form2js/dom or form2js().", packages: ["@form2js/dom"], createInitialOutputState: () => createStandardIdle("Ready to parse the form.") }, { id: "jquery", kind: "standard", label: "jQuery", summary: "Use the jQuery plugin adapter with selectable modes.", packages: ["@form2js/jquery"], createInitialOutputState: () => createStandardIdle("Ready to run the plugin.") }, { id: "js2form", kind: "standard", label: "js2form", summary: "Apply object data back into form controls.", packages: ["@form2js/js2form"], createInitialOutputState: () => createStandardIdle("Ready to apply object data.") }, { id: "core", kind: "standard", label: "Core", summary: "Parse raw key/value entries into nested objects.", packages: ["@form2js/core"], createInitialOutputState: () => createStandardIdle("Ready to parse entry data.") }, { id: "form-data", kind: "standard", label: "FormData", summary: "Convert FormData-like entries into structured objects.", packages: ["@form2js/form-data"], createInitialOutputState: () => createStandardIdle("Ready to parse form data.") } ] satisfies Omit[]; export const VARIANT_IDS = variants.map((variant) => variant.id) satisfies VariantId[]; const variantComponents: Record = { react: ReactVariant, form: FormVariant, jquery: JQueryVariant, js2form: Js2FormVariant, core: CoreVariant, "form-data": FormDataVariant }; export const variantsById = Object.fromEntries( variants.map((variant) => [ variant.id, { ...variant, Component: variantComponents[variant.id] } ]) ) as Record; ================================================ FILE: apps/docs/src/components/playground/variants/core-variant.tsx ================================================ // apps/docs/src/components/playground/variants/core-variant.tsx import React, { useRef } from "react"; import { entriesToObject } from "@form2js/core"; import type { StandardOutputState, VariantComponentProps } from "../types"; const INITIAL_ENTRIES_JSON = `[ { "key": "person.name.first", "value": "Moist" }, { "key": "person.name.last", "value": "von Lipwig" }, { "key": "person.city", "value": "ankh-morpork" }, { "key": "person.guild", "value": "thieves" }, { "key": "person.tags[]", "value": "crime" }, { "key": "person.tags[]", "value": "banking" } ]`; function createErrorState(message: string): StandardOutputState { return { kind: "standard", status: "error", statusMessage: "core parse failed.", errorMessage: message, parsedPayload: null }; } function createSuccessState(parsedPayload: unknown): StandardOutputState { return { kind: "standard", status: "success", statusMessage: "@form2js/core -> entriesToObject(entry objects)", errorMessage: null, parsedPayload }; } function formatVariantError(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } export function CoreVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element { const jsonInputRef = useRef(null); function handleRun(): void { const jsonInput = jsonInputRef.current; if (!jsonInput) return; let parsed: { key: string; value: unknown }[]; try { parsed = JSON.parse(jsonInput.value) as { key: string; value: unknown }[]; } catch { onOutputChange(createErrorState("JSON parse error: please provide valid entry-object JSON before parsing core entries.")); return; } try { onOutputChange(createSuccessState(entriesToObject(parsed))); } catch (error: unknown) { onOutputChange(createErrorState(`Core conversion failed: ${formatVariantError(error)}`)); } } return (