An open API service indexing awesome lists of open source software.

https://github.com/Zarlex/ngxAccessor

Novel approach to connect Angular signals with forms. It is 100% compatible with ngModel | formControl and provides type safety in the template
https://github.com/Zarlex/ngxAccessor

angular forms signals

Last synced: 5 days ago
JSON representation

Novel approach to connect Angular signals with forms. It is 100% compatible with ngModel | formControl and provides type safety in the template

Awesome Lists containing this project

README

        

# NgxAccessor

This library provides a novel approach to interact with Angular forms and signals.
The current Angular version (Angular 19) provides Template Driven Forms and Reactive Forms. This library
adds a third way to interact with forms. It deeply integrates signals, but it is also open to
integrate other state management libraries.

## Motivation
This library aims to improve the developer experience how to interact with complex forms.
Using `FormGroup | FormArray | FormControl` with lots of nested objects can be quite overwhelming to set up.
Also, there is no type safety in the template.
So you might access an attribute that does not even exist and get a runtime error instead of a typescript error.
You 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)

## Goals
- Integrate seamlessly into the Angular ecosystem
- Easy access to nested object properties
- Two-way binding for nested object properties (nested objects and arrays)
- Listen on value updates and validation state changes on nested properties
- Strictly typed (also in the template)
- Easy validation error handling
- Open for extension (integrate any state management library)

## Play with it
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/stackblitz-starters-5k7jycgl?file=src%2Fmain.ts)

## How to install it
`npm install --save @zarlex/ngx-accessor`

## How it works
If you are working with an object signal `Signal<{...}>` it is not possible to update individual object properties. You can only update the whole object.
This 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.
The two-way binding functionality of the accessor ensures that updates of the signal are propagated through the accessor for every object property.
Each object property can be read and written through the accessor.
In opposite direction, property updates of the accessor are applied on the signal which then updates the whole signal object.
The accessor acts as a virtual tree and decorates the actual signal object with additional functionality like validation and update events.

## How to use it
It can be used as you would use `ngModel | ngFormControl`. Instead of using `ngModel | ngFormControl` use the directive `ngxAceessor`.
The directive expects an instance of a class that implements the [`Accessor` interface](./src/libs/ngx-accessor/src/lib/accessor/interface.ts).

In order to use it, the component needs to import the `NgxAccessorModule`.
Next you need to create the signal. Then you create the actual accessor and provide the signal.

