{"id":19592296,"url":"https://github.com/smikhalevski/roqueform","last_synced_at":"2025-04-27T14:33:39.517Z","repository":{"id":38202533,"uuid":"449031926","full_name":"smikhalevski/roqueform","owner":"smikhalevski","description":"🧀 The form state management library that can handle hundreds of fields without breaking a sweat.","archived":false,"fork":false,"pushed_at":"2024-12-23T16:25:21.000Z","size":1910,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-19T14:12:16.660Z","etag":null,"topics":["field","form","plugin","validation"],"latest_commit_sha":null,"homepage":"https://smikhalevski.github.io/roqueform/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smikhalevski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-17T20:00:25.000Z","updated_at":"2024-12-23T16:22:42.000Z","dependencies_parsed_at":"2023-09-25T01:51:52.383Z","dependency_job_id":"f9c70058-d8b5-4f91-987b-99fedd13d9b8","html_url":"https://github.com/smikhalevski/roqueform","commit_stats":{"total_commits":92,"total_committers":2,"mean_commits":46.0,"dds":"0.010869565217391353","last_synced_commit":"47dbab07f34fbf5d4c3af196aeea8e730cbc3ff3"},"previous_names":[],"tags_count":46,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Froqueform","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Froqueform/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Froqueform/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Froqueform/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smikhalevski","download_url":"https://codeload.github.com/smikhalevski/roqueform/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251154703,"owners_count":21544545,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["field","form","plugin","validation"],"created_at":"2024-11-11T08:34:32.525Z","updated_at":"2025-04-27T14:33:38.602Z","avatar_url":"https://github.com/smikhalevski.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"#readme\"\u003e\n    \u003cimg src=\"./images/logo.png\" alt=\"Roqueform\" width=\"500\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nThe form state management library that can handle hundreds of fields without breaking a sweat.\n\n- Expressive and concise API with strict typings;\n- Controlled and uncontrolled inputs;\n- [Unparalleled extensibility with plugins;](#plugins-and-integrations)\n- Supports your favourite rendering and [validation libraries;](#validation-scaffolding-plugin)\n- [Just 2 kB gzipped.](https://bundlephobia.com/result?p=roqueform)\n\n🔥\u0026ensp;[**Try it on CodeSandbox**](https://codesandbox.io/s/2evfif)\n\n```sh\nnpm install --save-prod roqueform\n```\n\n- [Plugins and integrations](#plugins-and-integrations)\n- [Core features](#core-features)\n- [Events and subscriptions](#events-and-subscriptions)\n- [Transient updates](#transient-updates)\n- [Accessors](#accessors)\n- [Authoring a plugin](#authoring-a-plugin)\n- [Composing plugins](#composing-plugins)\n- [Errors plugin](#errors-plugin)\n- [Validation scaffolding plugin](#validation-scaffolding-plugin)\n- [Motivation](#motivation)\n\n# Plugins and integrations\n\n- [react](./packages/react#readme)\u003cbr/\u003e\n  Hooks and components to integrate with React.\n\n- [constraint-validation-plugin](./packages/constraint-validation-plugin#readme)\u003cbr/\u003e\n  Integrates fields with\n  [Constraint validation API](https://developer.mozilla.org/en-US/docs/Web/API/Constraint_validation).\n\n- [doubter-plugin](./packages/doubter-plugin#readme)\u003cbr/\u003e\n  Validates fields with [Doubter](https://github.com/smikhalevski/doubter#readme) shapes.\n\n- [ref-plugin](./packages/ref-plugin#readme)\u003cbr/\u003e\n  Associates field with DOM elements.\n\n- [reset-plugin](./packages/reset-plugin#readme)\u003cbr/\u003e\n  Manages field initial value and dirty status.\n\n- [scroll-to-error-plugin](./packages/scroll-to-error-plugin#readme)\u003cbr/\u003e\n  Enables scrolling to a field that has an associated validation error. Works with any validation plugin in this repo.\n\n- [uncontrolled-plugin](./packages/uncontrolled-plugin#readme)\u003cbr/\u003e\n  Updates fields by listening to change events of associated DOM elements.\n\n- [zod-plugin](./packages/zod-plugin#readme)\u003cbr/\u003e\n  Validates fields with [Zod](https://zod.dev/) schemas.\n\n- [annotations-plugin](./packages/annotations-plugin#readme)\u003cbr/\u003e\n  Enables custom annotations and metadata for fields.\n\n# Core features\n\nThe central piece of Roqueform is the concept of a field. A field holds a value and provides a means to update it.\n\nLet's start by creating a field:\n\n```ts\nimport { createField } from 'roqueform';\n\nconst field = createField();\n// ⮕ Field\u003cany\u003e\n```\n\nA value can be set to and retrieved from the field:\n\n```ts\nfield.setValue('Pluto');\n\nfield.value;\n// ⮕ 'Pluto'\n```\n\nProvide the initial value for a field:\n\n```ts\nconst ageField = createField(42);\n// ⮕ Field\u003cnumber\u003e\n\nageField.value;\n// ⮕ 42\n```\n\nThe field value type is inferred from the initial value, but you can explicitly specify the field value type:\n\n```ts\ninterface Planet {\n  name: string;\n}\n\ninterface Universe {\n  planets: Planet[];\n}\n\nconst universeField = createField\u003cUniverse\u003e();\n// ⮕ Field\u003cUniverse | undefined\u003e\n\nuniverseField.value;\n// ⮕ undefined\n```\n\nRetrieve a child field by its key:\n\n```ts\nconst planetsField = universeField.at('planets');\n// ⮕ Field\u003cPlanet[] | undefined\u003e\n```\n\n`planetsField` is a child field, and it is linked to its parent `universeField`.\n\n```ts\nplanetsField.key;\n// ⮕ 'planets'\n\nplanetsField.parent;\n// ⮕ universeField\n```\n\nFields returned by the [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#at)\nmethod have a stable identity. This means that you can invoke `at` with the same key multiple times and the same field\ninstance is returned:\n\n```ts\nuniverseField.at('planets');\n// ⮕ planetsField\n```\n\nSo most of the time you don't need to store a child field in a variable if you already have a reference to a parent\nfield.\n\nThe child field has all the same functionality as its parent, so you can access its children as well:\n\n```ts\nplanetsField.at(0).at('name');\n// ⮕ Field\u003cstring\u003e\n```\n\nWhen a value is set to a child field, a parent field value is also updated. If parent field doesn't have a value yet,\nRoqueform would infer its type from a key of the child field.\n\n```ts\nuniverseField.value;\n// ⮕ undefined\n\nuniverseField.at('planets').at(0).at('name').setValue('Mars');\n\nuniverseField.value;\n// ⮕ { planets: [{ name: 'Mars' }] }\n```\n\nBy default, for a key that is a numeric array index, a parent array is created, otherwise an object is created. You can\nchange this behaviour with [custom accessors](#accessors).\n\nWhen a value is set to a parent field, child fields are also updated:\n\n```ts\nconst nameField = universeField.at('planets').at(0).at('name');\n\nnameField.value;\n// ⮕ 'Mars'\n\nuniverseField.setValue({ planets: [{ name: 'Venus' }] });\n\nnameField.value;\n// ⮕ 'Venus'\n```\n\n# Events and subscriptions\n\nYou can subscribe events dispatched onto the field.\n\n```ts\nconst unsubscribe = planetsField.on('change:value', event =\u003e {\n  // Handle the field value change\n});\n// ⮕ () =\u003e void\n```\n\nThe [`Field.on`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#on) method\nassociates the event subscriber with an event type. All events that are dispatched onto fields have the share\n[`Event`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Event.html).\n\nWithout plugins, fields can dispatch events with\n[`change:value`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#on.on-2) type. This\nevent is dispatched when the field value is changed via\n[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue).\n\nPlugins may dispatch their own events. Here's an example of the\n[`change:errors`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.ErrorsPlugin.html#on.on-1) event\nintroduced by the [`errorsPlugin`](#errors-plugin).\n\n```ts\nimport { createField, errorsPlugin } from 'roqueform';\n\nconst field = createField({ name: 'Bill' }, errorsPlugin());\n\nfield.on('change:errors', event =\u003e {\n  // Handle error change\n});\n\nfield.addError('Illegal user');\n```\n\nThe root field and all child fields are updated before `change:value` subscribers are called, so it's safe to read field\nvalues in a subscriber. Fields use [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison to detect that the value has changed.\n\n```ts\nplanetsField.at(0).at('name').on('change:value', subscriber);\n\n// ✅ The subscriber is called\nplanetsField.at(0).at('name').setValue('Mercury');\n\n// 🚫 Value is unchanged, the subscriber isn't called\nplanetsField.at(0).setValue({ name: 'Mercury' });\n```\n\nSubscribe to all events dispatched onto the field using the glob event type:\n\n```ts\nplanetsField.on('*', event =\u003e {\n  // Handle all events\n});\n```\n\n# Transient updates\n\nWhen you call [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue)\non a field then subscribers of its ancestors and its updated child fields are triggered. To manually control the update\npropagation to fields ancestors, you can use transient updates.\n\nWhen a value of a child field is set transiently, values of its ancestors _aren't_ immediately updated.\n\n```ts\nconst avatarField = createField();\n\navatarField.at('eyeColor').setTransientValue('green');\n\navatarField.at('eyeColor').value;\n// ⮕ 'green'\n\n// 🟡 Parent value wasn't updated\navatarField.value;\n// ⮕ undefined\n```\n\nYou can check that a field is in a transient state:\n\n```ts\navatarField.at('eyeColor').isTransient;\n// ⮕ true\n```\n\nTo propagate the transient value contained by the child field to its parent, use the\n[`Field.propagate`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#propagate)\nmethod:\n\n```ts\navatarField.at('eyeColor').propagate();\n\navatarField.value;\n// ⮕ { eyeColor: 'green' }\n```\n\n[`Field.setTransientValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setTransientValue)\ncan be called multiple times, but only the most recent update is propagated to the parent field after the `propagate`\ncall.\n\nWhen a child field is in a transient state, its value visible from the parent may differ from the actual value:\n\n```ts\nconst planetsField = createField(['Mars', 'Pluto']);\n\nplanetsField.at(1).setTransientValue('Venus');\n\nplanetsField.value[1];\n// ⮕ 'Pluto'\n\n// 🟡 Transient value isn't visible from the parent\nplanetsField.at(1).value;\n// ⮕ 'Venus'\n```\n\nValues are synchronized after the update is propagated:\n\n```ts\nplanetsField.at(1).propagate();\n\nplanetsField.value[1];\n// ⮕ 'Venus'\n\nplanetsField.at(1).value;\n// ⮕ 'Venus'\n```\n\n# Accessors\n\n[`ValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.ValueAccessor.html) creates, reads and\nupdates field values.\n\n- When the child field is accessed via\n  [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#at) method for the\n  first time, its value is read from the value of the parent field using the\n  [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.\n\n- When a field value is updated via\n  [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue), then\n  the parent field value is updated with the value returned from the\n  [`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the\n  updated field has child fields, their values are updated with values returned from the\n  [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.\n\nYou can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses\n[`naturalValueAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalValueAccessor.html):\n\n```ts\nimport { createField, naturalValueAccessor } from 'roqueform';\n\nconst field = createField(['Mars', 'Venus'], naturalValueAccessor);\n```\n\n`naturalValueAccessor` supports:\n- plain objects,\n- class instances,\n- arrays,\n- `Map`-like,\n- `Set`-like instances.\n\nIf the field value object has `add` and `Symbol.iterator` methods, it is treated as a `Set` instance:\n\n```ts\nconst usersField = createField(new Set(['Bill', 'Rich']));\n\nusersField.at(0).value;\n// ⮕ 'Bill'\n\nusersField.at(1).value;\n// ⮕ 'Rich'\n```\n\nIf the field value object has `get` and `set` methods, it is treated as a `Map` instance:\n\n```ts\nconst planetsField = createField(new Map([\n  ['red', 'Mars'],\n  ['green', 'Earth']\n]));\n\nplanetsField.at('red').value;\n// ⮕ 'Mars'\n\nplanetsField.at('green').value;\n// ⮕ 'Earth'\n```\n\nWhen the field is updated, a parent field value is inferred from the key: for a key that is a numeric array index,\na parent array is created, otherwise an object is created.\n\n```ts\nconst carsField = createField();\n\ncarsField.at(0).at('brand').setValue('Ford');\n\ncarsField.value;\n// ⮕ [{ brand: 'Ford' }]\n```\n\n# Authoring a plugin\n\nPlugins are applied to a field using a\n[`PluginInjector`](https://smikhalevski.github.io/roqueform/types/roqueform.PluginInjector.html) callback. This callback\nreceives a mutable plugin instance and should enrich it with the plugin functionality. To illustrate how plugins work,\nlet's create a simple plugin that enriches a field with a DOM element reference.\n\n```ts\nimport { PluginInjector } from 'roqueform';\n\ninterface ElementPlugin {\n  element: Element | null;\n}\n\nconst injectElementPlugin: PluginInjector\u003cElementPlugin\u003e = field =\u003e {\n  // Update field with plugin functionality\n  field.element = null;\n};\n```\n\nTo apply the plugin to a field, pass it to the field factory:\n\n```ts\nconst planetField = createField(\n  { name: 'Mars' },\n  injectElementPlugin\n);\n// ⮕ Field\u003c{ name: string }, { element: Element | null }\u003e\n\nplanetField.element;\n// ⮕ null\n```\n\nThe plugin is applied to the `planetField` itself and each of its child fields when they are accessed for the first\ntime:\n\n```ts\nplanetField.at('name').element\n// ⮕ null\n```\n\nWe can now assign a DOM element reference to an `element` property, so we can later access an element through a field.\n\nPlugins may dispatch custom events. Let's update the plugin implementation to notify subscribers that the element has\nchanged.\n\n```ts\nimport { PluginInjector, dispatchEvents } from 'roqueform';\n\ninterface ElementPlugin {\n  element: Element | null;\n\n  setElement(element: Element | null): void;\n}\n\nconst injectElementPlugin: PluginInjector\u003cElementPlugin\u003e = field =\u003e {\n  field.element = null;\n\n  field.setElement = element =\u003e {\n    if (field.element !== element) {\n      field.element = element;\n\n      // Synchronously trigger associated subscribers\n      dispatchEvents([{\n        type: 'changed:element',\n        targetField: field,\n        originField: field,\n        data: null\n      }]);\n    }\n  };\n};\n```\n\nHere we used [`dispatchEvents`](https://smikhalevski.github.io/roqueform/functions/roqueform.dispatchEvents.html) helper\nthat invokes subscribers for provided events. So when `setElement` is called on a field, its subscribers would be\nnotified about element changes:\n\n```ts\nconst planetField = createField(\n  { name: 'Mars' },\n  injectElementPlugin\n);\n\nplanetField.at('name').on('changed:element', event =\u003e {\n  event.targetField.element;\n  // ⮕ document.body\n});\n\nplanetField.at('name').setElement(document.body);\n```\n\n# Composing plugins\n\nTo combine multiple plugins into a single function, use the\n[`composePlugins`](https://smikhalevski.github.io/roqueform/functions/roqueform.composePlugins.html) helper:\n\n```ts\nimport { createField, composePlugins } from 'roqueform';\n\ncreateField(['Mars'], composePlugins(plugin1, plugin2));\n// ⮕ Field\u003cstring[], …\u003e\n```\n\n# Errors plugin\n\nRoqueform is shipped with the plugin that allows to associate errors with fields\n[`errorsPlugin`](https://smikhalevski.github.io/roqueform/functions/roqueform.errorsPlugin.html).\n\n```ts\nimport { errorsPlugin } from 'roqueform';\n\nconst userField = createField({ name: '' }, errorsPlugin());\n\nuserField.at('name').addError('Too short');\n\nuserField.at('name').errors;\n// ⮕ ['Too short']\n```\n\nGet all invalid fields:\n\n```ts\nuserField.getInvalidFields();\n// ⮕ [userField.at('name')]\n```\n\n# Validation scaffolding plugin\n\nRoqueform is shipped with the validation scaffolding plugin\n[`validationPlugin`](https://smikhalevski.github.io/roqueform/functions/roqueform.validationPlugin.html), so you can\nbuild your validation on top of it.\n\n\u003e [!NOTE]\\\n\u003e This plugin provides a low-level functionality. Prefer\n\u003e [constraint-validation-plugin](./packages/constraint-validation-plugin), [doubter-plugin](./packages/doubter-plugin),\n\u003e or [zod-plugin](./packages/zod-plugin) or other high-level validation plugin.\n\n```ts\nimport { validationPlugin } from 'roqueform';\n\nconst plugin = validationPlugin({\n  validate(field) {\n    if (!field.at('name').value) {\n      field.at('name').isInvalid = true;\n    }\n  }\n});\n\nconst userField = createField({ name: '' }, plugin);\n\nuserField.validate();\n// ⮕ false\n\nuserField.at('name').isInvalid;\n// ⮕ true\n```\n\nThe plugin takes a [`Validator`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Validator.html) that has\n`validate` and `validateAsync` methods. Both methods receive a field that must be validated and should update the\n`isInvalid` property of the field or any of its children when needed.\n\nValidation plugin works best in conjunction with [the errors plugin](#errors-plugin). The latter would update\n`isInvalid` when an error is added or deleted:\n\n```ts\nimport { validationPlugin } from 'roqueform';\n\nconst plugin = validationPlugin(\n  // Make errors plugin available inside the validator\n  errorsPlugin(),\n  {\n    validate(field) {\n      if (!field.at('name').value) {\n        // Add an error to the invalid field\n        field.at('name').addError('Must not be blank')\n      }\n    }\n  }\n);\n\nconst userField = createField({ name: '' }, plugin);\n\nuserField.validate();\n// ⮕ false\n\nuserField.at('name').isInvalid;\n// ⮕ true\n\nuserField.at('name').errors;\n// ⮕ ['Must not be blank']\n```\n\n# Motivation\n\nRoqueform was built to satisfy the following requirements:\n\n- Since the form lifecycle consists of separate phases (input, validate, display errors, and submit), the form state\n  management library should allow to tap in (or at least not constrain the ability to do so) at any particular phase to \n  tweak the data flow.\n\n- Form data should be statically and strictly typed up to the very field value setter. So there must be a compilation\n  error if the string value from the silly input is assigned to the number-typed value in the form state object.\n\n- **Use the platform!** The form state management library must not constrain the use of the `form` submit behavior,\n  browser-based validation, and other related native features.\n\n- There should be no restrictions on how and when the form input is submitted because data submission is generally\n  an application-specific process.\n\n- There are many approaches to validation, and a great number of awesome validation libraries. The form library must\n  be agnostic to where (client-side, server-side, or both), how (on a field or on a form level), and when (sync, or\n  async) the validation is handled.\n\n- Validation errors aren't standardized, so an arbitrary error object shape must be allowed and related typings must be\n  seamlessly propagated to the error consumers/renderers.\n\n- The library API must be simple and easily extensible.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmikhalevski%2Froqueform","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmikhalevski%2Froqueform","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmikhalevski%2Froqueform/lists"}