Angelitedocs ENES

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.

ngModel + pipeslive
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);
The signal has to be writable

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

FormGroup with validatorslive
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 submitform.touch() marks everything as touched
Nested fieldsnew FormGroup({ address: new FormGroup({ street: … }) }) and form.get('address.street')
Lists of fields that growFormArray with push() and removeAt(); @for (c of list.controls(); track $index)
Go back to the initial stateform.reset() or form.reset({ name: 'x' })
A rule that looks at several fieldsa validator on the group: new FormGroup(controls, [v => …])
What is missing

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.