This episode covers Angular forms and validation: the differences between template-driven and reactive forms, using FormControl, FormGroup, and FormArray, built-in validation and custom validators, and dynamic forms with clear user feedback.

Forms are the main gateway for user interaction with an application — login, checkout, registration. Angular offers two different form approaches, each with its own strengths.
Episode 7 covers template-driven forms and reactive forms, using FormControl, FormGroup, and FormArray, built-in validation and custom validators, and dynamic forms with clear feedback. Reactive forms are the primary choice for serious applications, so that's where our main focus is.
ngModel. Good for simple forms with few fields.FormControl, FormGroup, and FormArray. More structured, easy to test, and better suited for complex logic.import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="submit()">
<input formControlName="email" placeholder="Email" />
<input formControlName="password" type="password" placeholder="Password" />
<button type="submit" [disabled]="loginForm.invalid">Masuk</button>
</form>
`,
})
export class LoginComponent {
loginForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
});
submit(): void {
console.log(this.loginForm.value);
}
}FormGroup wraps multiple FormControls. The formGroup and formControlName directives connect the class to the template. The submit button is disabled while the form is invalid — that's reactive forms' built-in feedback.
FormBuilder makes creating forms easier with the control, group, and array methods:
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-pesanan',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="kirim()">
<input formControlName="alamat" placeholder="Alamat" />
<div formArrayName="items">
@for (item of items.controls; track $index) {
<div [formGroupName]="$index">
<input formControlName="nama" placeholder="Nama item" />
</div>
}
</div>
<button type="submit">Kirim</button>
</form>
`,
})
export class PesananComponent {
private readonly fb = inject(FormBuilder);
form = this.fb.nonNullable.group({
alamat: ['', Validators.required],
items: this.fb.array([this.fb.group({ nama: [''] })]),
});
get items(): FormArray {
return this.form.get('items') as FormArray;
}
}formArrayName and formGroupName enable dynamic lists: you can add or remove items inside a FormArray at runtime. This is the foundation for order item forms, contact lists, and similar structures.
Angular provides Validators.required, Validators.email, Validators.minLength, Validators.maxLength, Validators.min, Validators.max, and Validators.pattern. Several can be combined in an array, like the login example above.
When built-in validation isn't enough, write your own validator. A validator is a function that returns null if valid, or an error object if not:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function hargaPositif(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const nilai = Number(control.value);
if (nilai === null || nilai === undefined || isNaN(nilai) || nilai <= 0) {
return { hargaTidakValid: { value: control.value } };
}
return null;
};
}The hargaPositif validator rejects non-numeric or zero values. It's used the same way as built-in validators: new FormControl('', [hargaPositif()]). Because validation can be at the control, group, or array level, cross-field logic like "start date before end date" can also be implemented.
Good feedback means errors appear clearly and on time. A common pattern: only show the message when the field has been touched (touched) and is invalid.
@if (email.invalid && email.touched) {
<small class="error">
@if (email.hasError('required')) {
Email wajib diisi
} @else if (email.hasError('email')) {
Format email tidak valid
}
</small>
}email.hasError('required') and hasError('email') check for specific error types. Combining invalid and touched prevents messages from appearing too early while the user is just starting to type.
For forms whose structure changes based on user input — for example, the payment method choice determining additional fields — combine a dynamic FormGroup with @if control flow in the template. Reactive forms make this kind of scenario relatively easy, because the form structure lives entirely in the class.
Key takeaways:
FormControl, FormGroup, and FormArray are the building blocks of reactive forms.FormBuilder reduces form-creation boilerplate.null or an error object.invalid and touched for well-timed error feedback.FormArray and @if control flow support dynamic forms.In the next episode, episode 8, we'll cover HTTP and data fetching — using HttpClient for REST API integration, working with Observables and RxJS, handling errors with retry, and caching and request optimization strategies.