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;
}
}