@rytrox/form-signals v0.4.4
Angular Form-Signals
Simple Reimplementation of Angular Forms in Signals. Focused on simplicity, with full Angular Material support.
Table of Contents
Installation
Install the library by using npm, pnpm or yarn:
npm install @rytrox/form-signalspnpm install @rytrox/form-signalsyarn add @rytrox/form-signalsCompatibility List
| Angular Version | Library Version |
|---|---|
| ^19.2.0 | ^0.2.1 |
Usage
FormControl
The popular FormControl in Angular is now a WritableSignal. It does not allow null if not explicitly specified in you declaration.
You can create a FormControl by using the following function
import {formControl} from '@rytrox/form-signals';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
// Creates a FormControl that allows null
protected readonly stringControl = formControl<string | null>('Hello');
// Creates a FormControl that only allows string, but with validators
protected readonly controlWithValidators = formControl({
value: 'World',
disabled: false, // optional, default value is false
validators: [
(val) => val.length > 0 ? {required: 'This field is required!'} : null,
]
});
}ValidatorFn is now type-safe. You can validate against the current value of the form.
Binding your FormControl in Templates
By default, we provide Directives for any Material- and HTML-Component that is interactable.
Those Directives are strictly typed. You can bind FormControl by using the [form]-Directive
<input type="text" [form]="form">
<!-- or. -->
<select [form]="form">
...
</select>This table shows supported types on certain Components:
| Directive | Component | Supported FormControls |
|---|---|---|
| InputCheckboxDirective | <input type="checkbox" ...> | FormControl<boolean> |
| InputDateDirective | <input type="date" ...>, also type="datetime-local" and type="time" | FormControl<Date> |
| InputFileDirective | <input type="file" ...> | FormControl<File \| null>, FormControl<File[]> when input accepts multiple files |
| InputNumberDirective | <input type="number" ...> | FormControl<number> |
| InputRadioDirective | <input type="radio" value="..." ...> | FormControl<string> |
| InputRangeDirective | <input type="range" ...> | FormControl<number> |
| InputTextDirective | Fallback for any other input and <textarea ...> | FormControl<string> |
| MatButtonToggleGroupDirective | MatButtonToggleGroup | FormControl<T> |
| MatCheckboxDirective | MatCheckbox | FormControl<boolean> |
| MatDatepickerDirective | <input [matDatepicker]="..." ...> | FormControl<D \| null> |
| MatRadioGroupDirective | MatRadioGroup | FormControl<T> |
| MatSelectDirective | MatSelect | FormControl<T \| null>, FormControl<T[]> on multiple select |
| MatSlideToggleDirective | MatSlideToggle | FormControl<boolean> |
| MatSliderRangeThumbDirective | MatSliderRangeThumb | FormControl<number> |
| MatSliderThumbDirective | MatSliderThumb | FormControl<number> |
| SelectDirective | <select ...> | FormControl<string> |
Usage of those Form-Directives are identical to Angular-FormControl / ngModel-Directives.
For more information, have a look at the official Angular Material Documentation
Reacting to FormChanges
Since all Forms are now Signals, you can use effect, computed etc. to react to value changes inside Forms.
import {formControl} from '@rytrox/form-signals';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
protected readonly form = formControl<string | null>('Hello');
public constructor() {
effect(() => {
const val = form();
// Interact with the value itself...
form.set(`${val} World`);
});
}
}Validators
Validators are now Functions that are bound to the FormControl's value.
You can either create a FormControl with Validators or you can add them by using the addValidator or removeValidator methods.
Via FormControl#validators, you have access to a signal of all validators of a form. You can
FormGroup
FormGroup is a record of multiple forms that creates a computed value of multiple forms.
You can declare a Builder-Function of a FormGroup by using the Factory-Function.
Let's assume, we have a Model like this:
interface Foo {
name: string;
id: number | null;
date?: Date;
}By using the formGroupFactory-Function, we create a Signal-Builder that creates our FormGroup:
export const FooGroup = formGroupFactory((val?: Foo) => {
return {
name: formControl(val?.name ?? ''),
id: formControl(val?.id ?? null),
date: formControl(val?.date)
};
});If a key is declared as optional, you can remove its control at any time by using setControl(null).
However, we must declare them inside our factory.
Now, we can create a FormGroup like this:
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
protected readonly form: FormFactoryType<typeof FooGroup> = FooGroup();
public constructor() {
effect(() => {
const val: Foo = form();
// Interact with the value itself...
});
}
}Access children of the group
You can access every field inside our FormGroup by using its key like this:
const form = FooGroup();
const nameControl = form.name;Properties of FormGroups cannot be named like errors, validators, addValidators, removeValidators, setControl, hasValidator.
Error Handling
You can access errors of all child forms by using the errors Signal of the group.
For example a typical form error looks like this:
{
"this": {
"validator1": "Group-Validator #1",
"validator2": "Group-Validator #2"
},
"controls": {
"name": {
"required": "This field is required!"
},
"id": {
"required": "This field is required!"
}
}
}FormArray
A FormArray is an array of the same form.
Like FormGroup it creates a computed array of multiple forms.
You can create a new FormArray-Build by using the formArrayFactory-Function like this:
import {formArrayFactory} from "./form-array";
const FooArray = formArrayFactory((foo: Foo) => FooGroup(foo));After that, you can create an Instance by using the builder just like before:
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
protected readonly form: FormFactoryType<typeof FooArray> = FooArray([]);
public constructor() {
effect(() => {
const val: Foo[] = form();
// Interact with the value itself...
});
}
}In contrast to Angular's FormArray, this one will not throw errors if you set a new Array with a different length.
Instead, it will automatically create new Forms for each value by using the factory function you declared earlier.
Iterating over the array
You can iterate though the FormArray like any other array by using for-of loop:
const form = FooArray([]);
for (const control of form) {
}Accessing the child
You can access the child by using its index:
const form = FooArray([]);
const control: FormGroup | undefined = form[0];It will return undefined, if the index is not set.
Error-Handling
Similar to FormGroup, you can access every error of each child form by using the error-Signal.
The Error will look like this:
{
"this": {
"validator1": "Array Validator #1"
},
"controls": [
{
"this": {
"validator1": "Group Validator #1"
},
"controls": {
"name": {
"required": "This field is required"
},
"id": {
"required": "This field is required"
}
}
},
null,
{
"this": null,
"controls": {
"name": {
"required": "This field is required"
}
}
}
]
}Contributing
Please feel free to contribute to this project
by creating issues or pull requests on our mirror project
License
Copyright (c) 2025 Team Rytrox
Licensed under the MIT license