Masukkan Password
68 Menampilkan Pengeluaran di UI
Di lecture ini, kita masuk ke tahap penting dalam project Expense Tracker: sebelumnya kita sudah berhasil menyimpan expense ke dalam array, sekarang kita belajar bagaimana mengambil data dari array tersebut lalu menampilkannya ke HTML/UI.
Inti lecture ini sebenarnya adalah:
Data → loop → tentukan container → buat HTML → tampilkan ke UI
1. Kondisi project sebelum lecture ini
Di lecture sebelumnya, kita sudah punya:
const expenseItems: Expense[] = [];
Array ini menyimpan semua expense yang dibuat user.
Misalnya user memasukkan:
Salary → credit → 10000
Groceries → debit → 200
Internet → debit → 500
Bonus → credit → 3000
Maka array kita kira-kira:
expenseItems
│
├── Expense #1
│ ├── type: "credit"
│ ├── description: "Salary"
│ └── amount: 10000
│
├── Expense #2
│ ├── type: "debit"
│ ├── description: "Groceries"
│ └── amount: 200
│
├── Expense #3
│ ├── type: "debit"
│ ├── description: "Internet"
│ └── amount: 500
│
└── Expense #4
├── type: "credit"
├── description: "Bonus"
└── amount: 3000
Masalahnya:
Data tersebut memang sudah ada di TypeScript, tetapi belum muncul di halaman HTML.
Jadi kita perlu membuat mekanisme:
expenseItems
↓
ambil setiap Expense
↓
cek type
↓
credit → credit container
debit → debit container
↓
buat HTML
↓
tampilkan ke browser
2. Mengakses container dari HTML
Di HTML sudah ada dua container:
<div class="expense-item-containers">
<div class="expense-debit-item-container">
...
</div>
<div class="expense-credit-item-container">
...
</div>
</div>
Kita ingin:
DEBIT
↓
expense-debit-item-container
CREDIT
↓
expense-credit-item-container
Jadi pertama kita perlu mengambil kedua <div> tersebut dari TypeScript.
3. Menggunakan querySelector()
Untuk debit:
const debitDiv = document.querySelector(
".expense-debit-item-container"
) as HTMLDivElement;
Untuk credit:
const creditDiv = document.querySelector(
".expense-credit-item-container"
) as HTMLDivElement;
Perhatikan tanda titik:
".expense-debit-item-container"
Kenapa ada .?
Karena kita sedang mencari class.
Sama seperti CSS:
.expense-debit-item-container
Jadi:
document.querySelector(".nama-class")
artinya:
“Cari element yang memiliki class tersebut.”
4. Kenapa menggunakan as HTMLDivElement?
querySelector() secara umum bisa mengembalikan:
Element | null
Padahal kita tahu bahwa element yang kita cari adalah <div>.
Maka kita mengatakan kepada TypeScript:
as HTMLDivElement
Artinya:
“Saya yakin element ini adalah HTMLDivElement.”
Sehingga:
const debitDiv = document.querySelector(
".expense-debit-item-container"
) as HTMLDivElement;
TypeScript sekarang tahu bahwa debitDiv adalah:
HTMLDivElement
bukan sekadar:
Element | null
5. Kenapa perlu ! juga?
Di transcript, lecture juga menggunakan:
document.querySelector(".expense-debit-item-container")!
Tanda:
!
disebut non-null assertion operator.
Artinya:
“Saya yakin hasil query ini tidak
null.”
Jadi biasanya bentuknya:
const debitDiv = document.querySelector(
".expense-debit-item-container"
)! as HTMLDivElement;
atau sering ditulis:
const debitDiv = document.querySelector(
".expense-debit-item-container"
) as HTMLDivElement;
Dalam contoh lecture, keduanya digunakan untuk meyakinkan TypeScript bahwa element memang tersedia dan merupakan <div>.
6. Sekarang kita punya dua container
Secara konsep:
HTML
│
┌──────────┴──────────┐
↓ ↓
Debit Container Credit Container
│ │
debitDiv creditDiv
Nanti kita akan memasukkan expense ke salah satu dari kedua container ini.
7. Membuat function displayExpenseItems()
Sekarang kita membuat function:
function displayExpenseItems() {
// ...
}
Tujuan function ini:
Mengambil semua expense dari
expenseItems, lalu menampilkannya ke UI.
Jadi function ini bertugas sebagai renderer sederhana.
8. Kenapa menggunakan for loop?
Karena expenseItems berisi banyak object.
Misalnya:
expenseItems = [
expense1,
expense2,
expense3
];
Kita ingin memproses:
expense1
expense2
expense3
satu per satu.
Maka digunakan:
for (
let i = 0;
i < expenseItems.length;
i++
) {
// ...
}
Mari kita pecah.
let i = 0
Mulai dari index:
0
i < expenseItems.length
Selama index masih berada dalam array.
i++
Setelah satu item selesai diproses, lanjut ke index berikutnya.
9. Mengambil Expense berdasarkan index
Di dalam loop:
const expenseItem = expenseItems[i];
Misalnya array:
index value
0 Expense #1
1 Expense #2
2 Expense #3
Ketika:
i = 0
maka:
expenseItems[0]
menghasilkan:
Expense #1
Ketika:
i = 1
menghasilkan:
Expense #2
dan seterusnya.
Jadi:
const expenseItem = expenseItems[i];
berarti:
“Ambil expense pada posisi/index saat ini.”
10. Sekarang kita punya satu Expense
Misalnya:
const expenseItem = expenseItems[i];
hasilnya:
{
id: 1,
type: "credit",
description: "Salary",
amount: 10000
}
Kita bisa mengakses:
expenseItem.type
expenseItem.description
expenseItem.amount
expenseItem.id
11. Menentukan apakah Credit atau Debit
Sekarang kita perlu menentukan:
Expense ini harus dimasukkan ke container mana?
Kita punya dua pilihan:
credit → creditDiv
debit → debitDiv
Maka:
if (expenseItem.type === "credit") {
// credit
} else {
// debit
}
12. Membuat containerDiv
Lecture membuat variable:
let containerDiv;
Variable ini akan menyimpan:
“Container mana yang harus digunakan untuk expense saat ini?”
Kemudian:
if (expenseItem.type === "credit") {
containerDiv = creditDiv;
} else {
containerDiv = debitDiv;
}
Secara visual:
expenseItem
│
↓
cek expenseItem.type
/ \
/ \
"credit" "debit"
↓ ↓
creditDiv debitDiv
13. Kenapa tidak langsung insert ke creditDiv / debitDiv?
Bisa saja.
Misalnya:
if (expenseItem.type === "credit") {
creditDiv.insertAdjacentHTML(...);
} else {
debitDiv.insertAdjacentHTML(...);
}
Tapi lecture memilih:
let containerDiv;
supaya setelah menentukan container, kita bisa memakai satu kode:
containerDiv.insertAdjacentHTML(...)
Jadi tidak perlu menulis kode HTML dua kali.
Ini membuat kode lebih DRY:
Don’t Repeat Yourself
14. Memasukkan HTML dengan insertAdjacentHTML()
Sekarang kita sudah tahu container-nya.
Kita perlu memasukkan HTML.
Lecture menggunakan:
containerDiv.insertAdjacentHTML(
"beforeend",
`...`
);
Method ini digunakan untuk memasukkan HTML ke element.
15. Apa arti "beforeend"?
Ini penting.
Misalnya kita punya:
<div class="container">
<p>Existing item</p>
</div>
Kemudian:
container.insertAdjacentHTML(
"beforeend",
"<h3>New item</h3>"
);
hasilnya:
<div class="container">
<p>Existing item</p>
<h3>New item</h3>
</div>
Jadi:
beforeend
↓
masukkan HTML tepat sebelum
closing tag element
Visual:
<div>
Existing content
← INSERT DI SINI
</div>
16. Kenapa menggunakan template literal?
Lecture menggunakan:
`
...
`
bukan:
""
atau:
''
Karena kita ingin memasukkan variable ke dalam HTML.
Misalnya:
const description = "Salary";
Kita bisa:
`<h3>${description}</h3>`
Hasilnya:
<h3>Salary</h3>
${...} disebut template literal interpolation.
17. Mengambil description
Lecture kemudian membuat:
`<h3>${expenseItem.description}</h3>`
Misalnya:
expenseItem.description
adalah:
Salary
maka HTML yang dihasilkan:
<h3>Salary</h3>
Kalau:
Groceries
maka:
<h3>Groceries</h3>
18. Jadi function sederhananya seperti ini
Secara konsep:
function displayExpenseItems() {
for (
let i = 0;
i < expenseItems.length;
i++
) {
const expenseItem = expenseItems[i];
let containerDiv;
if (expenseItem.type === "credit") {
containerDiv = creditDiv;
} else {
containerDiv = debitDiv;
}
containerDiv.insertAdjacentHTML(
"beforeend",
`<h3>${expenseItem.description}</h3>`
);
}
}
Ini adalah inti dari lecture.
19. Memanggil function setelah push
Sebelumnya kita punya:
expenseItems.push(expense);
Kemudian sebelumnya mungkin:
console.log(expenseItems);
Sekarang kita ganti dengan:
expenseItems.push(expense);
displayExpenseItems();
Urutannya menjadi:
User klik Add Expense
↓
buat Expense object
↓
expenseItems.push(expense)
↓
data masuk array
↓
displayExpenseItems()
↓
loop seluruh array
↓
buat HTML
↓
HTML masuk ke DOM
↓
muncul di browser
20. Contoh nyata
Misalnya user memasukkan:
Type: credit
Description: Salary
Amount: 10000
Object:
{
id: 1,
type: "credit",
description: "Salary",
amount: 10000
}
Masuk ke:
expenseItems
Kemudian:
displayExpenseItems();
Loop mengambil:
expenseItem.type
hasilnya:
"credit"
Maka:
containerDiv = creditDiv;
Kemudian:
containerDiv.insertAdjacentHTML(
"beforeend",
`<h3>${expenseItem.description}</h3>`
);
Hasil HTML:
<h3>Salary</h3>
Dan akhirnya browser menampilkan:
Salary
di bagian credit.
21. Kemudian user menambahkan debit
Misalnya:
Type: debit
Description: Groceries
Amount: 200
Object:
{
id: 2,
type: "debit",
description: "Groceries",
amount: 200
}
Setelah:
expenseItems.push(expense);
array menjadi:
[
Expense #1 → Salary
Expense #2 → Groceries
]
Function dijalankan.
Loop pertama:
Salary
credit
→ masuk creditDiv.
Loop kedua:
Groceries
debit
→ masuk debitDiv.
Hasil:
CREDIT
────────────
Salary
DEBIT
────────────
Groceries
22. Masalah yang muncul: Salary menjadi dua kali
Nah, ini bagian yang sangat penting dari lecture.
Misalnya awalnya:
Salary
Sudah tampil.
Kemudian user menambahkan:
Groceries
Function:
displayExpenseItems();
dijalankan lagi.
Sekarang array:
[
Salary,
Groceries
]
Function melakukan loop:
Salary
Groceries
dan memasukkan keduanya ke UI.
Masalahnya:
Salary yang lama masih ada di UI.
Jadi browser menjadi:
Salary
Salary
Groceries
23. Kenapa terjadi duplikasi?
Karena function kita melakukan:
ambil semua data
↓
tambahkan semua ke UI
tetapi tidak membersihkan UI yang lama.
Misalnya sebelum function:
UI:
Salary
Array:
[Salary, Groceries]
Function dijalankan:
tambahkan Salary
tambahkan Groceries
UI akhirnya:
Salary ← yang lama
Salary ← hasil render baru
Groceries
24. Solusinya: clear UI terlebih dahulu
Sebelum loop dimulai, kita kosongkan kedua container.
debitDiv.innerHTML = "";
creditDiv.innerHTML = "";
Jadi:
function displayExpenseItems() {
debitDiv.innerHTML = "";
creditDiv.innerHTML = "";
// kemudian baru loop
}
Ini sangat penting.
Sekarang alurnya:
displayExpenseItems()
↓
clear debit container
↓
clear credit container
↓
loop seluruh expense
↓
render ulang semua expense
25. Kenapa harus clear KEDUANYA?
Karena function menampilkan dua jenis data:
credit
debit
Kalau hanya:
creditDiv.innerHTML = "";
maka debit lama masih tetap ada.
Jadi kita harus:
debitDiv.innerHTML = "";
creditDiv.innerHTML = "";
26. innerHTML = "" sebenarnya melakukan apa?
Misalnya:
<div class="credit">
<h3>Salary</h3>
<h3>Bonus</h3>
</div>
Kemudian:
creditDiv.innerHTML = "";
Maka isinya menjadi:
<div class="credit">
</div>
Dengan kata lain:
Semua HTML di dalam element tersebut dihapus.
Perlu diperhatikan: yang dikosongkan adalah isi di dalam container, bukan container-nya sendiri.
27. Setelah ditambahkan clear, masalah selesai
Sekarang misalnya:
Sebelum klik kedua
Array:
[Salary]
UI:
CREDIT
Salary
Kemudian user menambahkan:
Groceries
Array:
[Salary, Groceries]
displayExpenseItems() dijalankan.
Pertama:
creditDiv.innerHTML = "";
debitDiv.innerHTML = "";
UI menjadi:
CREDIT
kosong
DEBIT
kosong
Kemudian loop:
Salary
Groceries
Render ulang:
CREDIT
Salary
DEBIT
Groceries
Tidak ada duplikasi.
28. Ini sebenarnya konsep penting: Re-render
Tanpa sadar, lecture ini sedang mengajarkan konsep yang sangat penting dalam frontend:
Re-render UI berdasarkan data terbaru.
Polanya:
DATA
↓
CLEAR UI
↓
RENDER DATA
↓
UI
Setiap kali data berubah:
expenseItems berubah
↓
displayExpenseItems()
↓
UI diperbarui
Jadi UI menjadi representasi dari data yang ada di array.
29. Analogi sederhana
Bayangkan expenseItems adalah daftar belanja di kertas.
Misalnya:
Kertas:
Salary
Groceries
Internet
Sedangkan UI adalah papan pengumuman.
Ketika data berubah, kita tidak ingin menulis:
Salary
Salary
Groceries
Salary
Groceries
Internet
Kita melakukan:
1. Hapus isi papan
2. Baca ulang kertas
3. Tulis semua data terbaru ke papan
Itulah yang dilakukan:
innerHTML = "";
kemudian:
for (...)
30. Struktur keseluruhan code
Kalau digabung dengan konsep lecture sebelumnya, kira-kira bentuknya:
const debitDiv = document.querySelector(
".expense-debit-item-container"
) as HTMLDivElement;
const creditDiv = document.querySelector(
".expense-credit-item-container"
) as HTMLDivElement;
class Expense {
readonly id: number;
private static currentId = 0;
type: "credit" | "debit" = "debit";
description = "";
amount = 0;
constructor(
type: "credit" | "debit",
description: string,
amount: number
) {
this.id = ++Expense.currentId;
this.type = type;
this.description = description;
this.amount = amount;
}
}
const expenseItems: Expense[] = [];
function displayExpenseItems() {
debitDiv.innerHTML = "";
creditDiv.innerHTML = "";
for (
let i = 0;
i < expenseItems.length;
i++
) {
const expenseItem = expenseItems[i];
let containerDiv;
if (expenseItem.type === "credit") {
containerDiv = creditDiv;
} else {
containerDiv = debitDiv;
}
containerDiv.insertAdjacentHTML(
"beforeend",
`<h3>${expenseItem.description}</h3>`
);
}
}
Kemudian saat tombol diklik:
addExpenseButton.addEventListener("click", (event) => {
event.preventDefault();
const expense = new Expense(
type,
expenseDescription.value,
+expenseAmount.value
);
expenseItems.push(expense);
displayExpenseItems();
});
31. Perhatikan pembagian tanggung jawab
Sekarang project kita mulai mempunyai beberapa bagian.
Expense
Bertanggung jawab merepresentasikan satu expense.
new Expense(...)
expenseItems
Bertanggung jawab menyimpan semua expense.
const expenseItems: Expense[] = [];
displayExpenseItems()
Bertanggung jawab mengubah data menjadi UI.
displayExpenseItems();
Event listener
Bertanggung jawab menangani aksi user.
addExpenseButton.addEventListener(...)
Jadi:
USER
↓
klik Add Expense
↓
EVENT HANDLER
↓
buat Expense
↓
expenseItems.push()
↓
displayExpenseItems()
↓
DOM
↓
UI
Ini adalah pola yang sangat bagus untuk dipahami karena nanti akan sering muncul di aplikasi frontend.
32. Kenapa loop seluruh array setiap kali?
Mungkin muncul pertanyaan:
“Kenapa tidak langsung menambahkan expense yang baru saja dibuat?”
Sebenarnya bisa.
Tetapi pendekatan lecture adalah:
hapus UI
↓
ambil seluruh data
↓
render seluruh data
Keuntungannya sederhana dan konsisten.
Misalnya array saat ini:
[
Salary,
Groceries,
Internet,
Bonus
]
Kita selalu bisa mengatakan:
“UI harus mencerminkan isi
expenseItems.”
Jadi kalau array memiliki 4 expense, UI harus memiliki 4 expense.
Ini adalah konsep:
Single source of truth
Dalam project kecil ini, sumber data utamanya adalah:
expenseItems
33. insertAdjacentHTML() vs innerHTML
Keduanya sama-sama berhubungan dengan HTML, tetapi penggunaannya berbeda.
innerHTML = ""
Kita gunakan untuk mengosongkan:
creditDiv.innerHTML = "";
insertAdjacentHTML()
Kita gunakan untuk menambahkan HTML:
containerDiv.insertAdjacentHTML(
"beforeend",
`<h3>${expenseItem.description}</h3>`
);
Jadi gampangnya:
innerHTML = ""
↓
CLEAR
insertAdjacentHTML()
↓
ADD
34. Kenapa belum menampilkan amount?
Di lecture ini, sementara hanya:
expenseItem.description
yang ditampilkan.
Misalnya:
<h3>Salary</h3>
Tetapi tujuan akhirnya adalah membuat tampilan yang lebih bagus:
┌────────────────────────────┐
│ Salary $10,000│
│ │
│ X │
└────────────────────────────┘
atau untuk debit:
┌────────────────────────────┐
│ Groceries $200 │
│ │
│ X │
└────────────────────────────┘
Lecture mengatakan bahwa pada lecture berikutnya mereka akan membuat desain yang lebih lengkap.
Jadi saat ini kita sedang fokus pada:
Bagaimana data dari array bisa masuk ke UI.
35. Hubungan dengan TypeScript yang sudah kita pelajari
Lecture ini sebenarnya menggabungkan banyak konsep sebelumnya.
Type Assertion
document.querySelector(...) as HTMLDivElement
Kita sudah belajar ini.
Literal Union Type
Di Expense:
type: "credit" | "debit"
Kemudian digunakan:
if (expenseItem.type === "credit")
Class
class Expense
Satu expense direpresentasikan sebagai object dari class tersebut.
Array of Class Objects
const expenseItems: Expense[] = [];
Artinya:
Array ini hanya boleh berisi object
Expense.
Loop
for (...)
Digunakan untuk mengambil setiap expense.
DOM manipulation
innerHTML
dan:
insertAdjacentHTML()
digunakan untuk mengubah UI.
36. Gambaran besar project sampai sekarang
Sekarang kita sudah memiliki pipeline yang cukup lengkap:
USER
│
↓
Isi form expense
│
↓
Klik Add Expense
│
↓
Event Listener
│
↓
new Expense(...)
│
↓
┌───────────────────┐
│ Expense Object │
│ │
│ type │
│ description │
│ amount │
│ id │
└───────────────────┘
│
↓
expenseItems.push()
│
↓
┌───────────────────┐
│ Expense[] Array │
│ │
│ E1 │
│ E2 │
│ E3 │
└───────────────────┘
│
↓
displayExpenseItems()
│
┌───────┴───────┐
↓ ↓
CREDIT DEBIT
↓ ↓
creditDiv debitDiv
│ │
└───────┬───────┘
↓
UI
37. Satu hal yang sangat penting: data ≠ UI
Ini konsep fundamental.
expenseItems:
const expenseItems: Expense[] = [];
adalah data.
Sedangkan:
<div class="expense-credit-item-container">
adalah bagian dari UI/DOM.
Keduanya berbeda.
DATA
expenseItems
│
│ render
↓
UI
HTML / DOM
Data bisa ada tanpa UI.
Misalnya:
expenseItems.push(expense);
data sudah tersimpan.
Tetapi kalau kita tidak menjalankan:
displayExpenseItems();
user belum tentu melihat data tersebut.
38. Kenapa ini penting dalam aplikasi frontend?
Karena hampir semua aplikasi frontend punya pola seperti ini.
Misalnya aplikasi Todo:
todos array
↓
renderTodos()
↓
HTML
Aplikasi chat:
messages array
↓
renderMessages()
↓
HTML
Aplikasi shopping cart:
cartItems array
↓
renderCart()
↓
HTML
Expense Tracker:
expenseItems array
↓
displayExpenseItems()
↓
HTML
Jadi project ini sebenarnya sedang mengajarkan fondasi aplikasi frontend.
39. Kenapa menggunakan type untuk menentukan container?
Karena kita sudah mendesain Expense dengan:
type: "credit" | "debit"
Maka property tersebut menjadi semacam penanda/kategori.
type = "credit"
↓
credit container
type = "debit"
↓
debit container
Ini mirip konsep yang sebelumnya kita pelajari tentang discriminated union.
Walaupun di sini kita menggunakan class, ide dasarnya sama:
Property
typememberi tahu kita variant/kategori dari object.
40. Ringkasan displayExpenseItems()
Function ini melakukan 4 langkah utama:
Step 1 — Bersihkan UI
debitDiv.innerHTML = "";
creditDiv.innerHTML = "";
Step 2 — Loop semua expense
for (let i = 0; i < expenseItems.length; i++)
Step 3 — Tentukan container
if (expenseItem.type === "credit") {
containerDiv = creditDiv;
} else {
containerDiv = debitDiv;
}
Step 4 — Masukkan HTML
containerDiv.insertAdjacentHTML(
"beforeend",
`<h3>${expenseItem.description}</h3>`
);
Jadi:
CLEAR
↓
LOOP
↓
CHECK TYPE
↓
CHOOSE CONTAINER
↓
INSERT HTML
41. Cheat Sheet Lecture
| Code | Fungsi |
|---|---|
document.querySelector() | mencari element HTML |
.className | selector untuk class |
as HTMLDivElement | type assertion ke <div> |
! | memberi tahu TS bahwa value tidak null/undefined |
Expense[] | array berisi Expense |
expenseItems[i] | mengambil Expense berdasarkan index |
expenseItem.type | mengetahui credit/debit |
innerHTML = "" | mengosongkan isi HTML |
insertAdjacentHTML() | memasukkan HTML |
"beforeend" | memasukkan HTML di akhir element |
`${value}` | memasukkan variable ke template literal |
displayExpenseItems() | render expense ke UI |
42. Inti lecture dalam satu kalimat
Kalau harus mengingat satu hal saja dari lecture ini:
Setiap kali expense ditambahkan, kita simpan ke
expenseItems, laludisplayExpenseItems()mengosongkan container, melakukan loop terhadap semua expense, menentukan apakah expense masuk ke credit atau debit container, kemudian membuat HTML untuk menampilkannya.
Atau hafalkan pola ini:
USER ACTION
↓
DATA UPDATE
↓
RENDER FUNCTION
↓
CLEAR OLD UI
↓
LOOP DATA
↓
CHOOSE CONTAINER
↓
CREATE HTML
↓
DISPLAY
Dan ini adalah jembatan penting dari TypeScript yang hanya mengolah data menuju TypeScript yang benar-benar mengontrol tampilan aplikasi di browser.