{"id":13627318,"url":"https://github.com/ngneat/reactive-forms","last_synced_at":"2025-10-03T20:07:37.313Z","repository":{"id":38085982,"uuid":"264690788","full_name":"ngneat/reactive-forms","owner":"ngneat","description":"(Angular Reactive) Forms with Benefits 😉","archived":false,"fork":false,"pushed_at":"2023-12-14T16:55:33.000Z","size":6147,"stargazers_count":612,"open_issues_count":17,"forks_count":57,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-05-07T18:15:36.378Z","etag":null,"topics":["angular","forms","reactive","reactive-forms","typed"],"latest_commit_sha":null,"homepage":"https://www.netbasal.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ngneat.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":"ngneat"}},"created_at":"2020-05-17T14:54:24.000Z","updated_at":"2025-04-25T03:52:06.000Z","dependencies_parsed_at":"2023-10-16T04:22:06.504Z","dependency_job_id":"1dfe0c36-a7ef-4bd0-a57f-42412b07b407","html_url":"https://github.com/ngneat/reactive-forms","commit_stats":{"total_commits":237,"total_committers":34,"mean_commits":6.970588235294118,"dds":0.6877637130801688,"last_synced_commit":"d60d618deb63bd6f9f0ed58766531d24bed9adaa"},"previous_names":[],"tags_count":39,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Freactive-forms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Freactive-forms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Freactive-forms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Freactive-forms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ngneat","download_url":"https://codeload.github.com/ngneat/reactive-forms/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253873900,"owners_count":21977187,"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":["angular","forms","reactive","reactive-forms","typed"],"created_at":"2024-08-01T22:00:32.774Z","updated_at":"2025-10-03T20:07:37.216Z","avatar_url":"https://github.com/ngneat.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n \u003cimg width=\"25%\" src=\"./logo.svg\"\u003e\n\u003c/p\u003e\n\n\u003cbr /\u003e\n\n![Test](https://github.com/ngneat/reactive-forms/workflows/Test/badge.svg?branch=master)\n[![MIT](https://img.shields.io/packagist/l/doctrine/orm.svg?style=flat-square)](LICENSE)\n[![commitizen](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square)]()\n[![PRs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)]()\n[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)\n[![ngneat](https://img.shields.io/badge/@-ngneat-383636?style=flat-square\u0026labelColor=8f68d4)](https://github.com/ngneat/)\n\n\u003e (Angular Reactive) Forms with Benefits 😉\n\nHow many times have you told yourself \"I wish Angular Reactive Forms would support types\", or \"I really want an API to query the form reactively. It missed some methods.\"\n\nYour wish is my command! \u003cbr\u003e\nThis library extends every Angular `AbstractControl`, and provides features that don't exist in the original one. It adds types, reactive queries, and helper methods. The most important thing is that you can start using it today! In most cases, the only thing that you need to change is the `import` path. So don't worry, no form refactoring required - we've got you covered;\n\nLet's take a look at all the neat things we provide:\n\n## 🔮 Features\n\n✅ Offers (almost) seamless `FormControl`, `FormGroup`, `FormArray` Replacement\u003cbr\u003e\n✅ Allows Typed Forms! \u003cbr\u003e\n✅ Provides Reactive Queries \u003cbr\u003e\n✅ Provides Helpful Methods \u003cbr\u003e\n✅ Typed and DRY `ControlValueAccessor` \u003cbr\u003e\n✅ Typed `FormBuilder` \u003cbr\u003e\n✅ Persist the form's state to local storage\n\n```\n👉 npm install @ngneat/reactive-forms\n```\n\n## Table of Contents\n\n- [Control Type](#control-type)\n- [Control Queries](#control-queries)\n- [Control Methods](#control-methods)\n- [Control Operators](#control-operators)\n- [ControlValueAccessor](#controlvalueaccessor)\n- [Form Builder](#form-builder)\n- [Persist Form](#persist-form)\n- [ESLint Rule](#eslint-rule)\n\n## Control Type\n\n`FormControl/FormArray` takes a generic that defines the `type` of the control. This `type` is then used to enhance every method exposed by Angular or this library. \n\nUse it with a `FormControl`:\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl\u003cstring\u003e();\n\n// Or auto infer it based on the initial value\nconst control = new FormControl('');\n\ncontrol.valueChanges.subscribe(value =\u003e {\n  // value is typed as string\n});\n```\n\nUse it with a `FormArray`:\n\n```ts\nimport { FormArray, FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormArray\u003cstring\u003e([new FormControl('')]);\n\ncontrol.value$.subscribe(value =\u003e {\n  // value is typed as string[]\n});\n```\n\nIf you use a `FormGroup`, it'll automatically infer the `type` based on the `controls` you supply:\n\n```ts\nimport { FormGroup, FormControl } from '@ngneat/reactive-forms';\n\nconst profileForm = new FormGroup({\n  firstName: new FormControl(''),\n  lastName: new FormControl(''),\n  address: new FormGroup({\n    street: new FormControl(''),\n    city: new FormControl('')\n  })\n});\n\n\nprofileForm.setValue(new Profile());\n\nprofileForm.patchValue({ firstName: 'Netanel' });\n```\n\nYou can use the **experimental** `ControlsOf` feature if you want to force a `FormGroup` to implement an external `type`:\n\n```ts\nimport { FormGroup, FormControl, ControlsOf } from '@ngneat/reactive-forms';\n\ninterface Profile {\n  firstName: string;\n  lastName: string;\n  address: {\n    street: string;\n    city: string;\n  };\n}\n\nconst profileForm = new FormGroup\u003cControlsOf\u003cProfile\u003e\u003e({\n  firstName: new FormControl(''),\n  lastName: new FormControl(''),\n  address: new FormGroup({\n    street: new FormControl(''),\n    city: new FormControl('')\n  })\n});\n```\n#### Gotchas\n\n- When using `array` types, it'll automatically infer it as `FormArray`. If you need a `FormControl`, you must set it within your interface explicitly:\n\n```ts\ninterface User {\n  name: string;\n  // 👇🏻\n  skills: FormControl\u003cstring[]\u003e;\n}\n```\n\n- Optional fields will only work with top-level values, and will not work with `FormGroup`:\n\n```ts\ninterface User {\n  name?: string;\n  foo?: string[]; \n  // 👇🏻 will not work \n  nested?: {\n    id: string;\n  };\n}\n```\n\n## Control Queries\n\n### `value$`\n\nObserves the control's value. Unlike the behavior of the built-in `valueChanges` observable, it emits the current `rawValue` **immediately** (which means you'll also get the values of `disabled` controls).\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.value$.subscribe(value =\u003e ...);\n```\n\n### `disabled$`\n\nObserves the control's `disable` status.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.disabled$.subscribe(isDisabled =\u003e ...);\n```\n\n### `enabled$`\n\nObserves the control's `enable` status.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.enabled$.subscribe(isEnabled =\u003e ...);\n```\n\n### `invalid$`\n\nObserves the control's `invalid` status.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.invalid$.subscribe(isInvalid =\u003e ...);\n```\n\n### `valid$`\n\nObserves the control's `valid` status.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.valid$.subscribe(isValid =\u003e ...);\n```\n\n### `status$`\n\nObserves the control's `status`.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.status$.subscribe(status =\u003e ...);\n```\n\nThe `status` is `typed` as `ControlState` (valid, invalid, pending or disabled).\n\n### `touch$`\n\nObserves the control's `touched` status.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.touch$.subscribe(isTouched =\u003e ...);\n```\n\nThis emits a value **only** when `markAsTouched`, or `markAsUnTouched`, has been called.\n\n### `dirty$`\n\nObserves the control's `dirty` status.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.dirty$.subscribe(isDirty =\u003e ...);\n```\n\nThis emits a value **only** when `markAsDirty`, or `markAsPristine`, has been called.\n\n### `errors$`\n\nObserves the control's `errors`.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.errors$.subscribe(errors =\u003e ...);\n```\n\n### `select()`\n\nSelects a `slice` of the form's state based on the given predicate.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup({\n  name: new FormControl('')\n});\n\ngroup.select(state =\u003e state.name).subscribe(name =\u003e ...)\n```\n\n## Control Methods\n\n### `setValue()`\n\nIn addition to the built-in method functionality, it can also take an `observable`.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup({\n  name: new FormControl('')\n});\n\ngroup.setValue(store.select('formValue'));\n```\n\n### `patchValue()`\n\nIn addition to the built-in method functionality, it can also take an `observable`.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup({\n  name: new FormControl('')\n});\n\ngroup.patchValue(store.select('formValue'));\n```\n\n### `disabledWhile()`\n\nTakes an observable that emits a boolean indicating whether to `disable` the control.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.disabledWhile(store.select('isDisabled'));\n```\n\n### `enabledWhile()`\n\nTakes an observable that emits a `boolean` indicating whether to `enable` the control.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.enabledWhile(store.select('isEnabled'));\n```\n\n### `markAllAsDirty()`\n\nMarks all the group's controls as `dirty`.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst control = new FormGroup();\ncontrol.markAllAsDirty();\n```\n\n### `hasErrorAndTouched()`\n\nA syntactic sugar method to be used in the template:\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('', Validators.required);\n```\n\n```html\n\u003cspan *ngIf=\"control.hasErrorAndTouched('required')\"\u003e\u003c/span\u003e\n```\n\n### `hasErrorAndDirty()`\n\nA syntactic sugar method to be used in the template:\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('', Validators.required);\n```\n\n```html\n\u003cspan *ngIf=\"control.hasErrorAndDirty('required')\"\u003e\u003c/span\u003e\n```\n\n### `setEnable()`\n\nSets whether the control is `enabled`.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.setEnable();\ncontrol.setEnable(false);\n```\n\n### `setDisable()`\n\nSets whether the control is `disabled`.\n\n```ts\nimport { FormControl } from '@ngneat/reactive-forms';\n\nconst control = new FormControl('');\ncontrol.setDisable();\ncontrol.setDisable(false);\n```\n\n### `get()`\n\nA method with `typed` parameters which obtains a reference to a specific control.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup({\n  name: new FormControl(''),\n  address: new FormGroup({\n    street: new FormControl(''),\n    city: new FormControl('')\n  })\n});\n\nconst name = group.get('name'); // FormControl\u003cstring\u003e\nconst city = group.get(['address', 'city']); // FormControl\u003cstring\u003e\n\n// Don't use it like this\ngroup.get('address.city') // AbstractControl\n```\n\n### `mergeErrors()`\n\nMerge validation errors. Unlike `setErrors()`, this will not overwrite errors already held by the control.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup(...);\ngroup.mergeErrors({ customError: true });\n```\n\n### `removeError()`\n\nRemove an error by key from the control.\n\n```ts\nimport { FormGroup } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup(...);\ngroup.removeError('customError');\n```\n\n### FormArray methods\n\n### remove()\n\nRemove a control from an array based on its value\n\n```ts\nimport { FormArray } from '@ngneat/reactive-forms';\n\nconst array = new FormArray\u003cstring\u003e(...);\n// Remove empty strings\narray.remove('')\n```\n\n### removeIf()\n\nRemove a control from an array based on a predicate\n\n```ts\nimport { FormArray } from '@ngneat/reactive-forms';\n\nconst array = new FormArray(...);\n// Only keep addresses in NYC\narray.removeIf((control) =\u003e control.get('address').get('city').value !== 'New York')\n```\n\n## Control Operators\n\nEach `valueChanges` or `values$` takes an operator `diff()`, which emits only changed parts of form:\n\n```ts\nimport { FormGroup, FormControl, diff } from '@ngneat/reactive-forms';\n\nconst control = new FormGroup({\n  name: new FormControl(''),\n  phone: new FormGroup({\n    num: new FormControl(''),\n    prefix: new FormControl('')\n  }),\n  skills: new FormArray\u003cstring\u003e([])\n});\n\ncontrol.value$\n  .pipe(diff())\n  .subscribe(value =\u003e {\n    // value is emitted only if it has been changed, and only the changed parts.\n  });\n```\n\n## ControlValueAccessor\n\nThe library exposes a `typed` version of `ControlValueAccessor`, which already implements `registerOnChange` and `registerOnTouched` under the hood:\n\n```ts\nimport { ControlValueAccessor } from '@ngneat/reactive-forms';\n\n@Component({\n  selector: 'my-checkbox',\n  host: { '(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()' },\n  providers: [\n    {\n      provide: NG_VALUE_ACCESSOR,\n      useExisting: MyCheckboxComponent,\n      multi: true\n    }\n  ]\n})\nexport class MyCheckboxComponent extends ControlValueAccessor\u003cboolean\u003e {\n  writeValue(value: boolean) {\n\n  }\n\n  // `this.onChange`, and `this.onTouched` are already here!\n}\n```\n\nNote that you can also use it as `interface`.\n\n## Form Builder\n\nWe also introduce a `typed` version of `FormBuilder` which returns a `typed` `FormGroup`, `FormControl` and `FormArray` with all our sweet additions:\n\n```ts\nimport { FormBuilder } from '@ngneat/reactive-forms';\n\nconstructor(\n  private fb: FormBuilder\n) {}\n\nconst group = this.fb.group({ name: 'ngneat', id: 1 });\n\ngroup.get('name') // FormControl\u003cstring\u003e\n```\n\nDue to the complexity of the builder API, we are currently couldn't create a \"good\" implementation of `ControlsOf` for the builder.\n\n## Persist Form\n\nAutomatically persist the `AbstractControl`'s value to the given storage:\n\n```ts\nimport { persistControl } from '@ngneat/reactive-forms';\n\nconst group = new FormGroup(...);\nconst unsubscribe = persistControl(group, 'profile').subscribe();\n```\n\nThe `persistControl` function will also set the `FromGroup` value to the latest state available in the storage before subscribing to value changes.\n\n##### `PersistOptions`\n\nChange the target storage or `debounceTime` value by providing options as a second argument in the `persist` function call.\n\n| Option              | Description                                           | Default               |\n|---------------------|-------------------------------------------------------|-----------------------|\n| `debounceTime`      | Update delay in ms between value changes              | 250                   |\n| `manager`           | A manager implementing the `PersistManager` interface | `LocalStorageManager` |\n| `arrControlFactory` | Factory functions for `FormArray`                     |                       |\n| `persistDisabledControls` | Defines whether values of disabled controls should be persisted | false |\n\n\nBy default the library provides `LocalStorageManager` and `SessionStorageManager`. It's possible to store the form value into a custom storage. Just implement the `PersistManager` interface, and use it when calling the `persistControl` function.\n\n```ts\n\nexport class StateStoreManager\u003cT\u003e implements PersistManager\u003cT\u003e {\n  setValue(key: string, data: T) {\n     ...\n  }\n\n  getValue(key: string) {\n    ...\n  }\n}\n\nexport class FormComponent implements OnInit {\n  group = new FormGroup();\n\n  ngOnInit() {\n    persist(this.group, 'profile', { manager: new StateStoreManager() }).subscribe();\n  }\n}\n```\n\n##### Using `FormArray` Controls.\n\nWhen working with a `FormArray`, it's required to pass a `factory` function that defines how to create the `controls` inside the `FormArray`.\n\n```ts\nconst group = new FormGroup({\n  skills: new FormArray\u003cstring\u003e([])\n});\n\npersist(group, 'profile', {\n  arrControlFactory: {\n     skills: value =\u003e new FormControl(value)\n  }\n});\n```\n\nBecause the form is strongly typed, you can only configure factories for properties that are of type `Array`. The library makes it also possible to correctly infer the type of `value` for the factory function.\n\n## ESLint Rule\n\nWe provide a special lint rule that forbids the imports of any token we expose, such as the following:\n`AbstractControl`,\n`AsyncValidatorFn`,\n`ControlValueAccessor`,\n`FormArray`,\n`FormBuilder`,\n`FormControl`,\n`FormGroup`,\n`ValidatorFn`,\nfrom `@angular/forms`.\n\nCheck out the [documentation](https://github.com/ngneat/reactive-forms/tree/master/libs/eslint-plugin-reactive-forms).\n","funding_links":["https://github.com/sponsors/ngneat"],"categories":["Table of contents","Projects by main language"],"sub_categories":["Third Party Components","angular"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngneat%2Freactive-forms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fngneat%2Freactive-forms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngneat%2Freactive-forms/lists"}