Home › Learn
Forms
Bind fields with ngModel and reactive forms with validators on top of signals.
Binding fields with ngModel#
[(ngModel)]="signal" connects a field to a writable signal: what the user types updates the signal, and if you change the signal, the field updates. It works for input (text, number, checkbox) and select. For forms with validation, use the reactive forms below.
View the code
const { signal, computed, component, mount } = AngeLite;
class Greeter {
name = signal('world');
price = signal(1234.5);
today = new Date();
}
component(Greeter, {
template: `
<input [(ngModel)]="name">
<p>Hello, {{ name() | titlecase }}! ({{ name() | uppercase }})</p>
<p>Price: {{ price() | currency:'EUR' }} · Today: {{ today | date }}</p>`
});
mount(Greeter, root);[(ngModel)] with a computed or with a value that is not a signal throws a clear error ("needs a writable signal").
Reactive forms#
FormControl, FormGroup and FormArray (@duvanjamid/angelite/forms) keep their state in signals: value, errors, valid, touched and dirty. Because value is a writable signal, it binds with [(ngModel)], and because the rest are signals, the template reacts on its own.
A validator is a function (value) => null | { key: detail }. required, minLength, maxLength, min, max, pattern, email and matches (for "repeat the password") are included. The errors of several validators are merged in errors().
View the code
const { signal, component, mount, FormControl, FormGroup, Validators: V } = AngeLite;
class Signup {
form = new FormGroup({
name: new FormControl('', [V.required, V.minLength(2)]),
email: new FormControl('', [V.required, V.email])
});
sent = signal('');
submit() {
this.form.touch(); // marks everything as touched so errors show
if (this.form.valid()) this.sent.set('Sent: ' + JSON.stringify(this.form.value()));
}
}
component(Signup, {
template: `
<p><input [(ngModel)]="form.get('name').value" (blur)="form.get('name').touch()" placeholder="Name">
@if (form.get('name').touched() && form.get('name').errors()) {<span class="err">at least 2 letters</span>}</p>
<p><input [(ngModel)]="form.get('email').value" (blur)="form.get('email').touch()" placeholder="Email">
@if (form.get('email').touched() && form.get('email').errors()) {<span class="err">invalid email</span>}</p>
<button (click)="submit()" [disabled]="!form.valid()">Send</button>
<p>{{ sent() }}</p>`
});
mount(Signup, root);| You want… | Use |
|---|---|
| Show an error only after the user touches the field | (blur)="field.touch()" and field.touched() && field.errors() |
| Show all errors on submit | form.touch() marks everything as touched |
| Nested fields | new FormGroup({ address: new FormGroup({ street: … }) }) and form.get('address.street') |
| Lists of fields that grow | FormArray with push() and removeAt(); @for (c of list.controls(); track $index) |
| Go back to the initial state | form.reset() or form.reset({ name: 'x' }) |
| A rule that looks at several fields | a validator on the group: new FormGroup(controls, [v => …]) |
There are no async validators yet. For "does that email already exist?", validate on submit with http and set the error by hand in your own state.