{"id":17551568,"url":"https://github.com/mati365/under-control","last_synced_at":"2025-04-15T16:34:49.016Z","repository":{"id":64202760,"uuid":"574059255","full_name":"Mati365/under-control","owner":"Mati365","description":"📝 🐕 Are you losing sanity every time you need to make a form? Are you have enough of all antipatterns and cursed frameworks in React? Screw that! Treat all forms and inputs as a recursive composable control!","archived":false,"fork":false,"pushed_at":"2024-06-18T22:38:00.000Z","size":823,"stargazers_count":13,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-28T22:23:18.894Z","etag":null,"topics":["controls","design-system","form","forms","framework","inputs","react","two-way-databinding","typescript","validation"],"latest_commit_sha":null,"homepage":"","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/Mati365.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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-12-04T09:51:20.000Z","updated_at":"2024-05-13T09:01:47.000Z","dependencies_parsed_at":"2024-11-01T11:02:32.336Z","dependency_job_id":"ef88048f-776e-4403-9e25-699d52da7176","html_url":"https://github.com/Mati365/under-control","commit_stats":{"total_commits":92,"total_committers":3,"mean_commits":"30.666666666666668","dds":"0.021739130434782594","last_synced_commit":"7c7a81eca57b6e9ebe9aa3d4c248ac960e8b0cf0"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mati365%2Funder-control","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mati365%2Funder-control/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mati365%2Funder-control/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mati365%2Funder-control/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Mati365","download_url":"https://codeload.github.com/Mati365/under-control/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246339327,"owners_count":20761531,"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":["controls","design-system","form","forms","framework","inputs","react","two-way-databinding","typescript","validation"],"created_at":"2024-10-21T04:46:41.269Z","updated_at":"2025-03-30T15:32:02.229Z","avatar_url":"https://github.com/Mati365.png","language":"TypeScript","readme":"\u003cp align='center'\u003e\n  \u003cpicture\u003e\n    \u003csource media='(prefers-color-scheme: dark)' srcset='assets/social/under-control-banner.png'\u003e\n    \u003cimg src='assets/social/under-control-banner.png' alt='Banner'\u003e\n  \u003c/picture\u003e\n\n  \u003ch1 align='center'\u003eunder-control\u003c/h1\u003e\n\u003c/p\u003e\n\n\u003cdiv align='center'\u003e\n\n[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/00361e89d67049baa02723ee0e818ed0?style=for-the-badge)](https://www.codacy.com/gh/Mati365/under-control/dashboard?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=Mati365/under-control\u0026utm_campaign=Badge_Coverage)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/00361e89d67049baa02723ee0e818ed0)](https://www.codacy.com/gh/Mati365/under-control/dashboard?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=Mati365/under-control\u0026utm_campaign=Badge_Grade)\n[![NPM](https://img.shields.io/npm/l/@under-control/core?style=flat)](LICENSE)\n![NPM Downloads](https://img.shields.io/npm/dm/@under-control/core)\n![NPM version](https://img.shields.io/npm/v/@under-control/core)\n\n\u003c/div\u003e\n\n\u003cp align='center'\u003e\n  Are you losing sanity every time you need to make a form? Are you tired enough of all antipatterns and cursed React frameworks? Screw that! Treat all forms and inputs as a recursive composable controls! \u003cb\u003eunder-control\u003c/b\u003e is a lightweight alternative to libraries such as \u003cb\u003ereact-hook-form\u003c/b\u003e, \u003cb\u003eformik\u003c/b\u003e, \u003cb\u003ereact-ts-form\u003c/b\u003e, which, unlike them, allows you to turn your components into controllable controls.\n\u003c/p\u003e\n\n![Object type check example](assets/examples/type-check-object.png 'Type check object with array')\n\n## 📖 Docs\n\n- [📖 Docs](#-docs)\n- [🚀 Quick start](#-quick-start)\n  - [📦 Install](#-install)\n  - [✨ Features](#-features)\n- [🏗️ Composition](#️-composition)\n  - [🖊️ Basic Custom Control](#️-basic-custom-control)\n- [📝 Forms](#-forms)\n  - [⚠️ Forms without validation](#️-forms-without-validation)\n  - [✅ Forms with validation](#-forms-with-validation)\n    - [Single validator](#single-validator)\n    - [Multiple validators](#multiple-validators)\n- [✨ Binding controls](#-binding-controls)\n  - [Bind whole state to input](#bind-whole-state-to-input)\n  - [Bind specific path to input](#bind-specific-path-to-input)\n  - [Defining relations between inputs](#defining-relations-between-inputs)\n  - [Mapping bound value to input](#mapping-bound-value-to-input)\n- [License](#license)\n\n## 🚀 Quick start\n\n### 📦 Install\n\n[![Edit React Typescript (forked)](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-typescript-forked-jt16nb?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\n![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/@under-control/forms)\n\n```bash\nnpm install @under-control/forms\n```\n\n### ✨ Features\n\n- Allows you to turn any component into a control with `value` and `onChange` properties. Treat your custom select-box the same as it would be plain `\u003cselect /\u003e` tag! Other libs such as **react-hook-form** do not provide similar mechanism.\n- Better encapsulation of data. Due to low `context` usage it allows you to reuse built controllable controls in other forms.\n- Small size, it is around 4x smaller than **react-hook-form** and weights ~2.6kb (gzip).\n- Performance. Automatic caching of callbacks that binds controls. Modification of control A is not triggering rerender on control B.\n- Built in mind to be type-safe. Provides type-safe validation and controls binding.\n- Provides rerender-free control value side effects. Modify of control can reset value of form without doing additional `useEffect`.\n- Exports additional hooks such as `use-promise-callback` / `use-update-effect` that can be reused in your project.\n- Highly tested codebase with 100% coverage.\n\n## 🏗️ Composition\n\n### 🖊️ Basic Custom Control\n\nBuild and treat your forms as composable set of controlled controls. Do not mess with implementing `value` / `onChange` logic each time when you create standalone controls.\n\nExample:\n\n```tsx\nimport { controlled } from '@under-control/forms';\n\ntype PrefixValue = {\n  prefix: string;\n  name: string;\n};\n\nconst PrefixedInput = controlled\u003cPrefixValue\u003e(({ control: { bind } }) =\u003e (\n  \u003c\u003e\n    \u003cinput type=\"text\" {...bind.path('prefix')} /\u003e\n    \u003cinput type=\"text\" {...bind.path('name')} /\u003e\n  \u003c/\u003e\n));\n```\n\nUsage in bigger component:\n\n```tsx\nimport { controlled } from '@under-control/forms';\nimport { PrefixedInput } from './prefixed-input';\n\ntype PrefixPair = {\n  a: PrefixValue;\n  b: PrefixValue;\n};\n\nconst PrefixedInputGroup = controlled\u003cPrefixPair\u003e(({ control: { bind } }) =\u003e (\n  \u003c\u003e\n    \u003cPrefixedInput {...bind.path('a')} /\u003e\n    \u003cPrefixedInput {...bind.path('b')} /\u003e\n  \u003c/\u003e\n));\n```\n\n`onChange` output from `PrefixedInput` component:\n\n```tsx\n{\n  a: { prefix, name },\n  b: { prefix, name }\n}\n```\n\nThese newly created inputs can be later used in forms. Such like in this example:\n\n```tsx\nimport { useForm, error, flattenMessagesList } from '@under-control/forms';\n\nconst Form = () =\u003e {\n  const { bind, handleSubmitEvent, isDirty, validator } = useForm({\n    defaultValue: {\n      a: { prefix: '', name: '' },\n      b: { prefix: '', name: '' },\n    },\n    onSubmit: async data =\u003e {\n      console.info('Submit!', data);\n    },\n  });\n\n  return (\n    \u003cform onSubmit={handleSubmitEvent}\u003e\n      \u003cPrefixedInputGroup {...bind.path('a')} /\u003e\n      \u003cPrefixedInputGroup {...bind.path('b')} /\u003e\n      \u003cinput type=\"submit\" value=\"Submit\" disabled={!isDirty} /\u003e\n    \u003c/form\u003e\n  );\n};\n```\n\nYou can use created in such way controls also in uncontrolled mode. In that mode `defaultValue` is required.\n\n```tsx\n\u003cPrefixedInputGroup defaultValue={{ prefix: 'abc', name: 'def' }} /\u003e\n```\n\nCheck out example of custom controls with validation from other example:\n\n[![Edit advanced-validation](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/advanced-validation-jt16nb?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\n## 📝 Forms\n\n### ⚠️ Forms without validation\n\nThe simplest possible form, without added validation:\n\n```tsx\nimport { useForm } from '@under-control/forms';\n\nconst Form = () =\u003e {\n  const { bind, handleSubmitEvent, isDirty } = useForm({\n    defaultValue: {\n      a: '',\n      b: '',\n    },\n    onSubmit: async data =\u003e {\n      console.info('Submit!', data);\n    },\n  });\n\n  return (\n    \u003cform onSubmit={handleSubmitEvent}\u003e\n      \u003cinput type=\"text\" {...bind.path('a')} /\u003e\n      \u003cinput type=\"text\" {...bind.path('b')} /\u003e\n      \u003cinput type=\"submit\" value=\"Submit\" disabled={!isDirty} /\u003e\n    \u003c/form\u003e\n  );\n};\n```\n\n[![Edit not-validated-form](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/not-validated-form-5osyih?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\n### ✅ Forms with validation\n\nValidation by default can result sync or async result and can be run in these modes:\n\n1. `blur` - when user blurs any input. In this mode `bind.path` returns also `onBlur` handler. You have to assign it to input otherwise this mode will not work properly.\n2. `change` - when user changes any control (basically when `getValue()` changes)\n3. `submit` - when user submits form\n\nEach validator can result also single error or array of errors with optional paths to inputs.\n\n#### Single validator\n\nExample of form that performs validation on `blur` or `submit` event.\n\n```tsx\nimport { useForm, error, flattenMessagesList } from '@under-control/forms';\n\nconst Form = () =\u003e {\n  const { bind, handleSubmitEvent, isDirty, validator } = useForm({\n    defaultValue: {\n      a: '',\n      b: '',\n    },\n    validation: {\n      mode: ['blur', 'submit'],\n      validators: ({ global }) =\u003e\n        global(({ value: { a, b } }) =\u003e {\n          if (!a || !b) {\n            return error('Fill all required fields!');\n          }\n        }),\n    },\n    onSubmit: async data =\u003e {\n      console.info('Submit!', data);\n    },\n  });\n\n  return (\n    \u003cform onSubmit={handleSubmitEvent}\u003e\n      \u003cinput type=\"text\" {...bind.path('a')} /\u003e\n      \u003cinput type=\"text\" {...bind.path('b')} /\u003e\n      \u003cinput type=\"submit\" value=\"Submit\" disabled={!isDirty} /\u003e\n      \u003cdiv\u003e{flattenMessagesList(validator.errors.all).join(',')}\u003c/div\u003e\n    \u003c/form\u003e\n  );\n};\n```\n\n[![Edit validated-form](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/validated-form-3rb96u?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\n#### Multiple validators\n\nMultiple validators can be provided. In example above `global` validator validates all inputs at once. If you want to assign error to specific input you can:\n\n1. Return `error(\"Your error\", null \"path.to.control\")` function call in `all` validator.\n2. User `path` validator and return plain `error(\"Your error\")`.\n\nExample:\n\n```tsx\nconst Form = () =\u003e {\n  const {\n    bind,\n    handleSubmitEvent,\n    submitState,\n    validator: { errors },\n  } = useForm({\n    validation: {\n      mode: ['blur', 'submit'],\n      validators: ({ path, global }) =\u003e [\n        global(({ value: { a, b } }) =\u003e {\n          if (!a || !b) {\n            return error('Fill all required fields!');\n          }\n\n          if (b === 'World') {\n            return error('It cannot be a world!', null, 'b');\n          }\n        }),\n        path('a.c', ({ value }) =\u003e {\n          if (value === 'Hello') {\n            return error('It should not be hello!');\n          }\n        }),\n      ],\n    },\n    defaultValue: {\n      a: {\n        c: '',\n      },\n      b: '',\n    },\n    onSubmit: () =\u003e {\n      console.info('Submit!');\n    },\n  });\n\n  return (\n    \u003cform onSubmit={handleSubmitEvent}\u003e\n      \u003cFormInput {...bind.path('a.c')} {...errors.extract('a.c')} /\u003e\n      \u003cFormInput {...bind.path('b')} {...errors.extract('b')} /\u003e\n\n      \u003cinput type=\"submit\" value=\"Submit\" /\u003e\n\n      {submitState.loading \u0026\u0026 \u003cdiv\u003eSubmitting...\u003c/div\u003e}\n      \u003cdiv\u003e{flattenMessagesList(errors.global().errors)}\u003c/div\u003e\n    \u003c/form\u003e\n  );\n};\n```\n\n[![Edit advanced-validation](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/advanced-validation-jt16nb?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\n## ✨ Binding controls\n\n`useControl` is a core hook that is included into `useForm` and identical `bind` functions are exported there too. It allows you to bind values to input and it can be used alone without any form.\n\n### Bind whole state to input\n\nIn example below it's binding whole input text to string state with initial value `Hello world`.\n\n```tsx\nimport { useControl } from '@under-control/inputs';\n\nconst Component = () =\u003e {\n  const { bind } = useControl({\n    defaultValue: 'Hello world',\n  });\n\n  return \u003cinput type=\"text\" {...bind.entire()} /\u003e;\n};\n```\n\n### Bind specific path to input\n\nYou can also bind specific nested path by providing path:\n\n```tsx\nimport { useControl } from '@under-control/inputs';\n\nconst Component = () =\u003e {\n  const { bind } = useControl({\n    defaultValue: {\n      message: {\n        nested: ['Hello world'],\n      },\n    },\n  });\n\n  return \u003cinput type=\"text\" {...bind.path('message.nested[0]')} /\u003e;\n};\n```\n\n### Defining relations between inputs\n\nWhen user modifies `a` input then `b` input is also modified with `a` value + `!` character.\n\n```tsx\nimport { useForm } from '@under-control/forms';\n\nconst App = () =\u003e {\n  const { bind } = useControl({\n    defaultValue: {\n      a: '',\n      b: '',\n    },\n  });\n\n  return (\n    \u003cdiv\u003e\n      \u003cinput\n        type=\"text\"\n        {...bind.path('a', {\n          relatedInputs: ({ newControlValue, newGlobalValue }) =\u003e ({\n            ...newGlobalValue,\n            b: `${newControlValue}!`,\n          }),\n        })}\n      /\u003e\n      \u003cinput type=\"text\" {...bind.path('b')} /\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n[![Edit form-inputs-relations](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/form-inputs-relations-gmbvb8?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\n### Mapping bound value to input\n\nIt picks value from `message.nested[0]`, appends `!` character to it, and assigns as `value` to input:\n\n```tsx\nimport { useControl } from '@under-control/inputs';\n\nconst Component = () =\u003e {\n  const { bind } = useControl({\n    defaultValue: {\n      message: {\n        nested: ['Hello world'],\n      },\n    },\n  });\n\n  return (\n    \u003cinput\n      type=\"text\"\n      {...bind.path('message.nested[0]', {\n        input: str =\u003e `${str}!`, // appends `!` value stored in message.nested[0]\n      })}\n    /\u003e\n  );\n};\n```\n\n## License\n\n[MIT](LICENSE)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmati365%2Funder-control","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmati365%2Funder-control","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmati365%2Funder-control/lists"}