Sebelum satu pun fitur pencarian ditulis, ada dua pertanyaan yang harus dijawab lebih dulu: data apa yang dicari, dan informasi apa yang perlu diingat antarmuka tentang keinginan pengguna. Menjawab keduanya di akhir sering membuat kode berantakan — fungsi pencarian ternyata butuh kolom yang tidak ada di data, atau state tersebar di banyak variabel global. Bagian pertama proyek ini menyelesaikan kedua fondasi itu sejak awal: kita menyusun dataset katalog buku, merancang state pencarian yang lengkap, dan mengunci keduanya dengan uji otomatis pertama.
Dataset: Katalog Buku sebagai Array of Objects #
Dataset proyek ini adalah katalog toko buku fiktif berisi 22 judul. Setiap buku direpresentasikan sebagai satu objek dengan lima properti — cukup untuk semua fitur proyek, tidak satu pun berlebih:
| Properti | Tipe | Contoh | Dipakai untuk |
|---|---|---|---|
judul | string | 'Pemrograman JavaScript untuk Pemula' | pencarian teks, pengurutan abjad |
penulis | string | 'Raka Pratama' | pencarian teks |
kategori | string | 'Pemrograman' | filter kategori |
harga | number | 99000 | filter rentang harga, pengurutan |
rating | number | 4.7 | pengurutan rating |
Tiga kategori (Pemrograman, Novel, Sejarah) dan dua kategori lain (Bisnis, Anak) mengisi katalog, dengan harga dari Rp35.000 sampai Rp148.000 dan rating dari 3.7 sampai 4.9. Nilai-nilai itu sengaja dibuat beragam supaya setiap filter dan pengurutan benar-benar menghasilkan urutan berbeda. Berkasnya, src/data.mjs, berisi array bernama BUKU:
// data.mjs — katalog buku fiktif yang menjadi bahan proyek pencarian dan filter.
// Setiap buku membawa lima properti: judul, penulis, kategori, harga, rating.
export const BUKU = [
{ judul: 'Pemrograman JavaScript untuk Pemula', penulis: 'Raka Pratama', kategori: 'Pemrograman', harga: 99000, rating: 4.7 },
{ judul: 'JavaScript Modern dari Variabel sampai Module', penulis: 'Sinta Larasati', kategori: 'Pemrograman', harga: 125000, rating: 4.8 },
{ judul: 'Algoritma dan Struktur Data', penulis: 'Bayu Nugroho', kategori: 'Pemrograman', harga: 110000, rating: 4.5 },
{ judul: 'Belajar Python dari Nol', penulis: 'Maya Kirana', kategori: 'Pemrograman', harga: 89000, rating: 4.3 },
{ judul: 'Dasar SQL untuk Analis Data', penulis: 'Hendra Wijaya', kategori: 'Pemrograman', harga: 95000, rating: 4.1 },
{ judul: 'Gadis di Ujung Pelabuhan', penulis: 'Dian Paramita', kategori: 'Novel', harga: 85000, rating: 4.4 },
{ judul: 'Hujan di Atas Menara', penulis: 'Fajar Ramadhan', kategori: 'Novel', harga: 78000, rating: 4.2 },
{ judul: 'Rumah Kaca di Jalanan Kota', penulis: 'Salsabila Zahra', kategori: 'Novel', harga: 92000, rating: 4.6 },
{ judul: 'Sepuluh Musim di Kota Tua', penulis: 'Gilang Saputra', kategori: 'Novel', harga: 81000, rating: 3.9 },
{ judul: 'Jejak Rempah Nusantara', penulis: 'Kartika Ayu', kategori: 'Sejarah', harga: 135000, rating: 4.9 },
{ judul: 'Bandung Tempo Dulu', penulis: 'Rizky Maulana', kategori: 'Sejarah', harga: 105000, rating: 4.0 },
{ judul: 'Kerajaan Maritim dan Jalur Sutra', penulis: 'Umar Abdillah', kategori: 'Sejarah', harga: 148000, rating: 4.7 },
{ judul: 'Merintis Warung Kopi Modern', penulis: 'Tania Puspita', kategori: 'Bisnis', harga: 88000, rating: 4.2 },
{ judul: 'Manajemen Uang Pribadi', penulis: 'Dimas Aryo', kategori: 'Bisnis', harga: 76000, rating: 4.4 },
{ judul: 'Pemasaran Digital untuk Usaha Kecil', penulis: 'Nadia Kusuma', kategori: 'Bisnis', harga: 102000, rating: 4.5 },
{ judul: 'Menulis untuk Membangun Personal Brand', penulis: 'Reza Firmansyah', kategori: 'Bisnis', harga: 94000, rating: 3.8 },
{ judul: 'Petualangan Kiko di Pulau Merah', penulis: 'Ririn Amalia', kategori: 'Anak', harga: 54000, rating: 4.6 },
{ judul: 'Kisah Satu Planet Kecil', penulis: 'Bagas Prakoso', kategori: 'Anak', harga: 62000, rating: 4.3 },
{ judul: 'Kelas Angka Menyenangkan', penulis: 'Lia Amanah', kategori: 'Anak', harga: 48000, rating: 4.0 },
{ judul: 'Dunia Binatang Bersayap', penulis: 'Yoga Prasetyo', kategori: 'Anak', harga: 57000, rating: 4.5 },
{ judul: 'Ensiklopedia Mini: Tubuh Manusia', penulis: 'Wulan Sari', kategori: 'Anak', harga: 69000, rating: 4.1 },
{ judul: 'Buku Mewarnai Hewan Ternak', penulis: 'Putri Anggraini', kategori: 'Anak', harga: 35000, rating: 3.7 },
];Mari kita telaah keputusan-keputusan di berkas ini:
export const BUKU: array diekspor agar module lain bisa mengimpornya, dan dideklarasikan denganconstagar pengikatan namanya tidak bisa diganti. Isinya kita perlakukan sebagai baca saja — seluruh fungsi logika pada bagian-bagian berikutnya mengembalikan array baru, bukan mengubahBUKU.- Satu objek per baris: lima properti masih muat dalam satu baris, sehingga seluruh 22 buku terbaca seperti tabel di layar editor. Bila kolom bertambah di kemudian hari, satu objek boleh dipecah menjadi beberapa baris tanpa mengubah apa pun.
hargadanratingberupa number: harga disimpan sebagai angka utuh (99000), bukan string"Rp99.000". Format tampilan adalah pekerjaan tampilan — logika perlu menghitung dan membandingkan, dan perbandingan string"Rp99.000" > "Rp125.000"menghasilkan urutan yang salah.- Semua judul unik: setiap baris bisa dibedakan dari
judul-nya, sehingga contoh dan uji pada bagian berikutnya selalu merujuk hasil yang jelas.
State Pencarian dalam Fungsi Murni #
Sekarang sisi pengguna. Apa yang perlu diingat program ketika pengguna sedang mencari? Ada teks yang diketik di kotak pencarian, kategori yang dipilih, dan urutan yang diminta. Tiga nilai itu disusun menjadi satu objek state, dan setiap perubahan dikelola oleh fungsi murni di src/daftar.mjs versi pertama:
// daftar.mjs — logika inti pencarian dan filter katalog buku.
// Semua fungsi murni: menerima data atau state, mengembalikan hasil baru,
// tanpa menyentuh DOM sehingga dapat diuji dengan node --test.
export function buatState() {
return { kataKunci: '', kategori: 'semua', urutan: 'judul-asc' };
}
export function ubahKataKunci(state, teks) {
return { ...state, kataKunci: teks };
}
export function ubahKategori(state, kategori) {
return { ...state, kategori };
}
export function ubahUrutan(state, urutan) {
return { ...state, urutan };
}
// Ringkasan satu baris untuk demo dan tampilan sederhana.
export function ringkasBuku(buku) {
return `${buku.judul} — ${buku.penulis} (Rp${buku.harga.toLocaleString('id-ID')})`;
}Mari kita bedah perannya satu per satu:
buatState(): mengembalikan state awal — kolom pencarian kosong, kategorisemua, dan urutanjudul-asc. Nilai-nilai awal ini adalah kontrak antarmuka: kontrol yang belum disentuh pengguna harus persis seperti ini.ubahKataKunci(state, teks): mengembalikan state baru dengankataKunciyang diperbarui. Spread{ ...state }menyalin seluruh properti lama terlebih dahulu, lalu properti yang berubah menimpanya.ubahKategori(state, kategori)danubahUrutan(state, urutan): pola yang sama untuk dua kontrol lain. Ketiga fungsiubah*identik bentuknya — perbedaannya hanya properti yang ditimpa.ringkasBuku(buku): pembantu kecil untuk demo dan tampilan sederhana;toLocaleString('id-ID')memformat99000menjadi99.000sehingga keluaran terbaca seperti harga di Indonesia.
Konvensi terpenting di berkas ini: tidak ada fungsi yang mengubah state lama. Aliran datanya satu arah — state masuk, state baru keluar — persis seperti state kalkulator pada proyek sebelumnya. Manfaatnya terasa langsung pada pengujian: hasil setiap fungsi bisa dibandingkan dengan pasti, dan tidak ada urutan pemanggilan tersembunyi yang mengubah data di belakang layar.
Mencoba State dari Terminal #
Karena module ini bebas DOM, kita bisa membuktikannya bekerja langsung di terminal. Berkas demo-state.mjs berikut ikut diunduh dari bagian ini — jalankan dari folder bagian:
// demo-state.mjs — mencoba state pencarian dari terminal, tanpa browser.
import { BUKU } from './data.mjs';
import {
buatState,
ringkasBuku,
ubahKategori,
ubahKataKunci,
ubahUrutan,
} from './daftar.mjs';
console.log('Jumlah buku :', BUKU.length);
console.log('Buku pertama :', ringkasBuku(BUKU[0]));
let state = buatState();
console.log('State awal :', JSON.stringify(state));
state = ubahKataKunci(state, 'javascript');
state = ubahKategori(state, 'Pemrograman');
state = ubahUrutan(state, 'harga-asc');
console.log('State setelah diubah :', JSON.stringify(state));
console.log('State awal tetap :', JSON.stringify(buatState()));node demo-state.mjsOutput yang Dihasilkan:
Jumlah buku : 22
Buku pertama : Pemrograman JavaScript untuk Pemula — Raka Pratama (Rp99.000)
State awal : {"kataKunci":"","kategori":"semua","urutan":"judul-asc"}
State setelah diubah : {"kataKunci":"javascript","kategori":"Pemrograman","urutan":"harga-asc"}
State awal tetap : {"kataKunci":"","kategori":"semua","urutan":"judul-asc"}Dua baris terakhir adalah inti keluaran ini. State setelah diubah memperlihatkan ketiga updater bekerja: kata kunci terisi, kategori berganti, urutan berubah. State awal tetap membuktikan bahwa buatState() dan seluruh fungsi ubah* tidak menyimpan jejak ke mana pun — memanggilnya kembali selalu menghasilkan objek yang sama persis. Perhatikan juga baris kedua: harga terformat Rp99.000 karena ringkasBuku memakai toLocaleString('id-ID').
Uji Pertama dengan node –test #
Sekarang kita kunci perilaku yang sudah ditulis. Tambahkan berikut ini ke tests/daftar.test.mjs pada folder proyek — berkas uji tumbuh mengikuti pembahasan, dan seluruh uji di bawah tetap ada sampai akhir proyek:
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { BUKU } from '../src/data.mjs';
import {
buatState,
ringkasBuku,
ubahKategori,
ubahKataKunci,
ubahUrutan,
} from '../src/daftar.mjs';
describe('data', () => {
test('katalog berisi 20+ buku dengan lima properti', () => {
assert.ok(BUKU.length >= 20);
for (const buku of BUKU) {
assert.strictEqual(typeof buku.judul, 'string');
assert.strictEqual(typeof buku.penulis, 'string');
assert.strictEqual(typeof buku.kategori, 'string');
assert.strictEqual(typeof buku.harga, 'number');
assert.strictEqual(typeof buku.rating, 'number');
}
});
});
describe('buatState', () => {
test('state awal menampilkan semua kategori terurut judul', () => {
const state = buatState();
assert.strictEqual(state.kataKunci, '');
assert.strictEqual(state.kategori, 'semua');
assert.strictEqual(state.urutan, 'judul-asc');
});
});
describe('perubahan state', () => {
test('ubahKataKunci memperbarui kata kunci', () => {
const state = ubahKataKunci(buatState(), 'javascript');
assert.strictEqual(state.kataKunci, 'javascript');
});
test('ubahKategori dan ubahUrutan mengganti pilihan aktif', () => {
let state = ubahKategori(buatState(), 'Novel');
state = ubahUrutan(state, 'rating-desc');
assert.strictEqual(state.kategori, 'Novel');
assert.strictEqual(state.urutan, 'rating-desc');
});
test('fungsi ubah mengembalikan state baru, bukan mengubah yang lama', () => {
const lama = buatState();
const baru = ubahKataKunci(lama, 'novel');
assert.strictEqual(lama.kataKunci, '');
assert.notStrictEqual(lama, baru);
});
});Jalankan dari folder proyek:
node --testOutput yang Dihasilkan:
▶ data
✔ katalog berisi 20+ buku dengan lima properti (0.647433ms)
✔ data (2.451582ms)
▶ buatState
✔ state awal menampilkan semua kategori terurut judul (0.179734ms)
✔ buatState (0.320204ms)
▶ perubahan state
✔ ubahKataKunci memperbarui kata kunci (0.174315ms)
✔ ubahKategori dan ubahUrutan mengganti pilihan aktif (0.110309ms)
✔ fungsi ubah mengembalikan state baru, bukan mengubah yang lama (0.114889ms)
✔ perubahan state (0.603345ms)
ℹ tests 5
ℹ suites 3
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 75.88896Kelima uji lulus (pass 5, fail 0). Kelompok uji data menjaga bentuk dataset: satu asersi memastikan jumlah bukunya, dan perulangan for...of memeriksa tipe kelima properti pada setiap buku — dengan begitu, kesalahan ketik nama properti di data.mjs tertangkap sejak hari pertama. Uji terakhir di kelompok perubahan state paling penting: assert.notStrictEqual(lama, baru) memastikan ubahKataKunci mengembalikan objek baru, sementara assert.strictEqual(lama.kataKunci, '') memastikan state lama tetap utuh. Angka durasi seperti 0.647433ms berbeda-beda setiap dijalankan karena mengukur waktu mesin Anda.
Ringkasan Bagian 1 #
Fondasi proyek telah berdiri: dataset 22 buku dengan lima properti yang bentuknya dijaga uji, state pencarian tiga nilai (kataKunci, kategori, urutan) yang selalu diganti — bukan diubah — oleh fungsi murni, dan lima uji otomatis yang lulus. State sudah bisa menampung keinginan pengguna, tetapi belum ada yang membaca nilai kataKunci untuk menyaring data. Pekerjaan itu — normalisasi teks dan fungsi pencarian — adalah isi Bagian 2.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
styles/styles.css
Unduh berkas/* styles.css — tampilan katalog buku pencarian dan filter */
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 24px 16px;
background-color: #f3f4f6;
color: #1f2937;
font-family: system-ui, sans-serif;
}
.katalog {
max-width: 640px;
margin: 0 auto;
}
h1 {
margin: 0 0 16px;
font-size: 1.6rem;
}
.panel-kontrol {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 16px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.panel-kontrol label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.85rem;
font-weight: 600;
}
.panel-kontrol input,
.panel-kontrol select {
padding: 8px 10px;
font-size: 1rem;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #ffffff;
font-family: inherit;
}
.kolom-cari {
grid-column: span 2;
}
.status {
margin: 16px 0 8px;
font-size: 0.9rem;
color: #6b7280;
}
.hasil {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 10px;
}
.kartu-buku {
padding: 12px 16px;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.kartu-buku h2 {
margin: 0 0 4px;
font-size: 1.05rem;
}
.kartu-buku .keterangan {
margin: 0 0 6px;
font-size: 0.85rem;
color: #6b7280;
}
.kartu-buku .harga {
margin: 0;
font-weight: 700;
color: #047857;
}