Topik ini mengeksekusi rencana Papan Catatan, dan proyeknya adalah isi topik itu sendiri. Urutan membangunnya dijaga tetap, yaitu lapisan inti lebih dahulu sebagai modul ES murni, lalu diuji di Node.js sebelum satu pun tampilan ditulis, dilanjutkan lapisan penyimpanan, dan diakhiri lapisan UI yang merangkai semuanya menjadi aplikasi yang berjalan di browser. Di akhir topik, aplikasi catatan lengkap dengan bintang, pencarian, dan simpan otomatis dapat dibuka dari folder proyek, dan logika intinya telah lulus pengujian otomatis.
Pohon berkas dan urutan membangun#
Proyek Papan Catatan berukuran enam berkas, dan setiap berkas memikul satu tanggung jawab.
papan-catatan/
├── inti-catatan.mjs logika inti sebagai modul ES murni
├── uji-inti.mjs pengujian inti dengan node:assert
├── penyimpanan.js lapisan penyimpanan berbasis localStorage
├── aplikasi.js aplikasi browser, hasil rangkaian tiga lapis
├── index.html struktur halaman
└── index.css penataan tampilanUrutan membangunnya mengikuti arah ketergantungan antar lapis, bukan urutan tampilan. Pertama, inti-catatan.mjs ditulis sebagai kumpulan fungsi murni yang tidak menyentuh DOM maupun localStorage. Kedua, uji-inti.mjs memeriksa seluruh perilaku inti dan dijalankan di Node.js, sehingga cacat logika tertangkap sebelum ada tampilan yang menutupinya. Ketiga, penyimpanan.js mengurus muat dan simpan sebagai satu-satunya pemakai localStorage. Keempat, aplikasi.js beserta halamannya menggambar semuanya. Dengan urutan ini, setiap lapis baru selalu berdiri di atas lapis yang sudah teruji.
Proyek ini sengaja tanpa package.json dan tanpa satu pun dependensi, karena segala yang dibutuhkannya sudah ada di browser dan di Node.js. Folder papan-catatan dapat disalin ke tempat mana pun dan langsung dijalankan tanpa langkah instalasi. Nama berkasnya juga memakai kata Indonesia sesuai perannya, inti untuk logika, penyimpanan untuk akses data, dan aplikasi untuk perakitannya. Pada proyek yang lebih besar, nama berkas yang menjelaskan isinya menjadi peta pertama saat mencari kode.
Lapisan inti sebagai modul ES#
// Logika inti Papan Catatan: fungsi murni yang mengolah daftar catatan.
// Satu catatan adalah object biasa: { id, judul, isi, penting, dibuat }.
// Tidak ada satu pun fungsi di sini yang menyentuh DOM atau localStorage,
// sehingga seluruhnya dapat diuji langsung di Node.js.
// Mengembalikan id berikutnya, yaitu id terbesar pada daftar ditambah satu.
export function idBaru(daftar) {
const terbesar = daftar.reduce((maks, catatan) => Math.max(maks, catatan.id), 0);
return terbesar + 1;
}
// Mengembalikan daftar baru dengan satu catatan tambahan di posisi akhir.
// Waktu pembuatan diberikan pemanggil agar fungsi tetap murni.
export function buatCatatan(daftar, judul, isi, waktu) {
const catatan = {
id: idBaru(daftar),
judul: judul,
isi: isi,
penting: false,
dibuat: waktu,
};
return [...daftar, catatan];
}
// Mengembalikan daftar baru dengan bintang catatan yang dituju terbalik.
export function tandaiPenting(daftar, id) {
return daftar.map((catatan) => {
if (catatan.id === id) {
return { ...catatan, penting: !catatan.penting };
}
return catatan;
});
}
// Mengembalikan daftar baru tanpa catatan yang id-nya dituju.
export function hapusCatatan(daftar, id) {
return daftar.filter((catatan) => catatan.id !== id);
}
// Mengembalikan catatan yang judulnya atau isinya memuat kueri.
// Perbandingan tidak memandang huruf besar-kecil, dan kueri kosong
// mengembalikan seluruh daftar.
export function cariCatatan(daftar, kueri) {
const pola = kueri.trim().toLowerCase();
if (pola === "") {
return [...daftar];
}
return daftar.filter(
(catatan) =>
catatan.judul.toLowerCase().includes(pola) ||
catatan.isi.toLowerCase().includes(pola)
);
}
// Mengembalikan daftar baru yang tertata: catatan penting lebih dahulu,
// lalu diikuti catatan terbaru menurut bidang dibuat.
export function urutkan(daftar) {
return [...daftar].sort((a, b) => {
if (a.penting !== b.penting) {
return a.penting ? -1 : 1;
}
if (a.dibuat > b.dibuat) {
return -1;
}
if (a.dibuat < b.dibuat) {
return 1;
}
return 0;
});
}Modul ini mengekspor enam fungsi yang seluruhnya murni, sesuai antarmuka yang disepakati pada perencanaan. Fungsi idBaru mengembalikan id terbesar pada daftar ditambah satu, sehingga id baru selalu melanjutkan urutan yang ada. Fungsi buatCatatan menerima waktu pembuatan dari pemanggilnya alih-alih membaca jam sendiri, karena membaca jam adalah efek samping yang membuat hasil fungsi berubah-ubah dan sulit diuji.
Fungsi tandaiPenting membalik bintang lewat map dan spread tanpa menyentuh object lama, sedangkan hapusCatatan menyaring lewat filter. Bila kuerinya kosong, cariCatatan mengembalikan salinan seluruh daftar. Fungsi urutkan bekerja pada salinan [...daftar] sebelum diurutkan, sehingga urutan daftar aslinya tidak pernah berubah.
Satu sifat modul ini penting dicatat, yaitu memuatnya tidak menimbulkan efek apa pun. Seluruh kode di dalamnya berada di dalam fungsi, sehingga proses import hanya menyiapkan enam nama yang siap dipanggil, tanpa menyentuh layar dan tanpa membaca jam. Kebiasaan ini menjaga modul tetap dapat dipakai di lingkungan mana pun, baik Node.js untuk pengujian maupun bagian inti aplikasi.js yang dirangkai untuk browser. Bila kelak perilaku baru dibutuhkan, misalnya menyunting isi catatan, fungsinya ditambahkan di sini lebih dahulu, diuji, lalu dirangkai ulang ke aplikasi.js.
Menguji inti di Node.js#
// tidak-dijalankan
// Uji logika inti Papan Catatan dengan modul node:assert.
// Berkas ini dijalankan dari folder proyek bersama inti-catatan.mjs:
// node uji-inti.mjs
import assert from "node:assert/strict";
import {
buatCatatan,
tandaiPenting,
hapusCatatan,
cariCatatan,
urutkan,
idBaru,
} from "./inti-catatan.mjs";
// 1. Id pertama untuk daftar yang masih kosong.
assert.equal(idBaru([]), 1);
console.log("lulus: id pertama adalah 1 bila daftar masih kosong");
// 2. Id berikutnya melanjutkan id terbesar, bukan panjang daftar.
const adaCelah = [
{ id: 1, judul: "a", isi: "", penting: false, dibuat: "2026-11-01T08:00:00.000Z" },
{ id: 4, judul: "b", isi: "", penting: false, dibuat: "2026-11-01T09:00:00.000Z" },
];
assert.equal(idBaru(adaCelah), 5);
console.log("lulus: id berikutnya melanjutkan id terbesar pada daftar");
// 3. buatCatatan menambah satu catatan dan membiarkan daftar lama utuh.
const awal = [];
const hasil = buatCatatan(awal, "Rapat pagi", "Siapkan anggaran.", "2026-11-02T08:00:00.000Z");
assert.equal(hasil.length, 1);
assert.equal(hasil[0].id, 1);
assert.equal(hasil[0].penting, false);
assert.equal(hasil[0].dibuat, "2026-11-02T08:00:00.000Z");
assert.deepEqual(awal, []);
console.log("lulus: buatCatatan menambah catatan tanpa mengubah daftar lama");
// 4. tandaiPenting membalik bintang dan bisa dibalik kembali.
const satu = [{ id: 2, judul: "b", isi: "", penting: false, dibuat: "2026-11-01T08:00:00.000Z" }];
const berbintang = tandaiPenting(satu, 2);
assert.equal(berbintang[0].penting, true);
assert.equal(satu[0].penting, false);
assert.equal(tandaiPenting(berbintang, 2)[0].penting, false);
console.log("lulus: tandaiPenting membalik bintang tanpa mengubah data lama");
// 5. hapusCatatan membuang catatan yang dituju dan hanya itu.
const tiga = [
{ id: 1, judul: "a", isi: "", penting: false, dibuat: "2026-11-01T08:00:00.000Z" },
{ id: 2, judul: "b", isi: "", penting: true, dibuat: "2026-11-01T09:00:00.000Z" },
{ id: 3, judul: "c", isi: "", penting: false, dibuat: "2026-11-02T08:00:00.000Z" },
];
const sisa = hapusCatatan(tiga, 2);
assert.deepEqual(sisa.map((catatan) => catatan.id), [1, 3]);
assert.equal(tiga.length, 3);
console.log("lulus: hapusCatatan membuang catatan dengan id yang dituju");
// 6. cariCatatan memeriksa judul dan isi tanpa memandang huruf besar-kecil,
// dan kueri kosong mengembalikan seluruh daftar.
const koleksi = [
{ id: 1, judul: "Rapat pagi", isi: "Siapkan anggaran.", penting: false, dibuat: "2026-11-01T08:00:00.000Z" },
{ id: 2, judul: "Belanja", isi: "Kopi dan gula.", penting: false, dibuat: "2026-11-01T09:00:00.000Z" },
];
assert.deepEqual(cariCatatan(koleksi, "rapat").map((catatan) => catatan.id), [1]);
assert.deepEqual(cariCatatan(koleksi, "KOPI").map((catatan) => catatan.id), [2]);
assert.deepEqual(cariCatatan(koleksi, "anggaran").map((catatan) => catatan.id), [1]);
assert.equal(cariCatatan(koleksi, " ").length, 2);
console.log("lulus: cariCatatan mencari judul dan isi tanpa memandang huruf besar-kecil");
// 7. urutkan menaruh catatan penting lebih dahulu, lalu yang terbaru.
const campur = [
{ id: 1, judul: "a", isi: "", penting: false, dibuat: "2026-11-01T08:00:00.000Z" },
{ id: 2, judul: "b", isi: "", penting: true, dibuat: "2026-11-01T07:00:00.000Z" },
{ id: 3, judul: "c", isi: "", penting: false, dibuat: "2026-11-02T09:00:00.000Z" },
];
assert.deepEqual(urutkan(campur).map((catatan) => catatan.id), [2, 3, 1]);
assert.equal(campur[0].id, 1);
console.log("lulus: urutkan menaruh catatan penting lebih dahulu lalu yang terbaru");Tujuh pemeriksaan di atas memakai node:assert/strict dan menutup perilaku yang paling mudah rusak, yaitu id pertama, id setelah celah, penambahan tanpa mutasi, pembalikan bintang, penghapusan, pencarian tak peduli huruf besar-kecil, dan urutan penting lalu terbaru. Pemeriksaan assert.deepEqual(awal, []) dan penghitungan ulang panjang daftar tiga sengaja ada untuk memastikan fungsi inti tidak pernah mengubah daftar masukannya. Jalankan pengujian dari folder proyek.
# jalankan pengujian inti dari folder proyek
node uji-inti.mjslulus: id pertama adalah 1 bila daftar masih kosong
lulus: id berikutnya melanjutkan id terbesar pada daftar
lulus: buatCatatan menambah catatan tanpa mengubah daftar lama
lulus: tandaiPenting membalik bintang tanpa mengubah data lama
lulus: hapusCatatan membuang catatan dengan id yang dituju
lulus: cariCatatan mencari judul dan isi tanpa memandang huruf besar-kecil
lulus: urutkan menaruh catatan penting lebih dahulu lalu yang terbaruKeluaran itu adalah hasil nyata Node.js 24, dan setiap barisnya berasal dari satu kelompok pemeriksaan yang lolos. Kegagalan assert menghentikan berkas dan menampilkan pesan error yang menunjuk baris pemeriksanya, sehingga letak kesalahannya langsung terbaca. Berkas ujinya sendiri adalah modul yang mengimpor modul lain, jadi latihan ini sekaligus melatih rantai import antar berkas modul.
Pemeriksaan assert.deepEqual membandingkan struktur data, bukan referensinya, sehingga dua array berisi object yang sama persis dianggap setara meskipun alamatnya berbeda di memori. Sifat inilah yang membuat jaminan tanpa mutasi dapat diperiksa secara mekanis, karena mutasi tersembunyi akan mengubah struktur yang sedang dibandingkan. Tanpa pengujian seperti ini, cacat mutasi justru paling sering terlihat di tampilan, pada bentuk daftar yang tiba-tiba urutannya berubah tanpa sebab yang jelas.
Lapisan penyimpanan#
// tidak-dijalankan
// Lapisan penyimpanan Papan Catatan: memuat dan menyimpan daftar catatan
// ke localStorage sebagai string JSON.
const KUNCI_CATATAN = "papan-catatan";
// Mengembalikan daftar catatan dari localStorage, atau daftar kosong
// bila belum ada data, data rusak, atau penyimpanan diblokir.
function muatCatatan() {
try {
const teks = localStorage.getItem(KUNCI_CATATAN);
if (teks === null) {
return [];
}
const data = JSON.parse(teks);
return Array.isArray(data) ? data : [];
} catch (error) {
return [];
}
}
// Menyimpan daftar catatan ke localStorage. Mengembalikan true bila
// berhasil dan false bila penyimpanan tidak dapat digunakan.
function simpanCatatan(daftar) {
try {
localStorage.setItem(KUNCI_CATATAN, JSON.stringify(daftar));
return true;
} catch (error) {
return false;
}
}Lapisan ini hanya berisi dua fungsi dan satu konstanta nama kunci. Fungsi muatCatatan mengembalikan daftar kosong untuk tiga keadaan sekaligus: kunci belum pernah disimpan, isi JSON rusak sehingga JSON.parse melempar error, dan penyimpanan diblokir oleh lingkungannya seperti pada mode privat. Fungsi simpanCatatan mengembalikan boolean yang dipakai lapisan UI untuk menampilkan keterangan bila penulisan gagal, misalnya karena kuota penuh. Dengan kontrak sederhana itu, halaman tidak pernah ikut error gara-gara penyimpanan, dan seluruh penanganan errornya terpusat di satu berkas.
Nama kuncinya diberi awalan nama aplikasi, sehingga isinya tidak bertabrakan dengan kunci aplikasi lain bila origin yang sama suatu saat memuat proyek kecil lainnya. Daftar disimpan utuh sebagai satu kunci, bukan per catatan, karena jumlah datanya masih kecil dan pembongkaran JSON sekali jalan jauh lebih sederhana daripada membongkar tiap butir. Bila kelak penyimpanannya diganti ke IndexedDB, hanya dua fungsi di berkas ini yang ditulis ulang, selama kontraknya tetap dijaga, yaitu muatCatatan mengembalikan daftar dan simpanCatatan mengembalikan boolean.
Lapisan UI dan halaman#
// tidak-dijalankan
// Aplikasi Papan Catatan untuk browser.
// Demo halaman berjalan sebagai satu berkas skrip tanpa server dan tanpa
// langkah build, sehingga ketiga lapis dirangkai di sini: fungsi inti yang
// sama dengan inti-catatan.mjs, lapisan penyimpanan yang sama dengan
// penyimpanan.js, lalu lapisan UI yang menggambar halaman.
// ===== Lapisan inti: fungsi murni tanpa DOM dan tanpa localStorage =====
// Mengembalikan id berikutnya, yaitu id terbesar pada daftar ditambah satu.
function idBaru(daftar) {
const terbesar = daftar.reduce((maks, catatan) => Math.max(maks, catatan.id), 0);
return terbesar + 1;
}
// Mengembalikan daftar baru dengan satu catatan tambahan di posisi akhir.
// Waktu pembuatan diberikan pemanggil agar fungsi tetap murni.
function buatCatatan(daftar, judul, isi, waktu) {
const catatan = {
id: idBaru(daftar),
judul: judul,
isi: isi,
penting: false,
dibuat: waktu,
};
return [...daftar, catatan];
}
// Mengembalikan daftar baru dengan bintang catatan yang dituju terbalik.
function tandaiPenting(daftar, id) {
return daftar.map((catatan) => {
if (catatan.id === id) {
return { ...catatan, penting: !catatan.penting };
}
return catatan;
});
}
// Mengembalikan daftar baru tanpa catatan yang id-nya dituju.
function hapusCatatan(daftar, id) {
return daftar.filter((catatan) => catatan.id !== id);
}
// Mengembalikan catatan yang judulnya atau isinya memuat kueri.
// Perbandingan tidak memandang huruf besar-kecil, dan kueri kosong
// mengembalikan seluruh daftar.
function cariCatatan(daftar, kueri) {
const pola = kueri.trim().toLowerCase();
if (pola === "") {
return [...daftar];
}
return daftar.filter(
(catatan) =>
catatan.judul.toLowerCase().includes(pola) ||
catatan.isi.toLowerCase().includes(pola)
);
}
// Mengembalikan daftar baru yang tertata: catatan penting lebih dahulu,
// lalu diikuti catatan terbaru menurut bidang dibuat.
function urutkan(daftar) {
return [...daftar].sort((a, b) => {
if (a.penting !== b.penting) {
return a.penting ? -1 : 1;
}
if (a.dibuat > b.dibuat) {
return -1;
}
if (a.dibuat < b.dibuat) {
return 1;
}
return 0;
});
}
// ===== Lapisan penyimpanan: localStorage dengan JSON dan try/catch =====
const KUNCI_CATATAN = "papan-catatan";
// Mengembalikan daftar catatan dari localStorage, atau daftar kosong
// bila belum ada data, data rusak, atau penyimpanan diblokir.
function muatCatatan() {
try {
const teks = localStorage.getItem(KUNCI_CATATAN);
if (teks === null) {
return [];
}
const data = JSON.parse(teks);
return Array.isArray(data) ? data : [];
} catch (error) {
return [];
}
}
// Menyimpan daftar catatan ke localStorage. Mengembalikan true bila
// berhasil dan false bila penyimpanan tidak dapat digunakan.
function simpanCatatan(daftar) {
try {
localStorage.setItem(KUNCI_CATATAN, JSON.stringify(daftar));
return true;
} catch (error) {
return false;
}
}
// ===== Lapisan UI: DOM, event, dan penyimpanan otomatis =====
const daftarElemen = document.querySelector("#daftar");
const kosongElemen = document.querySelector("#kosong");
const statusElemen = document.querySelector("#status");
const formTambah = document.querySelector("#form-tambah");
const judulMasukan = document.querySelector("#judul");
const isiMasukan = document.querySelector("#isi");
const cariMasukan = document.querySelector("#cari");
let daftarCatatan = muatCatatan();
let penundaSimpan = null;
// Menunda penulisan ke localStorage selama 300 ms sejak perubahan
// terakhir, sehingga serangkaian perubahan hanya ditulis satu kali.
function jadwalkanSimpan() {
clearTimeout(penundaSimpan);
penundaSimpan = setTimeout(() => {
const tersimpan = simpanCatatan(daftarCatatan);
statusElemen.textContent = tersimpan
? ""
: "Penyimpanan tidak tersedia, perubahan hanya bertahan selama halaman terbuka.";
}, 300);
}
// Menggambar ulang seluruh daftar dari data. Isi catatan ditulis lewat
// textContent sehingga teks tidak pernah dibaca sebagai markah.
function render() {
const tampil = urutkan(cariCatatan(daftarCatatan, cariMasukan.value));
daftarElemen.textContent = "";
for (const catatan of tampil) {
const butir = document.createElement("li");
butir.className = catatan.penting ? "catatan penting" : "catatan";
const judul = document.createElement("span");
judul.className = "judul";
judul.textContent = catatan.judul;
const isi = document.createElement("span");
isi.className = "isi";
isi.textContent = catatan.isi;
const waktu = document.createElement("time");
waktu.textContent = new Date(catatan.dibuat).toLocaleDateString("id-ID", {
day: "numeric",
month: "short",
year: "numeric",
});
const bintang = document.createElement("button");
bintang.type = "button";
bintang.className = "bintang";
bintang.dataset.id = String(catatan.id);
bintang.textContent = catatan.penting ? "★" : "☆";
const hapus = document.createElement("button");
hapus.type = "button";
hapus.className = "hapus";
hapus.dataset.id = String(catatan.id);
hapus.textContent = "Hapus";
butir.append(judul, isi, waktu, bintang, hapus);
daftarElemen.append(butir);
}
kosongElemen.hidden = tampil.length > 0;
}
// Satu pendengar untuk seluruh daftar: tombol bintang membalik tanda
// penting, tombol Hapus membuang catatannya.
daftarElemen.addEventListener("click", (event) => {
const tombol = event.target.closest("button");
if (tombol === null) {
return;
}
const id = Number(tombol.dataset.id);
if (tombol.classList.contains("bintang")) {
daftarCatatan = tandaiPenting(daftarCatatan, id);
} else {
daftarCatatan = hapusCatatan(daftarCatatan, id);
}
jadwalkanSimpan();
render();
});
// Menambah catatan baru dari form, lalu mereset kolom isian.
formTambah.addEventListener("submit", (event) => {
event.preventDefault();
const judul = judulMasukan.value.trim();
if (judul === "") {
judulMasukan.focus();
return;
}
daftarCatatan = buatCatatan(
daftarCatatan,
judul,
isiMasukan.value.trim(),
new Date().toISOString()
);
judulMasukan.value = "";
isiMasukan.value = "";
jadwalkanSimpan();
render();
});
// Pencarian menggambar ulang daftar pada setiap ketukan tombol.
cariMasukan.addEventListener("input", render);
render();Bagian UI-nya mengikuti pola data yang sama dengan inti. Variabel daftarCatatan menjadi satu-satunya sumber data, dan setiap perubahan selalu berupa penugasan hasil fungsi inti, bukan pengubahan isinya. Fungsi render menggambar ulang seluruh daftar dari data tersebut, dan isi catatan ditulis lewat textContent sehingga ketikan pengguna tidak pernah dibaca sebagai markah HTML. Satu pendengar click pada wadah daftar melayani seluruh tombol bintang dan hapus lewat event delegation, sementara form menjalankan preventDefault agar browser tidak memuat ulang halaman dan menghapus state.
Sisi asinkron aplikasi ini ada pada simpan otomatisnya. Fungsi jadwalkanSimpan menunda penulisan ke localStorage selama 300 ms sejak perubahan terakhir lewat setTimeout, sehingga serangkaian perubahan yang terjadi berdekatan hanya ditulis satu kali di akhir penundaan. Bila penulisan itu gagal karena penyimpanan diblokir, pesan penggantinya ditampilkan pada elemen status dan aplikasi tetap berjalan.
Struktur halamannya memasang elemen-elemen yang dirujuk bagian UI, dan penataannya disimpan terpisah.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Papan Catatan</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<main class="papan">
<h1>Papan Catatan</h1>
<form id="form-tambah" autocomplete="off">
<input id="judul" type="text" placeholder="Judul catatan" maxlength="60">
<textarea id="isi" rows="2" placeholder="Isi catatan"></textarea>
<button type="submit">Tambah</button>
</form>
<input id="cari" type="search" placeholder="Cari catatan">
<p id="status" class="status" role="status"></p>
<ul id="daftar"></ul>
<p id="kosong" class="kosong">Belum ada catatan.</p>
</main>
<script src="aplikasi.js"></script>
</body>
</html>/* Penataan ringkas Papan Catatan. */
* { box-sizing: border-box; }
body {
font-family: "Segoe UI", Arial, sans-serif;
font-size: 14px;
color: #1f2937;
background: #f8fafc;
margin: 0;
}
.papan {
max-width: 420px;
margin: 0 auto;
padding: 14px;
}
h1 {
font-size: 20px;
margin: 0 0 10px;
}
#form-tambah {
display: grid;
gap: 6px;
margin-bottom: 8px;
}
#form-tambah input,
#form-tambah textarea,
#cari {
padding: 7px 9px;
border: 1px solid #94a3b8;
border-radius: 6px;
font: inherit;
width: 100%;
}
#form-tambah textarea { resize: vertical; }
#form-tambah button {
justify-self: start;
padding: 7px 14px;
border: 0;
border-radius: 6px;
background: #2563eb;
color: #fff;
font: inherit;
cursor: pointer;
}
#cari { margin-bottom: 4px; }
.status { min-height: 16px; margin: 2px 0; font-size: 12px; color: #b45309; }
#daftar { list-style: none; margin: 0; padding: 0; }
.catatan {
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 8px 10px;
margin-bottom: 8px;
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: 8px;
align-items: center;
}
.catatan.penting { border-color: #f59e0b; background: #fffbeb; }
.catatan .judul { font-weight: 600; }
.catatan .isi { color: #475569; grid-column: 1; }
.catatan time { font-size: 12px; color: #64748b; }
.catatan button {
padding: 4px 9px;
border: 1px solid #94a3b8;
border-radius: 6px;
background: #f1f5f9;
font: inherit;
cursor: pointer;
}
.catatan .bintang { border-color: #f59e0b; background: #fffbeb; font-size: 14px; }
.kosong { color: #64748b; }Demo berikut memperagakan aplikasi utuhnya. Tambahkan dua catatan, bintangi salah satunya, lalu ketik pada kotak pencarian dan amati daftar yang tertata, catatan penting lebih dahulu. Bila lingkungan yang dipakai memblokir penyimpanan, baris keterangan menampilkan pesan penggantinya dan aplikasi tetap berjalan.
Kesalahan umum#
Empat kekeliruan berikut paling sering muncul saat proyek semacam ini dikerjakan, dan semuanya cepat dikenali dari gejalanya.
- Menjalankan pengujian dari folder yang salah. Node.js mencari
uji-inti.mjsdi folder kerja, sehingga perintahnode uji-inti.mjsdari luar folder proyek berakhir dengan errorCannot find module. Jalankan perintahnya dari folder proyek. Import"./inti-catatan.mjs"mengikuti letak berkas ujinya, bukan folder kerja. - Mengubah daftarCatatan langsung dengan push atau splice. Tampilan dan penyimpanan hanya ikut berubah bila render dan jadwalkanSimpan dipanggil, dan keduanya bekerja lewat penugasan hasil fungsi inti. Pengubahan langsung membuat layar dan localStorage tinggal menampilkan data lama.
- Membaca isi catatan lewat innerHTML. Isi catatan berasal dari ketikan pengguna, dan innerHTML membaca teks itu sebagai markah sehingga membuka celah penyuntikan skrip. Seluruh render memakai textContent, dan kebiasaan yang sama perlu dijaga saat tampilannya dikembangkan.
- Menyunting satu salinan inti saja. Fungsi inti hidup di inti-catatan.mjs untuk pengujian dan bagian inti aplikasi.js untuk demo. Perbaikan yang hanya menyentuh salah satunya membuat keduanya tidak konsisten. Terapkan setiap perubahan inti ke kedua tempat sebelum menguji ulang.
Ringkasan#
- Proyeknya enam berkas, dibangun berurutan dari inti murni, pengujian Node.js, penyimpanan, sampai UI beserta halamannya.
- uji-inti.mjs memeriksa tujuh perilaku inti dengan
node:assert/strict, termasuk jaminan bahwa daftar masukan tidak pernah diubah. - Lapisan penyimpanan memusatkan seluruh akses localStorage beserta penanganan errornya, dan mengembalikan hasil yang dapat diperiksa lapisan UI.
- Lapisan UI menggambar dari satu sumber data, menyimpan otomatis dengan penundaan setTimeout 300 ms, dan selamat di lingkungan yang memblokir penyimpanan.
Referensi#
- Assertion testing — Node.js — dokumentasi modul assert yang dipakai uji-inti.mjs beserta mode strict-nya.
- Window: localStorage — MDN — dokumentasi API yang dipakai lapisan penyimpanan.
- setTimeout() global function — MDN — dokumentasi penundaan eksekusi yang dipakai simpan otomatis.