@luistabotelho/angular-signal-forms v0.8.1
@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
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\ 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\, which is a wrapper around an array of SignalForm\, 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\ 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".
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 } from '@angular/core';
import { signalForm, signalFormValue, signalFormValid, resetSignalForm, signalFormSetTouched } from '../../projects/signal-forms/src/public-api';
import { FormsModule } from '@angular/forms';
import { signalFormErrors } from '../../projects/signal-forms/src/lib/helpers/signal-form-errors.helper';
import { CommonModule } from '@angular/common';
interface DataType {
field1: string
field1Child: string
field2: string
dateField: 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: [
(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
]
},
field1Child: {
initialValue: "",
validators: [
(val, form) => !val && form.field1.currentValue() ? new Error("Required if Field 1 contains a value") : null,
]
},
field2: {
initialValue: ""
},
dateField: {
initialValue: new Date().toISOString().slice(0, 16),
validators: [
(val) => !val ? new Error("Required") : null,
(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)
resetForm = () => resetSignalForm(this.form)
submit() {
signalFormSetTouched(this.form)
if (!this.$formValid()) {
return
}
// submit to server
}
}
HTML
<div>
<label for="field1">Text Input 1</label>
<br>
<input
id="field1"
type="text"
(blur)="form.field1.touched.set(true)"
[(ngModel)]="form.field1.currentValue">
<br>
Touched: {{form.field1.touched()}}
<br>
State: {{form.field1.state().state}} : {{form.field1.state().message}}
</div>
<br><br>
<div>
<label for="field1Child">Text Input 2 Depends on Text Input 1</label>
<br>
<input
id="field1Child"
type="text"
(blur)="form.field1Child.touched.set(true)"
[(ngModel)]="form.field1Child.currentValue">
<br>
Touched: {{form.field1Child.touched()}}
<br>
State: {{form.field1Child.state().state}} : {{form.field1Child.state().message}}
</div>
<br><br>
<div>
<label for="field2">Text Input with no Validations</label>
<br>
<input
id="field2"
type="text"
(blur)="form.field2.touched.set(true)"
[(ngModel)]="form.field2.currentValue">
<br>
Touched: {{form.field2.touched()}}
<br>
State: {{form.field2.state().state}} : {{form.field2.state().message}}
</div>
<br><br>
<div>
<label for="dateField">Date Input</label>
<br>
<input
id="dateField"
type="datetime-local"
(blur)="form.dateField.touched.set(true)"
[(ngModel)]="form.dateField.currentValue"
>
<br>
Touched: {{form.dateField.touched()}}
<br>
State: {{form.dateField.state().state}} : {{form.dateField.state().message}}
</div>
<br><br>
<div>
Form Valid: {{$formValid()}}
<br>
Current Value: {{$formValue() | json}}
<br>
All Errors: {{$formErrors() | json}}
<br><br>
<button (click)="resetForm()">Reset Form</button>
<br>
<button (click)="submit()">Submit</button>
<br>
<button (click)="submit()" [disabled]="!$formValid()">Submit If Valid</button>
</div>