Reactive Forms — Angular

Reactive Forms memberikan kontrol penuh atas form di TypeScript. Setup import { ReactiveFormsModule } from "@angular/forms" @Component({ imports…

Reactive form adalah form yang aturannya dibangun di class: tiap kolom jadi objek FormControl, kolom-kolomnya dikumpulkan dalam FormGroup, dan template cuma menyambungkan tiap <input> ke kontrolnya. Bayangin formulir pesan-antar Martabak Bang Jali. Pembeli mengisi lembar kertasnya (template), tapi kasir memegang buku aturan sendiri: nama wajib, nomor HP diawali 08, jumlah minimal 1. Tiap kali pembeli menulis, kasir langsung menilai kolom itu dan tahu formulirnya sah atau belum.

🧪 Tebakan cepat: Form pesanan baru dibuka. Kolom HP masih kosong dan punya Validators.required. Template-nya menulis @if (hp.errors) { <p>Nomor HP wajib diisi.</p> }. Pembeli belum menyentuh apa pun. Pesan itu sudah tampil? Jawabannya ada di bagian "Kapan pesan error ditampilkan".

FormGroup dan FormControl

import { Component } from '@angular/core'
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'

@Component({
  selector: 'app-pesanan',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="pesanForm" (ngSubmit)="kirim()">
      <input formControlName="nama" placeholder="Nama" />
      @if (nama.touched && nama.errors?.['required']) {
        <p class="error">Nama wajib diisi.</p>
      }
      <input formControlName="hp" placeholder="Nomor HP" />
      @if (hp.touched && hp.errors?.['required']) {
        <p class="error">Nomor HP wajib diisi.</p>
      }
      @if (hp.touched && hp.errors?.['pattern']) {
        <p class="error">Nomor HP diawali 08, total 10 sampai 13 angka.</p>
      }
      <input formControlName="jumlah" type="number" />
      @if (jumlah.touched && jumlah.errors) {
        <p class="error">Jumlah minimal 1.</p>
      }
      <button type="submit" [disabled]="pesanForm.invalid">Kirim</button>
    </form>
  `,
})
export class Pesanan {
  pesanForm = new FormGroup({
    nama: new FormControl('', Validators.required),
    hp: new FormControl('', [Validators.required, Validators.pattern('08[0-9]{8,11}')]),
    jumlah: new FormControl(1, [Validators.required, Validators.min(1)]),
  })

  get nama() { return this.pesanForm.controls.nama }
  get hp() { return this.pesanForm.controls.hp }
  get jumlah() { return this.pesanForm.controls.jumlah }

  constructor() {
    this.pesanForm.valueChanges.subscribe((nilai) => console.log('valueChanges:', JSON.stringify(nilai)))
  }

  kirim() {
    console.log('terkirim:', JSON.stringify(this.pesanForm.value))
  }
}

Validators: aturan tiap kolom

Validator Gagal saat Isi errors
Validators.required kosong ('' atau null) { required: true }
Validators.min(1) angkanya di bawah 1 { min: { min: 1, actual: 0 } }
Validators.pattern('08[0-9]{8,11}') bukan 08 diikuti 8 sampai 11 angka { pattern: { requiredPattern: '^08[0-9]{8,11} , actualValue: '0812' } }
Validators.minLength(8) kurang dari 8 karakter { minlength: { requiredLength: 8, actualLength: 3 } }
Validators.email bukan bentuk alamat email { email: true }

Isi kolom HP dari kosong sampai benar:

Isi kolom HP hp.errors pesanForm.status (nama dan jumlah sudah benar)
(kosong) { required: true } INVALID
0812 { pattern: { ... } } INVALID
081234567890 null VALID

Kapan pesan error ditampilkan

errors sudah terisi sejak form dibuat. Kolom HP yang kosong punya { required: true } dari detik pertama, jadi @if (hp.errors) menampilkan "Nomor HP wajib diisi." begitu form terbuka, sebelum pembeli menyentuh apa pun. Itu jawaban tebakannya. Pembeli dimarahi sebelum sempat mengetik.

Karena itu pesannya ditunggu sampai kolomnya disentuh:

Keadaan Jadi true saat
touched kolomnya pernah ditinggalkan (fokus pindah ke tempat lain)
dirty isinya pernah diubah oleh pembeli
invalid errors gak null

@if (hp.touched && hp.errors?.['pattern']) baru menampilkan pesannya sesudah pembeli mengisi 0812 lalu pindah kolom. errors?.['pattern'] membaca aturan mana yang gagal, jadi tiap aturan bisa punya kalimatnya sendiri.

Tombol Kirim dan valueChanges

Aturannya di TypeScript, jadi mudah dites

Semua aturan dan nilai form tinggal di class, bukan di template. Form-nya bisa diperiksa tanpa halaman sama sekali:

const form = new FormGroup({
  hp: new FormControl('', [Validators.required, Validators.pattern('08[0-9]{8,11}')]),
})
form.controls.hp.setValue('0812')
console.log(form.valid) // false

Itu keuntungan utama reactive form dibanding template-driven: logikanya di TypeScript, jadi bisa di-unit test tanpa DOM.

Latihan pertama memakai bentuk yang sama: loginForm = new FormGroup({ email: new FormControl('', [Validators.required]), password: new FormControl('', [Validators.required]) }), <form [formGroup]="loginForm">, dua <input formControlName>, dan tombol Login. Latihan kedua menambah Validators.email dan Validators.minLength(8), plus [disabled]="regForm.invalid".

📌 FormBuilder dan reset

FormBuilder menulis grup yang sama dengan lebih ringkas: private fb = inject(FormBuilder) lalu pesanForm = this.fb.group({ nama: ['', Validators.required], ... }). Hati-hati dengan pesanForm.reset(): semua nilainya jadi null, bukan nilai awal, jadi value berisi {"nama":null,"hp":null,"jumlah":null}. Pakai this.fb.nonNullable.group(...) supaya reset() kembali ke nilai awal.

⚠️ Jebakan yang sering ketemu

TL;DR: Reactive form dibangun di class: new FormGroup({ hp: new FormControl('', [Validators.required, Validators.pattern('08[0-9]{8,11}')]) }), lalu template menyambungkannya dengan [formGroup]="pesanForm" dan formControlName="hp" (plus ReactiveFormsModule di imports). Tiap kontrol punya errors (null kalau lolos), touched, dan dirty; tampilkan pesan dengan @if (hp.touched && hp.errors?.['pattern']), karena errors sudah terisi sejak form dibuka. [disabled]="pesanForm.invalid" mematikan Kirim selama ada yang salah, (ngSubmit) menjalankan kirim(), dan valueChanges mengirim nilai form tiap kali berubah. Karena logikanya di TypeScript, form ini bisa dites tanpa DOM.

Yang akan kamu pelajari