Forms

AngularDart provides two approaches for handling user input through forms: template-driven forms and reactive forms.

Template-Driven Forms

Template-driven forms are the simplest approach. Most of the logic is in the template.

Setup

Import the forms directives:

import 'package:angulardart/angular.dart';
import 'package:angulardart_forms/forms.dart';

@Component(
  selector: 'my-form',
  template: "
    <form #form="ngForm" (ngSubmit)="onSubmit(form)">
      <input ngModel name="name" required placeholder="Name">
      <input ngModel name="email" type="email" required placeholder="Email">
      <button type="submit" [disabled]="!form.valid">Submit</button>
    </form>
  ",
  directives: [formDirectives],
)
class MyFormComponent {
  void onSubmit(NgForm form) {
    print('Form data: ${form.value}');
  }
}

Accessing form data

<form #heroForm="ngForm">
  <input ngModel name="name" #nameField="ngModel" required>

  <div *ngIf="nameField.invalid && nameField.touched">
    Name is required
  </div>

  <p>Form valid: {{ heroForm.valid }}</p>
  <p>Form value: {{ heroForm.value | json }}</p>
</form>

NgModel properties

The ngModel directive provides these properties through a template reference:

Property Description
valid Whether the control is valid
invalid Whether the control is invalid
pristine Whether the user has not changed the value
dirty Whether the user has changed the value
touched Whether the control has been blurred
untouched Whether the control has not been blurred
errors Map of validation errors

Reactive Forms

Reactive forms provide a model-driven approach with explicit form model objects in the component class.

Setup

import 'package:angulardart/angular.dart';
import 'package:angulardart_forms/forms.dart';

@Component(
  selector: 'reactive-form',
  template: "
    <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
      <label>
        Name:
        <input formControlName="name">
      </label>
      <label>
        Email:
        <input formControlName="email" type="email">
      </label>
      <button type="submit" [disabled]="profileForm.invalid">Submit</button>
    </form>
  ",
  directives: [formDirectives, reactiveFormsDirectives],
)
class ReactiveFormComponent {
  late final FormGroup profileForm;

  ReactiveFormComponent() {
    profileForm = FormGroup({
      'name': FormControl<String>('', Validators.required),
      'email': FormControl<String>('', [Validators.required, Validators.email]),
    });
  }

  void onSubmit() {
    if (profileForm.valid) {
      print('Form data: ${profileForm.value}');
    }
  }
}

FormControl

A FormControl tracks the value and validation status of an individual form control:

final nameControl = FormControl<String>('John', Validators.required);

// Properties
nameControl.value;          // 'John'
nameControl.valid;          // true
nameControl.invalid;        // false
nameControl.errors;         // null
nameControl.touched;        // false
nameControl.dirty;          // false

// Methods
nameControl.markAsTouched();
nameControl.markAsDirty();
nameControl.setValue('Jane');
nameControl.reset();

FormGroup

A FormGroup tracks the value and status of a collection of FormControl instances:

final profileForm = FormGroup({
  'name': FormControl<String>('', Validators.required),
  'email': FormControl<String>('', Validators.required),
});

// Properties
profileForm.value;    // {'name': '', 'email': ''}
profileForm.valid;    // false (both controls invalid)
profileForm.controls; // Map of controls

// Methods
profileForm.setValue({'name': 'John', 'email': 'john@example.com'});
profileForm.patchValue({'name': 'Jane'}); // Partial update
profileForm.reset();

FormArray

A FormArray tracks the value and status of an array of form controls:

final aliases = FormArray<String>([
  FormControl<String>('alias1'),
  FormControl<String>('alias2'),
]);

aliases.add(FormControl<String>('alias3'));
aliases.removeAt(0);
aliases.length; // 2

Built-in Validators

Synchronous validators

Validators.required       // Value must not be null or empty
Validators.minLength(3)   // Minimum string length
Validators.maxLength(10)  // Maximum string length
Validators.min(0)         // Minimum numeric value
Validators.max(100)       // Maximum numeric value
Validators.pattern(r'^\d+$')  // Regex pattern
Validators.email          // Valid email format

Combining validators

FormControl<String>('', [
  Validators.required,
  Validators.minLength(3),
  Validators.maxLength(20),
]);

Custom validators

Map<String, dynamic>? forbiddenNameValidator(AbstractControl control) {
  final value = control.value as String?;
  if (value != null && value.toLowerCase() == 'admin') {
    return {'forbiddenName': true};
  }
  return null;
}

final nameControl = FormControl<String>('', forbiddenNameValidator);

Async validators

Future<Map<String, dynamic>?> uniqueEmailValidator(AbstractControl control) async {
  final email = control.value as String?;
  if (email == null) return null;

  final isTaken = await emailService.isEmailTaken(email);
  return isTaken ? {'emailTaken': true} : null;
}

final emailControl = FormControl<String>('', [], uniqueEmailValidator);

Cross-field validation

Validate multiple fields together:

Map<String, dynamic>? passwordMatchValidator(AbstractControl group) {
  final password = (group as FormGroup).controls['password']?.value;
  final confirm = group.controls['confirmPassword']?.value;
  return password == confirm ? null : {'passwordMismatch': true};
}

final form = FormGroup({
  'password': FormControl<String>('', Validators.required),
  'confirmPassword': FormControl<String>('', Validators.required),
}, passwordMatchValidator);

Form submission

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <input formControlName="name">
  <button type="submit" [disabled]="form.invalid">Submit</button>
</form>
void onSubmit() {
  if (form.valid) {
    // Process form data
    final data = form.value;
  }
}