In the template the `ngxAccessor` directive is attached to an input (or any component that implements the [`ControlValueAccessor`](https://angular.dev/api/forms/ControlValueAccessor) interface ).
Provide the accessor of the attribute you want to access by calling the method `access`:
``.

The `ngxAccessor` provides two-way binding to the provided accessor and the accessor reads and writes the value of the signal for the given attribute.
This 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.

This might sound more complicated than it is so let's have a look at the following examples.

### Access top level attribute
Accessing a top level attribute of the signal is as easy as calling `accessor.access(ATTRIBUTE NAME)`.
The `access` method ensures that the attribute name you want to access exists on the object type. If you are
accessing an attribute name that does not exist you get a typescript error.
Also, it enables auto-completion in your IDE. So you see all available attributes while typing.

```typescript
import { WritableSignal, signal, Component } from '@angular/core';
import { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';

interface User {
name: string;
}

@Component({
imports: [
NgxAccessorModule
],
template: `

// Typescript error becasue the attribute test does not exist on the user object
`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user);
}

```

### Access nested object
A nested object can be accessed by simply calling `access` again. It works recursively until you reach a leaf attribute.
The typing of the access `method` ensures that you can only call `access` again if the current accessed attribute is an object.
If you try to call `access` on a primitive attribute (for instance a string) you will get a typescript error.

You can also use the dot notation to access nested objects like `userAccessor.access('address.location.lat')`.
The typing of the access method also ensures that all attributes of the dot string exist otherwise you will get a typescript error.
```typescript
import { WritableSignal, signal, Component } from '@angular/core';
import { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';

interface User {
address: {
location: {
lat: number;
lng: number
}
}
}

@Component({
imports: [
NgxAccessorModule
],
template: `

// Typescript error becasue test does not exist


// Typescript error becasue test does not exist
`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user);
}

```

### Access array
If the accessed attribute is an array the accessor provides the method `getAccessors()` to get an `accessor` for each item of the array.
The returned `accessor` allows to access attributes of the array item by calling `access` again. Also, the `access` method ensures that you
can only access attributes that exist. If you provide an attribute name that does not exist you will get a typescript error.
The `getAccessors()` method is only available if the accessed attribute is an array. If it is not array it will return `never`.

```typescript
import { WritableSignal, signal, Component } from '@angular/core';
import { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';

interface User {
aliases: Array<{name: string}>;
}

@Component({
imports: [
NgxAccessorModule
],
template: `
@for(aliasAccessor of user.access('aliases').getAccessors(); track aliasAccessor.id){

- Remove Alias
}
+ Add Alias
`
})
class UserForm {
protected user: WritableSignal> = signal({});
protected userAccessor = new SignalAccessor(this.user);

protected addAlias(): void {
this.user.update((draft: Partial) => {
draft.aliases = draft.aliases || [];
draft.aliases.push({name: undefined});
return structuredClone(draft);
})
}

protected removeAlias(index: number): void {
this.user.update((draft: Partial) => {
draft.aliases.splice(index, 1);
return structuredClone(draft);
})
}
}

```

## Validation
The accessor keeps track of validation errors, async validation state and dirty state for each property.

### Reading validation, dirty and required state
The validation state for each property is accessible via the `validation` property of the accessor. It provides the signals
`isValid`, `isValidating`, and `errors`.
The validation state of nested properties is bubbling up all parents. So if a nested property has a validation error the
parent has a validation error as well. If a child `isValdiating` all its parents will be set to `isValidating` as well.

- `isValidating` is set to true for async property validators while they are executed
- `isValid` is set to true if all property validators are executed and the property has no validation errors
- `errors` reflect all errors that were reported by the validators

Additionally, the accessor also provides the signals `isDirty` and `isRequired`
- `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.
- `isRequired` is set to true if the `requiredValidator` is set on the property

```typescript
import { WritableSignal, signal, Component } from '@angular/core';
import { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';

interface User {
name: string;
}

@Component({
imports: [
NgxAccessorModule
],
template: `

Name



@if (!accessor.access('name').validation.isValidating() && !accessor.access('name').validation.isValid()) {


    @for (error of accessor.access('name').validation.errors(); track error.id) {
  • {{ error.message }}

  • }

}

`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user);
}
```

### Validator directives
Validators can be set on a property by adding a validator directive on the element.
The 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)
- required `html`
- email ``
- min ``
- max ``
- minLength ``
- maxLength ``
- pattern ``

```typescript
import { WritableSignal, signal, Component } from '@angular/core';
import { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';

interface User {
age: number;
}

@Component({
imports: [
NgxAccessorModule
],
template: `

`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user);
}
```

### Validation config on accessor
Validators 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.
Validators 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.
The property config allows configuring validators array. It also supports async validators.

```typescript
import { maxValidator, minValidator } from '@zarlex/ngx-accessor';
import { requiredValidator } from './required';

@Component({
imports: [
NgxAccessorModule
],
template: `

`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user, {
'age': {
validators: [
requiredValidator(),
minValidator(21),
maxValidator(99)
]
}
});
}
```

### Validation config on accessor for nested objects
The 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.

```typescript
import { requiredValidator } from '@zarlex/ngx-accessor';

interface User {
address: {
location: {lat: number, lng: number}
};
}

@Component({
imports: [
NgxAccessorModule
],
template: `


`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user, {
'address.location.lat': {
validators: [
requiredValidator()
]
},
'address.location.lng': {
validators: [
requiredValidator()
]
}
});
}
```

### Validation config on accessor for arrays
Validators can also be configured for object properties inside an array. So if the array consists of objects, and you want to add a validator
for 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.

```typescript
import { requiredValidator } from '@zarlex/ngx-accessor';

interface User {
aliases: Array<{name: string}>;
}

@Component({
imports: [
NgxAccessorModule
],
template: `
@for(aliasAccessor of user.access('aliases').getAccessors(); track aliasAccessor.id){

}
`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user, {
'aliases.name': {
validators: [
requiredValidator()
]
}
});
}
```

### Custom validators
Writing a custom validator is as simple as providing a validator config object. The config object can configure async and sync validators.
If `async` is set to true `isValid` expects a function that returns a promise or observable which must contain `true` (valid) or `false` invalid.
If 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.
If `async` is set to `false` the return value of `isValid` must be `true` or `false` and not a promise/observable.

```typescript
import { requiredValidator } from '@zarlex/ngx-accessor';

class AlreadyExistError extends ValidatorError {
constructor(name: string) {
super({
id: 'alreadyExists',
message: `${name} already exists`,
params: {},
});
}
}

interface User {
name: string;
}

@Component({
imports: [
NgxAccessorModule
],
template: `

`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user, {
'name': {
validators: [
requiredValidator(),
{
async: true,
isValid: (value: string) => timer(2000).pipe(map(() => value !== 'Test')),
error: (value: string) => new AlreadyExistError(value)
}
]
}
});
}
```

## Listen on updates
The accessor allows you to listen on value changes of each property as well as validation changes and dirty changes. This works
also for deep nested properties of objects and arrays.

### Value updates
To 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`.
`get` is signal so to access its value you would call `get()`.
Changes are also bubbling up to parents. So if a child is updated the child propagates a change but also the parent.
This is also the case for arrays. So if an array item is updated the array emits a change.

```typescript
import { WritableSignal, signal, Component } from '@angular/core';
import { SignalAccessor, NgxAccessorModule } from '@zarlex/ngx-accessor';

interface User {
name: string;
address: {location: {lat: number, lng: number}},
aliases: Array<{name: string}>
}

@Component({
imports: [
NgxAccessorModule
],
template: `


@for(aliasAccessor of user.access('aliases').getAccessors(); track aliasAccessor.id){

- Remove Alias
}
+ Add Alias
`
})
class UserForm {
protected user: WritableSignal = signal(undefined);
protected userAccessor = new SignalAccessor(this.user);

constructor() {
this.accessor.access('name').get$().subscribe(update => console.log('Name was updated', update));
effect(() => console.log('Name was updated', this.accessor.access('name').get()));

this.accessor.access('address').get$().subscribe(update => console.log('Address was updated', update));
effect(() => console.log('Address was updated', this.accessor.access('address').get()));
this.accessor.access('address.location.lng').get$().subscribe(update => console.log('Lng was updated', update));
effect(() => console.log('Lng was updated', this.accessor.access('address.location.lng').get()));

this.accessor.access('aliases').get$().subscribe(update => console.log('Aliases array was updated', update));
effect(() => console.log('Aliases array was updated', this.accessor.access('aliases').get()));
}
}

```

## Implement your own Accessor
If a different state library is used (for instance [Akita](https://opensource.salesforce.com/akita/)) a custom adapter can be provided which
has to implement the [`Accessor`](./src/libs/ngx-accessor/src/lib/accessor/interface.ts) interface.