npm package: @luistabotelho/angular-signal-forms
Description: A simple library to manage forms using signals. Use the provided signalForm<T>() function to create a new SignalForm<T>.
Latest version: 1.1.0

# @luistabotelho/angular-signal-forms

A simple library to manage forms using signals. Use the provided signalForm<T>() function to create a new SignalForm<T>.

> [!NOTE]
> If you have suggestions on how to improve this documentation, leave your feedback [here](https://github.com/luistabotelho/angular-signal-forms/issues)

> [!NOTE]
> If you want to report a bug or suggest an improvement create an issue [here](https://github.com/luistabotelho/angular-signal-forms/issues)

# Changelogs

[GitHub Release Changelogs](https://github.com/luistabotelho/angular-signal-forms/releases)

# Topics

- [signalForm()](#signalform)
- [signalFormGroup()](#signalformgroup)
  - [data](#data)
  - [addItem()](#additem)
  - [removeItem()](#removeitem)
  - [valid()](#valid)
  - [errors()](#errors)
- [Classes](#classes)
  - [T](#t)
  - [K](#k)
  - [SignalForm\<T>](#signalformt)
  - [SignalFormDefinition\<T>](#signalformdefinitiont)
  - [SignalFormOptions](#signalformoptions)
  - [State](#state)
  - [ValidatorFunction\<T, K>](#validatorfunctiont-k)
- [Validators](#validators-2)
  - [Required()](#required)
  - [RegularExpression()](#regularexpression)
  - [Email()](#email)
  - [MaxLength()](#maxlength)
  - [MinLength()](#minlength)
  - [Max()](#max)
  - [Min()](#min)
- [Helper Functions](#helper-functions)
  - [resetSignalForm()](#resetsignalform)
  - [signalFormValue()](#signalformvalue)
  - [signalFormValid()](#signalformvalid)
  - [signalFormErrors()](#signalformerrors)
  - [signalFormSetTouched()](#signalformsettouched)
- [Example Component](#example-component)
  - [Typescript](#typescript)
  - [HTML](#html)

# `signalForm()`

The `signalForm<T>(initialValue, options?)` function is the basis of the library. It takes in a [SignalFormDefinition](#signalformdefinitiont) and an optional [SignalFormOptions](#signalformoptions).

This function returns a new instance of [SignalForm\<T>](#signalformt) where [T](#t) is the generic user data.

Examples:

```typescript
form1 = signalForm({
  field1: {
    initialValue: ""
  }
})

// Defining <T> manually adds validation to SignalFormDefinition, garanteing that the resulting form is a valid DataType
form2 = signalForm<DataType>({
  someField: {
    initialValue: 0
  }
})
```

# `signalFormGroup()`

The `signalFormGroup<T>(initialValue, options?)` function works in a similar way as the [signalForm()](#signalform) function, except it generates a [SignalFormGroup\<T>](#signalformgroupt), which is a wrapper around an array of [SignalForm\<T>](#signalformt), allowing to quickly add and remove a SignalForm from the group, as well as get validation, errors and values from it.

The function accepts the same inputs as [signalForm()](#signalform), and those are passed down to the individual members of the group.

Examples:

```typescript
formGroup = signalFormGroup<DataType>({
  field1: {
    initialValue: ""
  }
})
formGroup2 = signalFormGroup<DataType>({
  field1: {
    initialValue: ""
  }
}, {
  requireTouched: true,
  defaultState: "success"
})
```

## `.data`

A `WritableSignal<Array<SignalForm<T>>>`. This signal can be manipulated manually or with help of the other SignalFormGroup methods.

## `.addItem()`

Allows adding a new item to the group. This accepts an optional object of type [Partial\<T>](#t) as an input to pre-fill the form fields. If not passed the SignalForm will be created with the initialValue supplied to the group.

## `.removeItem()`

Allows removing an item from the group by passing it's index in the array.

## `.value()`

Returns a signal with the value as `Array<T>`. This is the same as applying [signalFormValue()](#signalformvalue) to all members of the group and joining the result in a single array.

## `.valid()`

Returns a signal of boolean representing the validity of all members of the group. This is the same as applying [signalFormValid()](#signalformvalid) on all members of the group.

## `.errors()`

Returns a signal with an `Array<string>` containing all the errors of the group members in format: "{fieldKey}: {error}". (If two members have the same error the error will appear dupplicated in the array).

# Classes

## `T`

`T` is the generic type that represents the data the form is handling. This is the DataType created by the user, it can be UserModel, CarModel, or anything else.

> [!WARNING]
> Currently signalForm doesn't support deeply nested structures. Adding support for that is under investigation and input is welcomed.

Example:

``` typescript
interface DataType {
  field1: string
  field1Child: string
  field2: string
  dateField: string
}
```

## `K`

`K` is defined as a keyof `T`, therefore it represents a property of `T`.

We will now refer to K as a <b>field(s)</b>.

## `SignalForm<T>`

The instance of `SignalForm` generated by [signalForm()](#signalformt). The `SignalForm` instance has the same fields as T, but gives each field some extra properties as seen below:

```typescript
[K in keyof T]: {
  initialValue: T[K],
  validators: Array<ValidatorFunction<T, T[K]>>,
  currentValue: WritableSignal<T[K]>,
  touched: WritableSignal<boolean>,
  state: Signal<State>,
  valid: Signal<boolean>
}
```

### initialValue

Is constant and represents the initialValue defined by the user.

### validators

Is the array of [ValidatorFunction](#validatorfunctiont-k) defined by the user.

### currentValue

A WriteableSignal representing the current value of the field. This is the property that should be bound to the input fields.

`[(ngModel)]="form.field1.currentValue"`

### touched

A WriteableSignal to represent weather the field was touched. Touched has to be manually handled and the recomendation is to bind it to the blur event of the input.

`(blur)="form.field1.touched.set(true)"`

### state

A Signal containing the current input [State](#state-1)

### valid

A Signal containing a boolean that represents if the field is valid or not based on the [validators](#validators)

## `SignalFormGroup<T>`



## `SignalFormDefinition<T>`

The initial definition accepted by the [signalForm()](#signalform) constructor. Each field has two properties: [initialValue](#initialvalue-1) and [validators](#validators-1).

```typescript
{
  field1: {
    initialValue: "",
    validators: [
      (val) => !val ? new Error("Required") : null,
      (val) => val && !RegExp(/^[A-Z]{1}/).test(val) ? new Error("First letter must be upper case") : null,
      (val) => val && val.length > 10 ? new Error("Must not exceed 10 characters") : null
    ]
  }
}
```

### initialValue

The initial value of the field.

### validators

An array of [ValidatorFunction](#validatorfunctiont-k). Keep in mind they are run in sequence, and therefore should be defined in order of priority. Ex.: "Required" will always appear before "First letter must be upper case".

## `SignalFormOptions`

A series of options that optionally can be passed to the [signalForm()](#signalform) function. Current options are:

```typescript
{
  requireTouched: true,
  defaultState: 'default',
  errorState: 'error'
}
```

### requireTouched

If `true` requires the input to be touched before displaying the error state.

Default: `true`

### defaultState

The default state of the input field if all validators pass.

Default: `default`

### errorState

The error state of the input field if any of the validators returns an Error.

Default: `error`

## `State`

The value of the `state` property of each field. The State has two properties:

``` typescript
{
    state: string,
    message: string | null
}
```

### state

The current state of the input field. Either [defaultState](#defaultstate) or [errorState](#errorstate).

## `ValidatorFunction<T, K>`

A function which takes in two parameters, [PropertyValue](#propertyvalue) and [FormValue](#formvalue), and returns an `Error` or `null`.

Example:

```typescript
(propertyValue, formValue) => !propertyValue && formValue.otherField.currentValue() != "Some Value" ? new Error("Required") : null
```

This function will return an error if this fields currentValue is Falsy and otherFields currentValue != "Some Value".

# Validators

Validators are predefined [ValidatorFunctions](#validatorfunctiont-k) made available from @luistabotelho/angular-signal-forms/validators to help simplify basic validation of signal-forms.

Please look at the provided example app on how to use.

## `Required()`

Makes the field required.

- Accepts an optional custom error message. Default: "Required"

Example: `Required("This is required")`.

## `RegularExpression()`

Validates the field agains a provided regular expression using .test()

- Accepts a regular expression to validate against.
- Accepts an optional custom error message. Default: "Does not match required pattern {pattern}"

Example: `RegularExpression(/^[A-Z]{1}/, "First digit must be upper case letter")`

## `Email()`

Validates the field to be a valid email.

- Accept an optional custom error message. Default: "Must be a valid email"

Example: `Email("Invalid email")`

## `MaxLength()`

Validates the input to have a maximum length of ? characters.

- Accepts the maximum length of the field as a number
- Accepts an optional custom error message. Default: "Must not exceed {length} characters."

Example: `MaxLength(10, "Too long!")`

## `MinLength()`

Validates the input to have a minimum length of ? characters.

- Accepts the minimum length of the field as a number
- Accepts an optional custom error message. Default: "Must be at least {length} characters long."

Example: `MinLength(10, "Too short!")`

## `Max()`

Validates the input to have a maximum value of ?.

- Accepts the maximum value of the field
- Accepts an optional custom error message. Default: "Must be less or equal to {maxValue}"

Example: `Max(10, "Too big!")`

## `Min()`

Validates the input to have a minimum value of ?.

- Accepts the minimum value of the field
- Accepts an optional custom error message. Default: "Must be greater or equal than {minValue}"

Example: `Min(10, "Too small!")`

# Helper Functions

## `resetSignalForm()`

Accepts an instance of [SignalForm](#signalform).

Sets the [currentValue](#currentvalue) of all fields to the [initialValue](#initialvalue) and sets [touched](#touched) to false, essentially returning the form to it's initial state.

## `signalFormValue()`

Accepts an instance of [SignalForm](#signalformt)

Returns a Signal containing the updated instance of [T](#t).

Example: { "field1": "Input 1 value", "field1Child": "", "field2": "Input 2 value", "dateField": "2024-11-27T21:54" }

## `signalFormValid()`

Accepts an instance of [SignalForm](#signalformt)

Returns a Signal containing a boolean representing if all fields in the form are valid.

## `signalFormErrors()`

Accepts an instance of [SignalForm](#signalformt)

Returns a Signal containing an array of all errors returned by the SignalForm instance. This is usefull if you want to display all errors to the user at once.

This does not take into consideration the touched property and will return all errors regardless.

## `signalFormSetTouched()`

Accepts an instance of [SignalForm](#signalformt)

Will set all fields in the form to touched, making their state go into error if they are invalid even if the user didn't touch them.

This can be used if you want todisplay all fields with errors when the user attempts to submit the form, even if the user didn't interact with the field.

Be aware that this is not required if the [requireTouched](#requiretouched) option was set to false.

# Example Component

## Typescript
``` typescript
import { Component, computed } from '@angular/core';
import { signalForm, signalFormValue, signalFormValid, resetSignalForm, signalFormSetTouched, signalFormGroup, signalFormErrors, signalFormGroupErrors, signalFormGroupValid, signalFormGroupValue } from '@luistabotelho/angular-signal-forms';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { Email, MaxLength, MinLength, RegularExpression, Required } from '@luistabotelho/angular-signal-forms/validators';

interface DataType {
  field1: string
  field1Child: string
  field2: string
  dateField: string
}

interface TableItem {
  id: string
  name: string
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule, CommonModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  form = signalForm<DataType>({
    field1: {
      initialValue: "",
      validators: [
        Required(),
        MinLength(2),
        RegularExpression(/^[A-Z]{1}/, "First digit must be upper case letter"),
        MaxLength(10)
      ]
    },
    field1Child: {
      initialValue: "",
      validators: [
        (val, form) => !val && form.field1.$currentValue() ? new Error("Required if Field 1 contains a value") : null,
        Email()
      ]
    },
    field2: {
      initialValue: ""
    },
    dateField: {
      initialValue: new Date().toISOString().slice(0, 16),
      validators: [
        Required("Date field is required!"),
        (val) => val.slice(0, 10) < new Date().toISOString().slice(0, 10) ? new Error("Date cannot be in the past") : null
      ]
    }
  })

  $formValue = signalFormValue(this.form)
  $formErrors = signalFormErrors(this.form)
  $formValid = signalFormValid(this.form)

  $tableData = signalFormGroup<TableItem>({
    id: {
      initialValue: '',
      validators: [
        val => !val ? new Error('Required') : null
      ]
    },
    name: {
      initialValue: '',
      validators: undefined
    }
  })

  $tableValid = signalFormGroupValid(this.$tableData)
  $tableErrors = signalFormGroupErrors(this.$tableData)
  $tableValue = signalFormGroupValue(this.$tableData)

  $completeValid = computed(() => this.$formValid() && this.$tableValid())
  $completeErrors = computed(() => [...this.$formErrors(), ...this.$tableErrors()])
  $completeValue = computed(() => ({
    ...this.$formValue(),
    table: [
      ...this.$tableValue()
    ]
  }))

  resetForm = () => {
    resetSignalForm(this.form)
    this.$tableData.$data.set([])
  }
  
  submit() {
    signalFormSetTouched(this.form)
    this.$tableData.$data().forEach(tableForm => {
      signalFormSetTouched(tableForm)
    })
    if (!this.$completeValid()) {
      return
    }
    // submit to server
  }
}

```

# HTML
``` html
<div>
    <label for="field1">Text Input 1</label>
    <br>
    <input 
    id="field1"
    type="text"
    (focus)="form.field1.$touched.set(true)"
    [(ngModel)]="form.field1.$currentValue">
    <br>
    Touched: {{form.field1.$touched()}}
    <br>
    State: {{form.field1.$state()}} : {{form.field1.$stateMessage()}}
</div>
<br><br>
<div>
    <label for="field1Child">Text Input 2 Depends on Text Input 1</label>
    <br>
    <input 
    id="field1Child"
    type="text"
    (focus)="form.field1Child.$touched.set(true)"
    [(ngModel)]="form.field1Child.$currentValue">
    <br>
    Touched: {{form.field1Child.$touched()}}
    <br>
    State: {{form.field1Child.$state()}} : {{form.field1Child.$stateMessage()}}
</div>
<br><br>
<div>
    <label for="field2">Text Input with no Validations</label>
    <br>
    <input 
    id="field2"
    type="text"
    (focus)="form.field2.$touched.set(true)"
    [(ngModel)]="form.field2.$currentValue">
    <br>
    Touched: {{form.field2.$touched()}}
    <br>
    State: {{form.field2.$state()}} : {{form.field2.$stateMessage()}}
</div>
<br><br>
<div>
    <label for="dateField">Date Input</label>
    <br>
    <input 
    id="dateField"
    type="datetime-local"
    (focus)="form.dateField.$touched.set(true)"
    [(ngModel)]="form.dateField.$currentValue"
    >
    <br>
    Touched: {{form.dateField.$touched()}}
    <br>
    State: {{form.dateField.$state()}} : {{form.dateField.$stateMessage()}}
</div>
<br><br>
<div>
    Form Valid: {{$formValid()}}
    <br>
    Current Value: {{$formValue() | json}}
    <br>
    All Errors: {{$formErrors() | json}}
</div>
<br><br>
<div>
    <table>
        <thead>
            <th>ID</th>
            <th>Name</th>
            <th></th>
        </thead>
        <tbody>
            @for (item of $tableData.$data(); track $index) {
                <tr>
                    <td>
                        <input type="text" [(ngModel)]="item.id.$currentValue">
                    </td>
                    <td>
                        <input type="text" [(ngModel)]="item.name.$currentValue">
                    </td>
                    <td>
                        <button (click)="$tableData.removeItem($index)">Delete</button>
                    </td>
                </tr>
            }
            <tr>
                <td colspan="3">
                    <button (click)="$tableData.addItem()">Add</button>
                </td>
            </tr>
        </tbody>
    </table>
</div>
<br><br>
<div>
    Table Valid: {{$tableValid()}}
    <br>
    Table Errors: {{$tableErrors() | json}}
    <br>
    Table Value: {{$tableValue() | json}}
</div>
<br><br>
<div>
    Complete Valid: {{$completeValid()}}
    <br>
    Complete Errors: {{$completeErrors() | json}}
    <br>
    Complete Value: {{$completeValue() | json}}
    <br><br>
    <button (click)="resetForm()">Reset Form</button>
    <br>
    <button (click)="submit()">Submit</button>
    <br>
    <button (click)="submit()" [disabled]="!$completeValid()">Submit If Valid</button>
</div>
```