Proyek ini menyatukan semua yang sudah kamu pelajari jadi satu halaman pesanan Martabak Bang Jali yang utuh: menu dari @for, keranjang di service dengan signal dan computed, pipe rupiah, dan form yang mengirim pesanan lalu mengosongkan dirinya. Bayangin hari pertama warung buka. Papan menu, buku pesanan, dan kotak kasir sudah kamu coba satu per satu; hari ini semuanya dipakai bersamaan, dan angkanya harus cocok satu sama lain. Kalau kasir mencatat total di kertas sendiri, cepat atau lambat totalnya beda dengan isi buku pesanan.
🧪 Tebakan cepat:
tambah()diKeranjangStoreditulisthis.isi().push(item), bukanupdate. Pembeli klik Pesan dua kali, dan daftar keranjang memang menampilkan 2 baris. BadgeKeranjang: ... itemmenunjukkan berapa? Jawabannya ada di bagian "Satu sumber, diubah lewat update".
Isi proyeknya
bang-jali/
angular.json konfigurasi workspace: build, serve, test
package.json
tsconfig.json
src/
index.html <app-root></app-root>
main.ts bootstrapApplication(App, appConfig)
app/
app.config.ts provider untuk seluruh aplikasi
app.ts halaman: menu, keranjang, form
keranjang.store.ts signal + computed, satu sumber kebenaran
menu.ts interface Menu dan daftar menunya
rupiah.pipe.ts 40000 jadi Rp40.000
angular.jsonadalah file konfigurasi workspace. Di sana ada targetbuild,serve, dantest, folder output, dan"defaultConfiguration": "production"untuk build.- Tiap file punya satu tugas, dan tiap tugas berasal dari pelajaran sebelumnya:
@for/@emptydari Control Flow,signal/computeddari Signals dan State Management,RupiahPipedari Pipes,[(ngModel)]dan(ngSubmit)dari Template-driven Forms.
Halaman dan store-nya
// src/app/keranjang.store.ts
@Injectable({ providedIn: 'root' })
export class KeranjangStore {
private isi = signal<Item[]>([])
readonly daftar = this.isi.asReadonly()
readonly jumlah = computed(() => this.isi().length)
readonly total = computed(() => this.isi().reduce((t, item) => t + item.harga, 0))
tambah(item: Item) {
this.isi.update((isi) => [...isi, item])
}
buang(index: number) {
this.isi.update((isi) => isi.filter((_, i) => i !== index))
}
kosongkan() {
this.isi.set([])
}
}
<!-- template App (src/app/app.ts) -->
<p class="badge">Keranjang: {{ store.jumlah() }} item, {{ store.total() | rupiah }}</p>
@for (m of menu; track m.id) {
<p>
{{ m.nama }}: {{ m.harga | rupiah }}
<button (click)="store.tambah(m)" [disabled]="m.stok === 0">Pesan</button>
</p>
}
<ul>
@for (item of store.daftar(); track $index) {
<li>{{ item.nama }} <button (click)="store.buang($index)">Buang</button></li>
} @empty {
<li>Keranjang masih kosong</li>
}
</ul>
<form (ngSubmit)="kirim()">
<input name="nama" [(ngModel)]="nama" placeholder="Nama" />
<button type="submit">Kirim pesanan</button>
</form>
kirim() {
if (!this.nama.trim() || this.store.jumlah() === 0) {
return
}
this.nomor++
const total = 'Rp' + this.store.total().toLocaleString('id-ID')
this.terakhir.set(`Pesanan #${this.nomor} atas nama ${this.nama}: ${this.store.jumlah()} item, ${total}`)
this.store.kosongkan()
this.nama = ''
}
Dijalankan di Playground, langkah demi langkah:
| Aksi | isi |
jumlah() |
total() |
Yang tampil |
|---|---|---|---|---|
| Pesan Martabak Manis Cokelat Keju | 1 item | 1 | 40000 | Keranjang: 1 item, Rp40.000 |
| Pesan Martabak Telur Spesial | 2 item | 2 | 90000 | Keranjang: 2 item, Rp90.000 |
| Buang baris pertama | 1 item | 1 | 50000 | Keranjang: 1 item, Rp50.000 |
Nama Sinta, Kirim pesanan |
kosong | 0 | 0 | Pesanan #1 atas nama Sinta: 1 item, Rp50.000; Keranjang masih kosong; kolom Nama kosong |
Terang Bulan Pandan (stok 0) gak bisa dipesan: [disabled]="m.stok === 0".
Satu sumber, diubah lewat update
Versi tebakan tadi, dengan this.isi().push(item), setelah dua klik Pesan:
Versi update |
Versi push |
|
|---|---|---|
| Daftar keranjang | 2 baris | 2 baris |
| Badge | Keranjang: 2 item, Rp90.000 | Keranjang: 0 item, Rp0 |
| Kirim pesanan (nama terisi) | pesanan tercatat | gak terjadi apa-apa |
Badge-nya tetap 0 item. Itu jawaban tebakannya. push mengubah array yang lama, jadi signal-nya gak pernah memberi tahu bahwa isinya berubah, dan computed tetap memakai hasil lamanya. Daftarnya ikut bertambah cuma karena klik membuat Angular menggambar ulang template dan @for membaca array yang sama. kirim() pun berhenti di baris pertama, karena jumlah() masih 0. Selalu ubah signal lewat update atau set dengan array baru.
Pola form: tambah, lalu kosongkan
Dua latihan proyek memakai pola yang sama dengan kirim():
- Daftar disimpan di
signal<...>([]), totalnyacomputed(() => this.daftar().length). - Tiap kolom di dalam
<form>memakai[(ngModel)]plusname; tanpaname, Angular menolak dengan NG01352 (pelajaran Template-driven Forms). (ngSubmit)memanggil method yang mengabaikan isian kosong, menambah item baru denganupdate((list) => [...list, baru]), lalu mengosongkan kolomnya (this.name = '').- Hapus =
update((list) => list.filter((x) => x.id !== id)), bukan menyembunyikan baris.
@empty di dalam <ul> menggambar sebuah <li> saat daftarnya kosong. Itu pas untuk Contact List, tapi Note Taking App memeriksa bahwa ul li benar-benar habis setelah Hapus, jadi di sana jangan taruh @empty di dalam <ul>.
ng build: dari src ke dist
ng build mengubah semua file src jadi file siap upload. Defaultnya production (lihat angular.json); ng build --configuration development membuat versi yang gak dipadatkan untuk debugging. Output aplikasi ini:
Initial chunk files | Names | Raw size | Estimated transfer size
main-V7CNGQCQ.js | main | 155.05 kB | 43.94 kB
polyfills-RVUGEMAS.js | polyfills | 34.59 kB | 11.33 kB
| Initial total | 189.63 kB | 55.27 kB
Application bundle generation complete. [2.878 seconds]
Hasilnya ada di folder dist/: dist/bang-jali/browser berisi index.html, main-V7CNGQCQ.js, dan polyfills-RVUGEMAS.js. Isi folder browser itu yang diunggah ke hosting. Nama file berganti tiap isi kodenya berubah, jadi browser gak memakai versi lama dari cache.
⚠️ Jebakan yang sering ketemu
pushke array di dalam signal. Daftar kelihatan bertambah, tapicomputedgak pernah tahu. Pakaiupdatedengan array baru.- Total disimpan terpisah.
total = 0yang ditambah manual pasti tertinggal; turunkan dengancomputed. - Lupa mengosongkan kolom. Setelah ditambah,
this.name = ''(dan kolom lainnya), kalau gak, latihannya gagal di cek "kotak isian kosong". @emptydi Note Taking App.<li>dari@emptydihitung sebagai baris yang tersisa.- Detail latihan pertama (Contact List). Lengkapi template dengan
<h1>Contact List</h1>,<p>Total: {{ total() }} kontak</p>,<form (ngSubmit)="addContact()">berisi<input [(ngModel)]="name" name="name" />,<input [(ngModel)]="email" name="email" />, dan<button type="submit">Tambah</button>, lalu<ul>dengan@for (contact of contacts(); track contact.id), tombolHapusper baris, dan@empty. Di class:contacts = signal<Contact[]>([])dantotal = computed(...). Penilai mengetikBudidi semua kolom, menekanTambah, lalu mengharapBudiada di<ul>dan kolom pertama kosong lagi. Penilai juga membaca totalnya:Total: 0 kontak,Total: 1 kontaksesudahTambah, danTotal: 0 kontaklagi sesudahHapus. - Detail latihan kedua (Note Taking App).
notes = signal<Note[]>([]), form dengan<input [(ngModel)]="title" name="title" />,<textarea [(ngModel)]="content" name="content">, dan tombolSimpan;addNote()menolak judul kosong. Penilai mengetikRapatdi judul dan kontennya, menekanSimpan, laluHapus, dan mengharap gak ada<li>tersisa di<ul>.
TL;DR: Proyek Angular = workspace dengan angular.json (konfigurasi build, serve, test) dan src/app berisi file-file kecil yang masing-masing punya satu tugas. Simpan data bersama di service dengan signal, turunkan jumlah dan total dengan computed, dan ubah lewat update/set dengan array baru; push membuat daftar dan total gak cocok (2 baris, badge 0 item). Pola form: [(ngModel)] plus name, (ngSubmit), tambah dengan spread, lalu kosongkan kolomnya. ng build (default production) menulis hasilnya ke dist/bang-jali/browser, folder yang diunggah ke hosting.