npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@luistabotelho/angular-signal-forms

v1.1.0

Published

A simple library to manage forms using signals. Use the provided signalForm<T>() function to create a new SignalForm<T>.

Readme

@luistabotelho/angular-signal-forms

A simple library to manage forms using signals. Use the provided signalForm() function to create a new SignalForm.

[!NOTE] If you have suggestions on how to improve this documentation, leave your feedback here

[!NOTE] If you want to report a bug or suggest an improvement create an issue here

Changelogs

GitHub Release Changelogs

Topics

signalForm()

The signalForm<T>(initialValue, options?) function is the basis of the library. It takes in a SignalFormDefinition and an optional SignalFormOptions.

This function returns a new instance of SignalForm<T> where T is the generic user data.

Examples:

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() function, except it generates a SignalFormGroup<T>, which is a wrapper around an array of SignalForm<T>, 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(), and those are passed down to the individual members of the group.

Examples:

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> 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() 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() 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:

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 field(s).

SignalForm<T>

The instance of SignalForm generated by signalForm(). The SignalForm instance has the same fields as T, but gives each field some extra properties as seen below:

[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 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

valid

A Signal containing a boolean that represents if the field is valid or not based on the validators

SignalFormGroup<T>

SignalFormDefinition<T>

The initial definition accepted by the signalForm() constructor. Each field has two properties: initialValue and validators.

{
  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. 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() function. Current options are:

{
  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:

{
    state: string,
    message: string | null
}

state

The current state of the input field. Either defaultState or errorState.

ValidatorFunction<T, K>

A function which takes in two parameters, PropertyValue and FormValue, and returns an Error or null.

Example:

(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 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.

Sets the currentValue of all fields to the initialValue and sets touched to false, essentially returning the form to it's initial state.

signalFormValue()

Accepts an instance of SignalForm

Returns a Signal containing the updated instance of T.

Example: { "field1": "Input 1 value", "field1Child": "", "field2": "Input 2 value", "dateField": "2024-11-27T21:54" }

signalFormValid()

Accepts an instance of SignalForm

Returns a Signal containing a boolean representing if all fields in the form are valid.

signalFormErrors()

Accepts an instance of SignalForm

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

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 option was set to false.

Example Component

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

<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>