{"id":13902849,"url":"https://github.com/ngneat/forms-manager","last_synced_at":"2025-05-16T03:06:54.730Z","repository":{"id":36475604,"uuid":"225315298","full_name":"ngneat/forms-manager","owner":"ngneat","description":"🦄 The Foundation for Proper Form Management in Angular","archived":false,"fork":false,"pushed_at":"2023-06-02T06:38:49.000Z","size":8433,"stargazers_count":517,"open_issues_count":25,"forks_count":29,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-05-07T18:15:37.624Z","etag":null,"topics":["angular","forms","persistent-storage"],"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":"mit","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":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null},"funding":{"github":"ngneat"}},"created_at":"2019-12-02T07:46:08.000Z","updated_at":"2025-04-26T13:15:35.000Z","dependencies_parsed_at":"2023-01-17T02:00:46.682Z","dependency_job_id":null,"html_url":"https://github.com/ngneat/forms-manager","commit_stats":{"total_commits":63,"total_committers":16,"mean_commits":3.9375,"dds":0.5714285714285714,"last_synced_commit":"e1ad8082dd69472483b44b5789afee6abd8b1e61"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Fforms-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Fforms-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Fforms-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngneat%2Fforms-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ngneat","download_url":"https://codeload.github.com/ngneat/forms-manager/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254459088,"owners_count":22074605,"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","persistent-storage"],"created_at":"2024-08-06T22:01:27.791Z","updated_at":"2025-05-16T03:06:49.722Z","avatar_url":"https://github.com/ngneat.png","language":"TypeScript","readme":"\u003cbr /\u003e\n\n\u003cp align=\"center\"\u003e\n \u003cimg width=\"50%\" height=\"50%\" src=\"./logo.png\"\u003e\n\u003c/p\u003e\n\n\u003e The Foundation for Proper Form Management in Angular\n\n[![Build Status](https://img.shields.io/travis/datorama/akita.svg?style=flat-square)](https://travis-ci.org/ngneat/transloco)\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[![coc-badge](https://img.shields.io/badge/codeof-conduct-ff69b4.svg?style=flat-square)]()\n[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e5079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)\n[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)\n[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)\n\n## 🔮 Features\n\n✅ Allows Typed Forms!\u003cbr\u003e\n✅ Auto persists the form's state upon user navigation.\u003cbr\u003e\n✅ Provides an API to reactively querying any form, from anywhere. \u003cbr\u003e\n✅ Persist the form's state to local storage.\u003cbr\u003e\n✅ Built-in dirty functionality.\n\n\u003chr /\u003e\n\n`NgFormsManager` lets you sync Angular’s `FormGroup`, `FormControl`, and `FormArray`, via a unique store created for that purpose. The store will hold the controls' data like values, validity, pristine status, errors, etc.\n\nThis is powerful, as it gives you the following abilities:\n\n1. It will automatically save the current control value and update the form value according to the value in the store when the user navigates back to the form.\n2. It provides an API so you can query a form’s values and properties from anywhere. This can be useful for things like multi-step forms, cross-component validation and more.\n3. It can persist the form's state to local storage.\n\n\u003ca href=\"https://www.buymeacoffee.com/basalnetanel\" target=\"_blank\"\u003e\u003cimg src=\"https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png\" alt=\"Buy Me A Coffee\" style=\"height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;\" \u003e\u003c/a\u003e\n\nThe goal in creating this was to work with the existing Angular form ecosystem, and save you the trouble of learning a new API. Let’s see how it works:\n\nFirst, install the library:\n\n## Installation\n\n```\nnpm i @ngneat/forms-manager\n```\n\nThen, create a component with a form:\n\n```ts\nimport { NgFormsManager } from '@ngneat/forms-manager';\n\n@Component({\n  template: `\n    \u003cform [formGroup]=\"onboardingForm\"\u003e\n      \u003cinput formControlName=\"name\" /\u003e\n      \u003cinput formControlName=\"age\" /\u003e\n      \u003cinput formControlName=\"city\" /\u003e\n    \u003c/form\u003e\n  `,\n})\nexport class OnboardingComponent {\n  onboardingForm: FormGroup;\n\n  constructor(private formsManager: NgFormsManager, private builder: FormBuilder) {}\n\n  ngOnInit() {\n    this.onboardingForm = this.builder.group({\n      name: [null, Validators.required],\n      age: [null, Validators.required],\n      city: [null, Validators.required],\n    });\n\n    this.formsManager.upsert('onboarding', this.onboardingForm);\n  }\n\n  ngOnDestroy() {\n    this.formsManager.unsubscribe('onboarding');\n  }\n}\n```\n\nAs you can see, we’re still working with the existing API in order to create a form in Angular. We’re injecting the `NgFormsManager` and calling the `upsert` method, giving it the form name and an `AbstractForm`.\nFrom that point on, `NgFormsManager` will track the `form` value changes, and update the store accordingly.\n\nWith this setup, you’ll have an extensive API to query the store and update the form from anywhere in your application:\n\n## API\n\n- `valueChanges()` - Observe the control's value\n\n```ts\nconst value$ = formsManager.valueChanges('onboarding');\nconst nameValue$ = formsManager.valueChanges\u003cstring\u003e('onboarding', 'name');\n```\n\n- `validityChanges()` - Whether the control is valid\n\n```ts\nconst valid$ = formsManager.validityChanges('onboarding');\nconst nameValid$ = formsManager.validityChanges('onboarding', 'name');\n```\n\n- `dirtyChanges()` - Whether the control is dirty\n\n```ts\nconst dirty$ = formsManager.dirtyChanges('onboarding');\nconst nameDirty$ = formsManager.dirtyChanges('onboarding', 'name');\n```\n\n- `disableChanges()` - Whether the control is disabled\n\n```ts\nconst disabled$ = formsManager.disableChanges('onboarding');\nconst nameDisabled$ = formsManager.disableChanges('onboarding', 'name');\n```\n\n- `errorsChanges()` - Observe the control's errors\n\n```ts\nconst errors$ = formsManager.errorsChanges\u003cErrors\u003e('onboarding');\nconst nameErrors$ = formsManager.errorsChanges\u003cErrors\u003e('onboarding', 'name');\n```\n\n- `controlChanges()` - Observe the control's state\n\n```ts\nconst control$ = formsManager.controlChanges('onboarding');\nconst nameControl$ = formsManager.controlChanges\u003cstring\u003e('onboarding', 'name');\n```\n\n- `getControl()` - Get the control's state\n\n```ts\nconst control = formsManager.getControl('onboarding');\nconst nameControl = formsManager.getControl\u003cstring\u003e('onboarding', 'name');\n```\n\n`controlChanges` and `getControl` will return the following state:\n\n```ts\n{\n   value: any,\n   rawValue: object,\n   errors: object,\n   valid: boolean,\n   dirty: boolean,\n   invalid: boolean,\n   disabled: boolean,\n   touched: boolean,\n   pristine: boolean,\n   pending: boolean,\n   untouched: boolean,\n}\n```\n\n- `hasControl()` - Whether the control exists\n\n```ts\nconst hasControl = formsManager.hasControl('onboarding');\n```\n\n- `patchValue()` - A proxy to the original `patchValue` method\n\n```ts\nformsManager.patchValue('onboarding', value, options);\n```\n\n- `setValue()` - A proxy to the original `setValue` method\n\n```ts\nformsManager.setValue('onboarding', value, options);\n```\n\n- `reset()` - A proxy to the original `reset` method\n\n```ts\nformsManager.reset('onboarding', value, options);\n```\n\n- `markAllAsTouched()` - A proxy to the original `markAllAsTouched` method\n\n```ts\nformsManager.markAllAsTouched('onboarding', options);\n```\n\n- `markAsTouched()` - A proxy to the original `markAsTouched` method\n\n```ts\nformsManager.markAsTouched('onboarding', options);\n```\n\n- `markAllAsDirty()` - Marks the control and all its descendant controls as dirty\n\n```ts\nformsManager.markAllAsDirty('onboarding', options);\n```\n\n- `markAsDirty()` - A proxy to the original `markAsDirty` method\n\n```ts\nformsManager.markAsDirty('onboarding', options);\n```\n\n- `markAsPending()` - A proxy to the original `markAsPending` method\n\n```ts\nformsManager.markAsPending('onboarding', options);\n```\n\n- `markAsPristine()` - A proxy to the original `markAsPristine` method\n\n```ts\nformsManager.markAsPristine('onboarding', options);\n```\n\n- `markAsUntouched()` - A proxy to the original `markAsUntouched` method\n\n```ts\nformsManager.markAsUntouched('onboarding', options);\n```\n\n- `unsubscribe()` - Unsubscribe from the form's `valueChanges` observable (always call it on `ngOnDestroy`)\n\n```ts\nformsManager.unsubscribe('onboarding');\nformsManager.unsubscribe();\n```\n\n- `clear()` - Delete the form from the store\n\n```ts\nformsManager.clear('onboarding');\nformsManager.clear();\n```\n\n- `destroy()` - Destroy the form (Internally calls `clear` and `unsubscribe`)\n\n```ts\nformsManager.destroy('onboarding');\nformsManager.destroy();\n```\n\n- `controlDestroyed()` - Emits when the control is destroyed\n\n```ts\nformsManager.controlChanges('login').pipe(takeUntil(controlDestroyed('login')));\n```\n\n## Persist to browser storage (localStorage, sessionStorage or custom storage solution)\n\nIn the `upsert` method, pass the `persistState` flag:\n\n```ts\nformsManager.upsert(formName, abstractContorl, {\n  persistState: true,\n});\n```\n\nBy default, the state is persisted to `localStorage` ([Link](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)).\n\nFor storage to `sessionStorage` ([Link](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)), add `FORMS_MANAGER_SESSION_STORAGE_PROVIDER` to the providers array in `app.module.ts`:\n\n```ts\nimport { FORMS_MANAGER_SESSION_STORAGE_PROVIDER } from '@ngneat/forms-manager';\n\n@NgModule({\n  declarations: [AppComponent],\n  imports: [ ... ],\n  providers: [\n    ...\n    FORMS_MANAGER_SESSION_STORAGE_PROVIDER,\n    ...\n  ],\n  bootstrap: [AppComponent],\n})\nexport class AppModule {}\n```\n\nFurthermore, a **custom storage provider**, which must implement the `Storage` interface ([Link](https://developer.mozilla.org/en-US/docs/Web/API/Storage)) can be provided through the `FORMS_MANAGER_STORAGE` token:\n\n```ts\nimport { FORMS_MANAGER_STORAGE } from '@ngneat/forms-manager';\n\nclass MyStorage implements Storage {\n  public clear() { ... }\n  public key(index: number): string | null { ... }\n  public getItem(key: string): string | null { ... }\n  public removeItem(key: string) { ... }\n  public setItem(key: string, value: string) { ... }\n}\n\n@NgModule({\n  declarations: [AppComponent],\n  imports: [ ... ],\n  providers: [\n    ...\n    {\n      provide: FORMS_MANAGER_STORAGE,\n      useValue: MyStorage,\n    },\n    ...\n  ],\n  bootstrap: [AppComponent],\n})\nexport class AppModule {}\n```\n\n## Validators\n\nThe library exposes two helpers method for adding cross component validation:\n\n```ts\nexport function setValidators(\n  control: AbstractControl,\n  validator: ValidatorFn | ValidatorFn[] | null\n);\n\nexport function setAsyncValidators(\n  control: AbstractControl,\n  validator: AsyncValidatorFn | AsyncValidatorFn[] | null\n);\n```\n\nHere's an example of how we can use it:\n\n```ts\nexport class HomeComponent{\n  ngOnInit() {\n    this.form = new FormGroup({\n      price: new FormControl(null, Validators.min(10))\n    });\n\n    /*\n    * Observe the `minPrice` value in the `settings` form\n    * and update the price `control` validators\n    */\n    this.formsManager.valueChanges\u003cnumber\u003e('settings', 'minPrice')\n     .subscribe(minPrice =\u003e setValidators(this.form.get('price'), Validators.min(minPrice));\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`. For example:\n\n```ts\nimport { NgFormsManager } from '@ngneat/forms-manager';\n\nexport class HomeComponent {\n  skills: FormArray;\n  config: FormGroup;\n\n  constructor(private formsManager: NgFormsManager\u003cFormsState\u003e) {}\n\n  ngOnInit() {\n    this.skills = new FormArray([]);\n\n    /** Or inside a FormGroup */\n    this.config = new FormGroup({\n      skills: new FormArray([]),\n    });\n\n    this.formsManager\n      .upsert('skills', this.skills, { arrControlFactory: value =\u003e new FormControl(value) })\n      .upsert('config', this.config, {\n        arrControlFactory: { skills: value =\u003e new FormControl(value) },\n      });\n  }\n\n  ngOnDestroy() {\n    this.formsManager.unsubscribe();\n  }\n}\n```\n\n## NgFormsManager Generic Type\n\n`NgFormsManager` can take a generic type where you can define the forms shape. For example:\n\n```ts\ninterface AppForms {\n  onboarding: {\n    name: string;\n    age: number;\n    city: string;\n  };\n}\n```\n\nThis will make sure that the queries are typed, and you don't make any mistakes in the form name.\n\n```ts\nexport class OnboardingComponent {\n  constructor(private formsManager: NgFormsManager\u003cAppForms\u003e, private builder: FormBuilder) {}\n\n  ngOnInit() {\n    this.formsManager.valueChanges('onboarding').subscribe(value =\u003e {\n      // value now typed as AppForms['onboarding']\n    });\n  }\n}\n```\n\nNote that you can split the types across files using a definition file:\n\n```ts\n// login-form.d.ts\ninterface AppForms {\n  login: {\n    email: string;\n    password: string\n  }\n}\n\n// onboarding.d.ts\ninterface AppForms {\n  onboarding: {\n    ...\n  }\n}\n```\n\n## Using the Dirty Functionality\n\nThe library provides built-in support for the common \"Is the form dirty?\" question. Dirty means that the current control's\nvalue is different from the initial value. It can be useful when we need to toggle the visibility of a \"save\" button or displaying a dialog when the user leaves the page.\n\nTo start using it, you should set the `withInitialValue` option:\n\n```ts\n@Component({\n  template: `\n    \u003cbutton *ngIf=\"isDirty$ | async\"\u003eSave\u003c/button\u003e\n  `,\n})\nexport class SettingsComponent {\n  isDirty$ = this.formsManager.initialValueChanged(name);\n\n  constructor(private formsManager: NgFormsManager\u003cAppForms\u003e) {}\n\n  ngOnInit() {\n    this.formsManager.upsert(name, control, {\n      withInitialValue: true,\n    });\n  }\n}\n```\n\n### `setInitialValue(name, value)` - Set the initial form's value\n\n```ts\nformsManager.setInitialValue('form', initialValue);\n```\n\n### `getInitialValue(name)` - Get the initial value or `undefined` if not exist.\n\n```ts\nformsManager.getInitialValue('form');\n```\n\n## NgFormsManager Config\n\nYou can override the default config by passing the `NG_FORMS_MANAGER_CONFIG` provider:\n\n```ts\nimport { NG_FORMS_MANAGER_CONFIG, NgFormsManagerConfig } from '@ngneat/forms-manager';\n\n@NgModule({\n  declarations: [AppComponent],\n  imports: [ReactiveFormsModule],\n  providers: [\n    {\n      provide: NG_FORMS_MANAGER_CONFIG,\n      useValue: new NgFormsManagerConfig({\n        debounceTime: 1000, // defaults to 300\n        storage: {\n          key: 'NgFormManager',\n        },\n      }),\n    },\n  ],\n  bootstrap: [AppComponent],\n})\nexport class AppModule {}\n```\n\n## Contributors ✨\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://www.netbasal.com\"\u003e\u003cimg src=\"https://avatars1.githubusercontent.com/u/6745730?v=4?s=100\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eNetanel Basal\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=NetanelBasal\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=NetanelBasal\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#ideas-NetanelBasal\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/Coly010\"\u003e\u003cimg src=\"https://avatars2.githubusercontent.com/u/12140467?v=4?s=100\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eColum Ferry\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=Coly010\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=Coly010\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/mehmet-erim\"\u003e\u003cimg src=\"https://avatars0.githubusercontent.com/u/34455572?v=4?s=100\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eMehmet Erim\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=mehmet-erim\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/dspeirs7\"\u003e\u003cimg src=\"https://avatars2.githubusercontent.com/u/739058?v=4?s=100\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eDavid Speirs\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=dspeirs7\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=dspeirs7\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/manudss\"\u003e\u003cimg src=\"https://avatars3.githubusercontent.com/u/1046806?v=4?s=100\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eEmmanuel De Saint Steban\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=manudss\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=manudss\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/adrianriepl\"\u003e\u003cimg src=\"https://avatars2.githubusercontent.com/u/11076678?v=4?s=100\" width=\"100px;\" alt=\"\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAdrian Riepl\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=adrianriepl\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/ngneat/forms-manager/commits?author=adrianriepl\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n","funding_links":["https://github.com/sponsors/ngneat","https://www.buymeacoffee.com/basalnetanel"],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngneat%2Fforms-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fngneat%2Fforms-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngneat%2Fforms-manager/lists"}