{"id":28683710,"url":"https://github.com/Zarlex/ngxAccessor","last_synced_at":"2025-06-14T03:03:53.418Z","repository":{"id":298291681,"uuid":"525387317","full_name":"Zarlex/ngxAccessor","owner":"Zarlex","description":"Novel approach to connect Angular signals with forms. It is 100% compatible with ngModel | formControl and provides type safety in the template","archived":false,"fork":false,"pushed_at":"2025-06-05T15:49:47.000Z","size":1235,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-10T11:12:46.408Z","etag":null,"topics":["angular","forms","signals"],"latest_commit_sha":null,"homepage":"https://stackblitz.com/edit/stackblitz-starters-5k7jycgl?file=src%2Fmain.ts","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/Zarlex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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,"zenodo":null}},"created_at":"2022-08-16T13:16:55.000Z","updated_at":"2025-06-09T10:53:59.000Z","dependencies_parsed_at":"2025-06-10T11:12:52.540Z","dependency_job_id":"d7c2c2ce-daad-4818-b255-be6a4876874a","html_url":"https://github.com/Zarlex/ngxAccessor","commit_stats":null,"previous_names":["zarlex/ngxaccessor"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/Zarlex/ngxAccessor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zarlex%2FngxAccessor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zarlex%2FngxAccessor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zarlex%2FngxAccessor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zarlex%2FngxAccessor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Zarlex","download_url":"https://codeload.github.com/Zarlex/ngxAccessor/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zarlex%2FngxAccessor/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259752030,"owners_count":22905968,"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","signals"],"created_at":"2025-06-14T03:01:12.235Z","updated_at":"2025-06-14T03:03:53.410Z","avatar_url":"https://github.com/Zarlex.png","language":"TypeScript","funding_links":[],"categories":["Third Party Components"],"sub_categories":["Forms"],"readme":"# NgxAccessor\n\nThis library provides a novel approach to interact with Angular forms and signals.\nThe current Angular version (Angular 19) provides Template Driven Forms and Reactive Forms. This library \nadds a third way to interact with forms. It deeply integrates signals, but it is also open to\nintegrate other state management libraries.\n\n## Motivation\nThis library aims to improve the developer experience how to interact with complex forms.\nUsing `FormGroup | FormArray | FormControl` with lots of nested objects can be quite overwhelming to set up.\nAlso, there is no type safety in the template. \nSo you might access an attribute that does not even exist and get a runtime error instead of a typescript error.\nYou can read more about my motivation and the story of the library on this [blogpost](https://zarlex.medium.com/novel-approach-to-connect-angular-signals-with-forms-0156d18e96ca) \n\n## Goals\n- Integrate seamlessly into the Angular ecosystem\n- Easy access to nested object properties\n- Two-way binding for nested object properties (nested objects and arrays)\n- Listen on value updates and validation state changes on nested properties\n- Strictly typed (also in the template)\n- Easy validation error handling\n- Open for extension (integrate any state management library)\n\n## Play with it\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/stackblitz-starters-5k7jycgl?file=src%2Fmain.ts)\n\n## How to install it\n`npm install --save @zarlex/ngx-accessor`\n\n## How it works\nIf you are working with an object signal `Signal\u003c{...}\u003e` it is not possible to update individual object properties. You can only update the whole object.  \nThis library provides the class [`SignalAccessor`](./src/libs/ngx-accessor/src/lib/accessors/signal/signal-accessor.ts) to get access to every property of an object signal.\nThe two-way binding functionality of the accessor ensures that updates of the signal are propagated through the accessor for every object property. \nEach object property can be read and written through the accessor.\nIn opposite direction, property updates of the accessor are applied on the signal which then updates the whole signal object.\nThe accessor acts as a virtual tree and decorates the actual signal object with additional functionality like validation and update events.\n\n## How to use it\nIt can be used as you would use `ngModel | ngFormControl`. Instead of using `ngModel | ngFormControl` use the directive `ngxAceessor`.\nThe directive expects an instance of a class that implements the [`Accessor` interface](./src/libs/ngx-accessor/src/lib/accessor/interface.ts).\n\nIn order to use it, the component needs to import the `NgxAccessorModule`.\nNext you need to create the signal. Then you create the actual accessor and provide the signal.\n\nIn the template the `ngxAccessor` directive is attached to an input (or any component that implements the [`ControlValueAccessor`](https://angular.dev/api/forms/ControlValueAccessor) interface ).\nProvide the accessor of the attribute you want to access by calling the method `access`: \n`\u003cinput [ngxAccessor]=\"accessor.access(ATTRIBUTE NAME)/\u003e`.\n\nThe `ngxAccessor` provides two-way binding to the provided accessor and the accessor reads and writes the value of the signal for the given attribute.\nThis means, whenever the user changes the input value, the `ngxAccessor` will update the value of the accessor which then will update the value of the signal for the given attribute.\n\nThis might sound more complicated than it is so let's have a look at the following examples.\n\n### Access top level attribute\nAccessing a top level attribute of the signal is as easy as calling `accessor.access(ATTRIBUTE NAME)`. \nThe `access` method ensures that the attribute name you want to access exists on the object type. If you are\naccessing an attribute name that does not exist you get a typescript error.\nAlso, it enables auto-completion in your IDE. So you see all available attributes while typing.\n\n```typescript\nimport { WritableSignal, signal, Component } from '@angular/core';\nimport { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';\n\ninterface User {\n  name: string;\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('name')\"\u003e\n    \u003cinput [ngxAccessor]=\"userAccessor.access('test')\"\u003e // Typescript error becasue the attribute test does not exist on the user object\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user);\n}\n\n```\n\n### Access nested object\nA nested object can be accessed by simply calling `access` again. It works recursively until you reach a leaf attribute.\nThe typing of the access `method` ensures that you can only call `access` again if the current accessed attribute is an object.\nIf you try to call `access` on a primitive attribute (for instance a string) you will get a typescript error.\n\nYou can also use the dot notation to access nested objects like `userAccessor.access('address.location.lat')`. \nThe typing of the access method also ensures that all attributes of the dot string exist otherwise you will get a typescript error. \n```typescript\nimport { WritableSignal, signal, Component } from '@angular/core';\nimport { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';\n\ninterface User {\n  address: {\n    location: {\n      lat: number;\n      lng: number\n    }\n  }\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address').access('location').access('lat')\"\u003e\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address').access('location').access('lng').access('test')\"\u003e // Typescript error becasue test does not exist\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address.location.lat')\"\u003e\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address.location.lng')\"\u003e\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address.location.test')\"\u003e // Typescript error becasue test does not exist\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user);\n}\n\n```\n\n### Access array\nIf the accessed attribute is an array the accessor provides the method `getAccessors()` to get an `accessor` for each item of the array.\nThe returned `accessor` allows to access attributes of the array item by calling `access` again. Also, the `access` method ensures that you \ncan only access attributes that exist. If you provide an attribute name that does not exist you will get a typescript error.\nThe `getAccessors()` method is only available if the accessed attribute is an array. If it is not array it will return `never`.\n\n```typescript\nimport { WritableSignal, signal, Component } from '@angular/core';\nimport { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';\n\ninterface User {\n  aliases: Array\u003c{name: string}\u003e;\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    @for(aliasAccessor of user.access('aliases').getAccessors(); track aliasAccessor.id){\n      \u003cinput [ngxAccessor]=\"aliasAccessor.access('name')\"\u003e\n      \u003cbutton (click)=\"removeAlias($index)\"\u003e - Remove Alias\u003c/button\u003e\n    }\n    \u003cbutton (click)=\"addAlias()\"\u003e + Add Alias\u003c/button\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cPartial\u003cUser\u003e\u003e = signal({});\n  protected userAccessor = new SignalAccessor(this.user);\n\n  protected addAlias(): void {\n    this.user.update((draft: Partial\u003cTestProperties\u003e) =\u003e {\n      draft.aliases = draft.aliases || [];\n      draft.aliases.push({name: undefined});\n      return structuredClone(draft);\n    })\n  }\n\n  protected removeAlias(index: number): void {\n    this.user.update((draft: Partial\u003cTestProperties\u003e) =\u003e {\n      draft.aliases.splice(index, 1);\n      return structuredClone(draft);\n    })\n  }\n}\n\n```\n\n## Validation\nThe accessor keeps track of validation errors, async validation state and dirty state for each property.\n\n### Reading validation, dirty and required state\nThe validation state for each property is accessible via the `validation` property of the accessor. It provides the signals\n`isValid`, `isValidating`, and `errors`.\nThe validation state of nested properties is bubbling up all parents. So if a nested property has a validation error the\nparent has a validation error as well. If a child `isValdiating` all its parents will be set to `isValidating` as well.\n\n- `isValidating` is set to true for async property validators while they are executed \n- `isValid` is set to true if all property validators are executed and the property has no validation errors\n- `errors` reflect all errors that were reported by the validators\n\nAdditionally, the accessor also provides the signals `isDirty` and `isRequired` \n- `isDirty` is set to true once a user interacts with an input. `isDirty` also bubbles up so once a child property is set to dirty the parent is set to dirty as well.\n- `isRequired` is set to true if the `requiredValidator` is set on the property\n\n```typescript\nimport { WritableSignal, signal, Component } from '@angular/core';\nimport { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';\n\ninterface User {\n  name: string;\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003clabel [class.error]=\"!accessor.access('name').validation.isValid()\"\n       [class.validating]=\"accessor.access('name').validation.isValidating()\"\n       [class.required]=\"accessor.access('name').isRequired()\"\n       [class.dirty]=\"accessor.access('name').isDirty()\"\u003e\n      Name\n    \n      \u003cinput\n        type=\"text\"\n        [ngxAccessor]=\"accessor.access('name')\"\n        [required]=\"true\"\u003e\n        \n      @if (!accessor.access('name').validation.isValidating() \u0026\u0026 !accessor.access('name').validation.isValid()) {\n        \u003cul\u003e\n          @for (error of accessor.access('name').validation.errors(); track error.id) {\n            \u003cli\u003e{{ error.message }}\u003c/li\u003e\n          }\n        \u003c/ul\u003e\n      }\n    \u003c/label\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user);\n}\n```\n\n\n### Validator directives\nValidators can be set on a property by adding a validator directive on the element.\nThe following are provided, but you can also implement your own directive that provides [`NG_VALIDATORS`](https://angular.dev/api/forms/NG_VALIDATORS) or [`NG_ASYNC_VALIDATORS`](https://angular.dev/api/forms/NG_ASYNC_VALIDATORS)\n- required `html\u003cinput [ngxAccessor]=\"{...}\" [required]=\"true\"\u003e`\n- email `\u003cinput [ngxAccessor]=\"{...}\" [email]=\"true\"\u003e`\n- min `\u003cinput [ngxAccessor]=\"{...}\" [min]=\"1\"\u003e`\n- max `\u003cinput [ngxAccessor]=\"{...}\" [max]=\"100\"\u003e`\n- minLength `\u003cinput [ngxAccessor]=\"{...}\" [minLength]=\"2\"\u003e`\n- maxLength `\u003cinput [ngxAccessor]=\"{...}\" [maxLength]=\"255\"\u003e`\n- pattern `\u003cinput [ngxAccessor]=\"{...}\" pattern=\"^[a-zA-Z]*$\"\u003e`\n\n```typescript\nimport { WritableSignal, signal, Component } from '@angular/core';\nimport { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';\n\ninterface User {\n  age: number;\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('age')\" [required]=\"true\" min=\"21\" max=\"99\"\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user);\n}\n```\n\n### Validation config on accessor\nValidators can also be configured on the accessor constructor. If the validators are provided through the constructor the validator directives on the element will be ignored.\nValidators can be configured through the config argument of the accessor constructor. It accepts an object where you can configure each property through a property config.\nThe property config allows configuring validators array. It also supports async validators.\n\n\n```typescript\nimport { maxValidator, minValidator } from '@zarlex/ngx-accessor';\nimport { requiredValidator } from './required';\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('age')\"\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user, {\n    'age': {\n      validators: [\n        requiredValidator(),\n        minValidator(21),\n        maxValidator(99)\n      ]\n    }\n  });\n}\n```\n\n### Validation config on accessor for nested objects\nThe config argument also allows configuring deep nested properties by using the dot notation. Each key is validated through typescript to make sure the key exists as property.\n\n```typescript\nimport { requiredValidator } from '@zarlex/ngx-accessor';\n\ninterface User {\n  address: {\n    location: {lat: number, lng: number}\n  };\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address.location.lat')\"\u003e\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address.location.lng')\"\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user, {\n    'address.location.lat': {\n      validators: [\n        requiredValidator()\n      ]\n    },\n    'address.location.lng': {\n      validators: [\n        requiredValidator()\n      ]\n    }\n  });\n}\n```\n\n### Validation config on accessor for arrays\nValidators can also be configured for object properties inside an array. So if the array consists of objects, and you want to add a validator \nfor a property of the object you can also use the dot notation. The array index is omitted as the validator is applicable for each array item.\n\n```typescript\nimport { requiredValidator } from '@zarlex/ngx-accessor';\n\ninterface User {\n  aliases: Array\u003c{name: string}\u003e;\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    @for(aliasAccessor of user.access('aliases').getAccessors(); track aliasAccessor.id){\n      \u003cinput [ngxAccessor]=\"aliasAccessor.access('name')\"\u003e\n    }\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user, {\n    'aliases.name': {\n      validators: [\n        requiredValidator()\n      ]\n    }\n  });\n}\n```\n\n### Custom validators\nWriting a custom validator is as simple as providing a validator config object. The config object can configure async and sync validators.\nIf `async` is set to true `isValid` expects a function that returns a promise or observable which must contain `true` (valid)  or `false` invalid.\nIf the return value of `isValid` is `false` the function `error` is called which has to return a validator error. The returned error is then set on the accessor validation instance.\nIf `async` is set to `false` the return value of `isValid` must be `true` or `false` and not a promise/observable.\n\n```typescript\nimport { requiredValidator } from '@zarlex/ngx-accessor';\n\nclass AlreadyExistError extends ValidatorError {\n  constructor(name: string) {\n    super({\n      id: 'alreadyExists',\n      message: `${name} already exists`,\n      params: {},\n    });\n  }\n}\n\ninterface User {\n  name: string;\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('name')\"\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user, {\n    'name': {\n      validators: [\n        requiredValidator(),\n        {\n          async: true,\n          isValid: (value: string) =\u003e timer(2000).pipe(map(() =\u003e value !== 'Test')),\n          error: (value: string) =\u003e new AlreadyExistError(value)\n        }\n      ]\n    }\n  });\n}\n```\n\n## Listen on updates\nThe accessor allows you to listen on value changes of each property as well as validation changes and dirty changes. This works\nalso for deep nested properties of objects and arrays.\n\n### Value updates\nTo listen on value updates you can either use the observable that is exposed via `get$` or you can use the actual virtual signal through `get`. \n`get` is signal so to access its value you would call `get()`.\nChanges are also bubbling up to parents. So if a child is updated the child propagates a change but also the parent.\nThis is also the case for arrays. So if an array item is updated the array emits a change.\n\n```typescript\nimport { WritableSignal, signal, Component } from '@angular/core';\nimport { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';\n\ninterface User {\n  name: string;\n  address: {location: {lat: number, lng: number}},\n  aliases: Array\u003c{name: string}\u003e\n}\n\n@Component({\n  imports: [\n    NgxAccessorModule\n  ],\n  template: `\n    \u003cinput [ngxAccessor]=\"userAccessor.access('name')\"\u003e\n    \u003cinput [ngxAccessor]=\"userAccessor.access('address.location.lng')\"\u003e\n    @for(aliasAccessor of user.access('aliases').getAccessors(); track aliasAccessor.id){\n      \u003cinput [ngxAccessor]=\"aliasAccessor.access('name')\"\u003e\n      \u003cbutton (click)=\"removeAlias($index)\"\u003e - Remove Alias\u003c/button\u003e\n    }\n    \u003cbutton (click)=\"addAlias()\"\u003e + Add Alias\u003c/button\u003e\n  `\n})\nclass UserForm {\n  protected user: WritableSignal\u003cUser\u003e = signal(undefined);\n  protected userAccessor = new SignalAccessor(this.user);\n\n  constructor() {\n    this.accessor.access('name').get$().subscribe(update =\u003e console.log('Name was updated', update));\n    effect(() =\u003e console.log('Name was updated', this.accessor.access('name').get()));\n\n    this.accessor.access('address').get$().subscribe(update =\u003e console.log('Address was updated', update));\n    effect(() =\u003e console.log('Address was updated', this.accessor.access('address').get()));\n    this.accessor.access('address.location.lng').get$().subscribe(update =\u003e console.log('Lng was updated', update));\n    effect(() =\u003e console.log('Lng was updated', this.accessor.access('address.location.lng').get()));\n\n    this.accessor.access('aliases').get$().subscribe(update =\u003e console.log('Aliases array was updated', update));\n    effect(() =\u003e console.log('Aliases array was updated', this.accessor.access('aliases').get()));\n  }\n}\n\n```\n\n## Implement your own Accessor\nIf a different state library is used (for instance [Akita](https://opensource.salesforce.com/akita/)) a custom adapter can be provided which\nhas to implement the [`Accessor`](./src/libs/ngx-accessor/src/lib/accessor/interface.ts) interface.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FZarlex%2FngxAccessor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FZarlex%2FngxAccessor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FZarlex%2FngxAccessor/lists"}