Two-way binding mengikat satu properti di class dengan satu kotak isian di template, ke dua arah sekaligus: yang diketik di kotak langsung mengubah properti, dan properti yang diubah kode langsung mengubah isi kotak. Bayangin catatan pesanan di Martabak Bang Jali ditulis di papan tulis kecil yang menghadap dua arah. Pembeli menulis "pedas" dari depan, Bang Jali langsung membacanya dari belakang. Bang Jali menghapusnya, papan di depan pembeli ikut bersih. Satu papan, dua sisi.
🧪 Tebakan cepat: Kamu menulis
<input [(ngModel)]="catatan" />dengancatatan = 'tanpa daun bawang', tapi lupaimports: [FormsModule]. Saat halaman terbuka, apa isi kotaknya? Dan saat kamu mengetik, apakah<p>Catatan: ...</p>ikut berubah? Jawabannya ada di bagian "FormsModule: tanpa itu, ngModel gak dikenal".
[(ngModel)]: kotak isian dan properti jadi satu
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-pesanan',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="catatan" placeholder="Catatan" />
<button (click)="catatan = ''">Hapus</button>
<p>Catatan: {{ catatan }}</p>
`,
})
export class Pesanan {
catatan = 'tanpa daun bawang'
}
| Kejadian | Isi kotak | catatan |
<p> |
|---|---|---|---|
| Render pertama | tanpa daun bawang | 'tanpa daun bawang' |
Catatan: tanpa daun bawang |
| Isi kotak diganti jadi "pedas" | pedas | 'pedas' |
Catatan: pedas |
| Klik Hapus | (kosong) | '' |
Catatan: |
ngModeladalah directive dari@angular/formsyang menghubungkan kotak isian dengan properti.- Arah pertama: properti ke kotak. Saat halaman dibuka, kotaknya sudah berisi
tanpa daun bawang, dan saatcatatan = ''dijalankan tombol Hapus, kotaknya ikut kosong. - Arah kedua: kotak ke properti. Tiap huruf yang diketik langsung menjadi nilai
catatan, jadi<p>ikut berubah tanpa method apa pun. - Di checkbox,
[(ngModel)]="setuju"menyimpantrueataufalse, bukan teks.
Latihan pertama memakai bentuk yang sama: <input [(ngModel)]="nama" placeholder="Ketik nama..." /> dan <p>Halo, {{ nama }}!</p>. Mengetik Sinta membuat paragrafnya berbunyi Halo, Sinta!.
Banana in a box: [( )] = [ ] + ( )
Kurung siku di luar, kurung biasa di dalam: bentuknya kayak pisang di dalam kotak, makanya disebut banana in a box. Sintaks [()] ini cuma singkatan dari dua binding yang sudah kamu kenal:
<input [(ngModel)]="catatan" />
<!-- sama persis dengan: -->
<input [ngModel]="catatan" (ngModelChange)="catatan = $event" />
[ngModel]="catatan"membawa nilai dari class ke kotak.(ngModelChange)="catatan = $event"membawa isi kotak yang baru kembali ke class.$eventdi sini teks isinya, bukan objek event.- Tulis
[ngModel]saja, dan arahnya cuma satu: kotaknya terisi, tapi ketikanmu gak pernah sampai kecatatan. - Urutannya penting.
([ngModel])dibaca Angular sebagai event bernama[ngModel], yang gak pernah terjadi: kotaknya kosong,catatangak berubah, dan gak ada error yang memberi tahu.
FormsModule: tanpa itu, ngModel gak dikenal
ngModel bukan bagian dari template seperti @if. Dia directive yang tinggal di FormsModule, jadi komponen standalone harus mengimpornya: import { FormsModule } from '@angular/forms' di atas file, dan imports: [FormsModule] di @Component.
Kalau lupa, Angular gak mengenali ngModel. Kotaknya jadi input HTML biasa yang kosong saat halaman terbuka, walaupun catatan berisi tanpa daun bawang. Kamu tetap bisa mengetik di kotak itu, tapi <p> diam di Catatan: tanpa daun bawang, karena gak ada yang membawa ketikanmu ke class. Itu jawaban tebakannya. Console menulis:
NG0303: Can't bind to 'ngModel' since it isn't a known property of 'input' (used in the 'Pesanan' component template).
Two-way untuk komponenmu sendiri: [(jumlah)]
Banana in a box juga jalan di komponen buatanmu, asal pasangannya benar: satu @Input() bernama x dan satu @Output() bernama persis xChange.
@Component({
selector: 'app-pilih-jumlah',
standalone: true,
template: `
<button (click)="ubah(-1)">-</button>
<span>{{ jumlah }}</span>
<button (click)="ubah(1)">+</button>
`,
})
export class PilihJumlah {
@Input() jumlah = 1
@Output() jumlahChange = new EventEmitter<number>()
ubah(selisih: number) {
const baru = this.jumlah + selisih
if (baru < 1) {
return
}
this.jumlah = baru
this.jumlahChange.emit(baru)
}
}
<!-- template Pesanan, dengan PilihJumlah di imports dan jumlah = 1 -->
<app-pilih-jumlah [(jumlah)]="jumlah" />
<p>Total: Rp{{ jumlah * 50000 }}</p>
[(jumlah)]="jumlah"adalah singkatan dari[jumlah]="jumlah" (jumlahChange)="jumlah = $event".- Tekan + dua kali: angka di tombol jadi 3,
jumlahmilik Pesanan ikut jadi 3, danTotal: Rp150000. - Namai output-nya
jumlahBerubah, atau lupa memanggilemit, dan cuma angka di tombol yang naik: Pesanan tetap1, totalnya tetapRp50000, dan gak ada error.
💡 model(): pasangan input dan output dalam satu baris
Sejak Angular 17.2, jumlah = model(1) menggantikan pasangan @Input() dan @Output() jumlahChange. Nilainya dibaca jumlah() dan diubah dengan this.jumlah.set(baru), dan induknya tetap menulis [(jumlah)]="jumlah". Seperti input() di pelajaran sebelumnya, model() butuh Angular CLI: editor latihan dan Playground gak mengenalinya (NG0303: Can't bind to 'jumlah' since it isn't a known property of 'app-pilih-jumlah'), jadi di sini kita memakai pasangan dekoratornya.
⚠️ Jebakan yang sering ketemu
- Lupa
FormsModule. Kotaknya kosong, ketikan gak sampai ke class, dan console menulis NG0303 untukngModel. - Kurungnya terbalik.
([ngModel])diam saja tanpa error. Ingat pisang di dalam kotak:[(...)]. - Nama output bukan
xChange.[(jumlah)]cuma bekerja dengan output bernamajumlahChange. ngModeldi dalam<form>tanpaname. Angular menolak:If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.Tambahkanname="catatan", atau jangan bungkus dengan<form>.- Urutan paragraf di latihan kedua. Penilai membaca
<p>pertama, jadi<p>Email: {{ email }}</p>harus ditulis sebelum<p>Setuju: {{ setuju }}</p>, dan isinya dibaca langsung dariemail, bukan disalin sekali ke properti lain.
TL;DR: [(ngModel)]="catatan" mengikat kotak isian dan properti ke dua arah: nilai class mengisi kotak, ketikan mengubah class. Sintaks banana in a box [()] itu singkatan dari [ngModel]="catatan" (ngModelChange)="catatan = $event". ngModel tinggal di FormsModule, jadi komponen standalone butuh imports: [FormsModule]; tanpa itu kotaknya kosong, ketikan gak sampai, dan console menulis NG0303. Komponenmu sendiri bisa dipakai dengan [(jumlah)] kalau punya @Input() jumlah dan @Output() jumlahChange yang di-emit; model() adalah bentuk singkatnya di proyek Angular CLI.