{"_id":"@molecule/app-forms","_rev":"3-000f40eddad7490baa4f95d162b91f4b","name":"@molecule/app-forms","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@molecule/app-forms","version":"1.0.0","keywords":["molecule","forms","validation"],"license":"Apache-2.0","_id":"@molecule/app-forms@1.0.0","maintainers":[{"name":"vialoh","email":"npm@vialoh.me"}],"homepage":"https://github.com/molecule-dev/molecule/tree/main/packages/app/core/forms","bugs":{"url":"https://github.com/molecule-dev/molecule/issues"},"dist":{"shasum":"be15a7c6af7ced9957d1a64a6cfc7193f14eda32","tarball":"https://registry.npmjs.org/@molecule/app-forms/-/app-forms-1.0.0.tgz","fileCount":26,"integrity":"sha512-HAtNomrKq0Irwz19LP1WasS19nVSg047T5WoIBfwHYEh8hMvtrViPOrMoQ/t9965jsy7Dd9CMY3NbC82LpHBaQ==","signatures":[{"sig":"MEYCIQCe8HMPdjd0RMlUFyw71G7JHww4g3xm2O3mFYiahljiPgIhAKMXddZ5TQ2TwVjSQdK5WOZ+tha1DBIQLEMkMyiGTfH5","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63242},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"92623e72a527ca467963169420f4cf07533e4699","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"vialoh","email":"npm@vialoh.me"},"repository":{"url":"git+https://github.com/molecule-dev/molecule.git","type":"git","directory":"packages/app/core/forms"},"_npmVersion":"11.12.1","description":"Form handling interface for molecule.dev","directories":{},"_nodeVersion":"25.9.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.1.10","typescript":"6.0.3","@types/node":"26.1.2","@molecule/app-bond":"1.0.0"},"peerDependencies":{"@molecule/app-bond":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/app-forms_1.0.0_1785796138427_0.7940809591653053","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@molecule/app-forms","version":"1.0.1","keywords":["molecule","forms","validation"],"license":"Apache-2.0","_id":"@molecule/app-forms@1.0.1","maintainers":[{"name":"vialoh","email":"npm@vialoh.me"}],"homepage":"https://github.com/molecule-dev/molecule/tree/main/packages/app/core/forms","bugs":{"url":"https://github.com/molecule-dev/molecule/issues"},"dist":{"shasum":"de8dd457c3a7140161b3fcbc44c4cdfb81fef0a5","tarball":"https://registry.npmjs.org/@molecule/app-forms/-/app-forms-1.0.1.tgz","fileCount":27,"integrity":"sha512-cQYTFSTpNQ1NraZntB98aCZBQf2XupMEf3d9PS558ssYi7rKt9/LPFNKhDTr1ZieBVufkc4ETaKJRmiDeCMdSQ==","signatures":[{"sig":"MEQCIByxC3Y2H8oNthztUtnSnS4Lw2S/baCZ8cqtg2zQYgx0AiBk0APJk1U0ir1lHBLr61XBabCRGJm+bxqB9P3YNrrckA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@molecule%2fapp-forms@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":77026},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"8621216fd4c8c9abe863e4e4f41efd2bc866fb09","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"vialoh","email":"npm@vialoh.me"},"repository":{"url":"git+https://github.com/molecule-dev/molecule.git","type":"git","directory":"packages/app/core/forms"},"_npmVersion":"12.0.2","description":"Form handling interface for molecule.dev","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.1.10","typescript":"6.0.3","@types/node":"26.1.2","@molecule/app-bond":"1.0.1"},"peerDependencies":{"@molecule/app-bond":"^1.0.1"},"_npmOperationalInternal":{"tmp":"tmp/app-forms_1.0.1_1785820974445_0.6689527023278954","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"_id":"@molecule/app-forms@1.0.2","bugs":{"url":"https://github.com/molecule-dev/molecule/issues"},"dist":{"shasum":"6f4e982b1bfbacf87ecb998f467ad7a343ca54c5","tarball":"https://registry.npmjs.org/@molecule/app-forms/-/app-forms-1.0.2.tgz","fileCount":27,"integrity":"sha512-MEJxbDoNFQmXboaif4dtuE3LRqzIbBtE/NHSDYd3QqJR0YSvqFWBwOq3S14LrmObUTIEzFUtSyBzHRq5ID8lUw==","signatures":[{"sig":"MEUCICswYAI2qR6Z69s0jn4sFq5IN7dBf7mEEHfzBp3tU4KKAiEArPtJN+jadaRzS2k2FifsApldx/l2DzjE6F0Fu3JcO4I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD/pnedtDRg/KgPyng8P/BC5sOatenjxx2rru5nOAL7awIgO+T4BfY3Tfq9KUvVtEuRNaGAqoQk0Ns2n4QFnFGF5/Q="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@molecule%2fapp-forms@1.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":76995},"main":"dist/index.js","name":"@molecule/app-forms","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"41bbb7d6c46b04d052a6b333e1b18e76ed007d23","license":"Apache-2.0","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"version":"1.0.2","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:39cc2d83-98f9-4365-bf5e-943704980c2a"}},"homepage":"https://www.molecule.dev/packages/app-forms","keywords":["molecule","forms","validation"],"repository":{"url":"git+https://github.com/molecule-dev/molecule.git","type":"git","directory":"packages/app/core/forms"},"_npmVersion":"12.0.2","description":"Form handling interface for molecule.dev","directories":{},"maintainers":[{"name":"vialoh","email":"npm@vialoh.me"}],"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"4.1.11","typescript":"6.0.3","@types/node":"26.1.2","@molecule/app-bond":"1.0.2"},"peerDependencies":{"@molecule/app-bond":"^1.0.1"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/app-forms_1.0.2_1789909286795_0.7207944708943113"}}},"time":{"created":"2026-08-03T22:28:58.271Z","modified":"2026-09-20T13:01:27.276Z","1.0.0":"2026-08-03T22:28:58.561Z","1.0.1":"2026-08-04T05:22:54.615Z","1.0.2":"2026-09-20T13:01:26.873Z"},"bugs":{"url":"https://github.com/molecule-dev/molecule/issues"},"license":"Apache-2.0","homepage":"https://www.molecule.dev/packages/app-forms","keywords":["molecule","forms","validation"],"repository":{"url":"git+https://github.com/molecule-dev/molecule.git","type":"git","directory":"packages/app/core/forms"},"description":"Form handling interface for molecule.dev","maintainers":[{"name":"vialoh","email":"npm@vialoh.me"}],"readme":"<!--\nAUTO-GENERATED — DO NOT EDIT THIS FILE.\nGenerated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.\nEdits here are overwritten on the next commit (molecule's pre-commit hook regenerates).\nTo change this document, edit the module-level JSDoc in src/index.ts.\nGenerated: 2026-08-04T01:50:52.975Z\n-->\n\n# @molecule/app-forms\n\n> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.\n> It is written to be read by coding agents as much as by people, and is generated from this\n> package's source — edit `src/index.ts` JSDoc, not this file.\n\nForm handling interface for molecule.dev.\n\nProvides a unified form management API that works across different\nform libraries (native, React Hook Form, Formik, etc.).\n\n## Quick Start\n\n```typescript\nimport { createForm } from '@molecule/app-forms'\n// (React apps: prefer the `useForm` hook from `@molecule/app-react` — same options.)\n\nconst form = createForm<{ email: string; password: string }>({\n  defaultValues: { email: '', password: '' },\n  mode: 'onBlur',\n})\n\nconst email = form.register({\n  name: 'email',\n  required: t('forms.required', undefined, { defaultValue: 'This field is required' }),\n  email: true,\n})\n// Wire email.value / email.onChange / email.onBlur to your input element.\n\nconst onSubmit = form.handleSubmit(async (values) => {\n  await http.post('/signup', values) // relative path via the app HTTP client\n})\n```\n\n## Type\n\n`core`\n\n## Installation\n\n```bash\nnpm install @molecule/app-forms @molecule/app-bond\n```\n\n## API\n\n### Interfaces\n\n#### `FieldRegistration`\n\nField registration result (for native inputs).\n\n```typescript\ninterface FieldRegistration {\n  /**\n   * Field name.\n   */\n  name: string\n\n  /**\n   * Field value.\n   */\n  value: unknown\n\n  /**\n   * Change handler.\n   */\n  onChange: (event: { target: { value: unknown; name: string } } | unknown) => void\n\n  /**\n   * Blur handler.\n   */\n  onBlur: () => void\n\n  /**\n   * Reference setter (for DOM elements).\n   */\n  ref?: (element: HTMLElement | null) => void\n}\n```\n\n#### `FieldState`\n\nReactive state for a single form field (value, validation errors, touched/dirty flags).\n\n```typescript\ninterface FieldState<T = unknown> {\n  /**\n   * Current field value.\n   */\n  value: T\n\n  /**\n   * Error message (if any).\n   */\n  error?: string\n\n  /**\n   * Whether the field has been touched.\n   */\n  touched: boolean\n\n  /**\n   * Whether the field is dirty (value changed from initial).\n   */\n  dirty: boolean\n\n  /**\n   * Whether the field is valid.\n   */\n  valid: boolean\n\n  /**\n   * Whether the field is currently being validated.\n   */\n  validating: boolean\n}\n```\n\n#### `FormController`\n\nForm controller interface.\n\nAll form providers must implement this interface.\n\n```typescript\ninterface FormController<T extends Record<string, unknown> = Record<string, unknown>> {\n  /**\n   * Gets the current form state.\n   */\n  getState(): FormState<T>\n\n  /**\n   * Gets the value of a specific field.\n   */\n  getValue(name: string): unknown\n  getValue<K extends keyof T>(name: K): T[K]\n\n  /**\n   * Gets all form values.\n   */\n  getValues(): T\n\n  /**\n   * Sets the value of a specific field.\n   */\n  setValue(\n    name: string,\n    value: unknown,\n    options?: { shouldValidate?: boolean; shouldDirty?: boolean; shouldTouch?: boolean },\n  ): void\n\n  /**\n   * Sets multiple values at once.\n   */\n  setValues(values: Partial<T>, options?: { shouldValidate?: boolean }): void\n\n  /**\n   * Gets the error for a specific field.\n   */\n  getError(name: string): string | undefined\n\n  /**\n   * Sets the error for a specific field.\n   */\n  setError(name: string, error: string | undefined): void\n\n  /**\n   * Clears the error for a specific field.\n   */\n  clearError<K extends keyof T>(name: K): void\n\n  /**\n   * Clears all errors.\n   */\n  clearErrors(): void\n\n  /**\n   * Gets the field state for a specific field.\n   */\n  getFieldState<K extends keyof T>(name: K): FieldState<T[K]>\n\n  /**\n   * Registers a field for form management.\n   */\n  register(nameOrOptions: string | RegisterOptions, options?: RegisterOptions): FieldRegistration\n\n  /**\n   * Unregisters a field.\n   */\n  unregister(name: string): void\n\n  /**\n   * Validates a specific field.\n   */\n  validateField<K extends keyof T>(name: K): Promise<boolean>\n\n  /**\n   * Validates all fields.\n   */\n  validate(): Promise<boolean>\n\n  /**\n   * Resets the form to initial values.\n   */\n  reset(values?: Partial<T>): void\n\n  /**\n   * Handles form submission.\n   */\n  handleSubmit(\n    onSubmit: (values: T) => void | Promise<void>,\n    onError?: (errors: Partial<Record<keyof T, string>>) => void,\n  ): (event?: { preventDefault?: () => void }) => Promise<void>\n\n  /**\n   * Sets focus to a field.\n   */\n  setFocus(name: keyof T): void\n\n  /**\n   * Subscribes to form state changes.\n   */\n  subscribe(callback: (state: FormState<T>) => void): () => void\n\n  /**\n   * Destroys the form controller.\n   */\n  destroy(): void\n}\n```\n\n#### `FormOptions`\n\nForm creation options.\n\n```typescript\ninterface FormOptions<T extends Record<string, unknown>> {\n  /**\n   * Default values.\n   */\n  defaultValues?: Partial<T>\n\n  /**\n   * Validation mode.\n   */\n  mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'all'\n\n  /**\n   * Revalidation mode.\n   */\n  reValidateMode?: 'onChange' | 'onBlur' | 'onSubmit'\n\n  /**\n   * Whether to focus the first error field on submit.\n   */\n  shouldFocusError?: boolean\n\n  /**\n   * Form-level validation function.\n   */\n  validate?: (\n    values: T,\n  ) => Partial<Record<keyof T, string>> | Promise<Partial<Record<keyof T, string>>>\n}\n```\n\n#### `FormProvider`\n\nForm provider interface.\n\nImplementations create form controllers.\n\n```typescript\ninterface FormProvider {\n  /**\n   * Creates a new form controller.\n   */\n  createForm<T extends Record<string, unknown>>(options: FormOptions<T>): FormController<T>\n}\n```\n\n#### `FormState`\n\nAggregate state of an entire form (all field values, errors, and submission status).\n\n```typescript\ninterface FormState<T extends Record<string, unknown> = Record<string, unknown>> {\n  /**\n   * Form values.\n   */\n  values: T\n\n  /**\n   * Field errors.\n   */\n  errors: Partial<Record<keyof T, string>>\n\n  /**\n   * Touched fields.\n   */\n  touched: Partial<Record<keyof T, boolean>>\n\n  /**\n   * Whether the form is valid.\n   */\n  isValid: boolean\n\n  /**\n   * Whether the form is dirty.\n   */\n  isDirty: boolean\n\n  /**\n   * Whether the form is submitting.\n   */\n  isSubmitting: boolean\n\n  /**\n   * Number of times the form has been submitted.\n   */\n  submitCount: number\n}\n```\n\n#### `RegisterOptions`\n\nField registration options.\n\n```typescript\ninterface RegisterOptions extends ValidationSchema {\n  /**\n   * Field name.\n   */\n  name: string\n\n  /**\n   * Default value.\n   */\n  defaultValue?: unknown\n\n  /**\n   * Value transformation on change.\n   */\n  transform?: (value: unknown) => unknown\n\n  /**\n   * Dependencies for validation.\n   */\n  deps?: string[]\n}\n```\n\n#### `ValidationRule`\n\nField validation rule.\n\n```typescript\ninterface ValidationRule {\n  /**\n   * Rule type.\n   */\n  type:\n    'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'email' | 'url' | 'custom'\n\n  /**\n   * Rule value (for rules like min, max, pattern).\n   */\n  value?: unknown\n\n  /**\n   * Error message when validation fails.\n   */\n  message: string\n}\n```\n\n#### `ValidationSchema`\n\nField validation schema.\n\n```typescript\ninterface ValidationSchema {\n  /**\n   * Whether the field is required.\n   */\n  required?: boolean | string\n\n  /**\n   * Minimum value (for numbers).\n   */\n  min?: number | { value: number; message: string }\n\n  /**\n   * Maximum value (for numbers).\n   */\n  max?: number | { value: number; message: string }\n\n  /**\n   * Minimum length (for strings).\n   */\n  minLength?: number | { value: number; message: string }\n\n  /**\n   * Maximum length (for strings).\n   */\n  maxLength?: number | { value: number; message: string }\n\n  /**\n   * Pattern to match (regex).\n   */\n  pattern?: RegExp | { value: RegExp; message: string }\n\n  /**\n   * Validate as email.\n   */\n  email?: boolean | string\n\n  /**\n   * Validate as URL.\n   */\n  url?: boolean | string\n\n  /**\n   * Custom validation function.\n   */\n  validate?: (value: unknown) => boolean | string | Promise<boolean | string>\n}\n```\n\n### Functions\n\n#### `createForm(options)`\n\nCreates a new form controller for the given options using the active\nform provider. The controller manages field values, validation, dirty\ntracking, and submission.\n\n```typescript\nfunction createForm(options: FormOptions<T>): FormController<T>\n```\n\n- `options` — Form configuration including initial values, validation rules, and submit handler.\n\n**Returns:** A form controller instance for managing the form lifecycle.\n\n#### `createNativeFormProvider()`\n\nCreates a native form provider that manages form state, validation,\nand field registration without any external library. This is the\nbuilt-in default used when no form library bond is configured.\n\n```typescript\nfunction createNativeFormProvider(): FormProvider\n```\n\n**Returns:** A `FormProvider` backed by vanilla JavaScript state management.\n\n#### `getProvider()`\n\nRetrieves the bonded form provider. If none is bonded, automatically\ncreates and bonds the built-in native form provider.\n\n```typescript\nfunction getProvider(): FormProvider\n```\n\n**Returns:** The active form provider.\n\n#### `hasProvider()`\n\nChecks whether a form provider has been explicitly bonded.\n\n```typescript\nfunction hasProvider(): boolean\n```\n\n**Returns:** `true` if a form provider is bonded.\n\n#### `setProvider(provider)`\n\nRegisters a form provider as the active singleton.\n\n```typescript\nfunction setProvider(provider: FormProvider): void\n```\n\n- `provider` — The form provider implementation to bond.\n\n#### `validateValue(value, schema, t)`\n\nValidates a value against a validation schema.\n\nWhen a translation function `t` is provided, default validation messages\nwill be passed through it for i18n support.\n\n```typescript\nfunction validateValue(\n  value: unknown,\n  schema: ValidationSchema,\n  t?: TranslateFn,\n): Promise<string | undefined>\n```\n\n- `value` — The value to validate (string, number, array, or any type accepted by custom validators).\n- `schema` — The validation rules to check against (required, min/max, pattern, email, etc.).\n- `t` — Optional i18n translation function for localizing error messages.\n\n**Returns:** The first validation error message, or `undefined` if the value passes all checks.\n\n### Constants\n\n#### `nativeProvider`\n\nPre-instantiated native form provider, ready to use without calling `createNativeFormProvider()`.\n\n```typescript\nconst nativeProvider: FormProvider\n```\n\n## Injection Notes\n\n### Requirements\n\nPeer dependencies:\n\n- `@molecule/app-bond` ^1.0.1\n\n### Runtime Dependencies\n\n- `@molecule/app-bond`\n\nBuild forms with {@link createForm} (or the framework hook), not a direct react-hook-form /\nformik import — that couples you to one library and breaks the swap.\n\n- **Client validation is UX, NOT a security boundary.** {@link validateValue} / client rules\n  give instant feedback, but the SERVER must re-validate every field it receives — a request\n  can skip the form entirely (curl, a tampered client). Never trust a value because the\n  client \"validated\" it, and never enforce authorization in the form.\n- Keep secrets out of any form state you persist (see `@molecule/app-storage`), and submit\n  through the HTTP client (`@molecule/app-http`) with a relative path — never a hardcoded URL.\n\n## E2E Tests\n\nIntegration checklist — drive the real UI (live preview, no mocks), adapt\neach item to this app's actual screens/flows, and check every box off one\nby one. A box you can't check is an integration bug to fix — not a skip:\n\n- [ ] Typing into each field updates its displayed value — interact_preview\n      into the field's data-mol-id, then read_preview_ui shows the new value in\n      the input (no stuck/blank input, no lag behind what you typed).\n- [ ] Submitting with a required field empty, a malformed email, or an\n      out-of-range number BLOCKS submit and shows that field's own error message\n      beside it — the handler does not run (no navigation, success state, or POST).\n- [ ] Fixing the offending field clears its error, and once every field is\n      valid the same submit succeeds — a valid submit passes the correct current\n      values (confirm the request/next screen carries what you typed, not stale\n      or blank data).\n- [ ] Errors appear at the configured time, not before: with mode onBlur or\n      onSubmit a pristine, untouched field shows NO error on first render — the\n      error only surfaces after you blur/touch it or attempt submit. No field\n      screams before the user has interacted.\n- [ ] Cross-field / form-level rules fire (e.g. a confirm-password mismatch\n      via the form-level validate) and block submit until satisfied, showing the\n      message on the right field.\n- [ ] If the form does async validation (e.g. a username-taken check), submit\n      waits for it to resolve before running the handler, any pending/validating\n      indicator shows while it is in flight, and an async failure blocks submit\n      with its message.\n- [ ] Resetting the form restores the initial default values in the inputs and\n      clears every error and touched state — a previously-shown error is gone and\n      the submit control returns to its initial enabled/disabled state.\n- [ ] Dirty/touched tracking is observable: an unchanged form reads as pristine\n      (no \"unsaved changes\" affordance; save disabled if the app gates on dirty),\n      editing a field flips it to dirty, and an invalid submit focuses the first\n      error field.\n\n## Translations\n\nTranslation strings are provided by `@molecule/app-locales-forms`.\n","readmeFilename":"README.md"}