JavaScript

Membuat Aplikasi Catatan dengan JavaScript Tanpa Framework

Rusmawan Abdullah Sani
Rusmawan Abdullah Sani
Diterbitkan:
Diperbarui:
18

Belajar membuat aplikasi catatan (notes app) lengkap dengan JavaScript Vanilla tanpa library atau framework. Dilengkapi diagram alur CRUD, API Cheatsheet, live interactive demo, dan localStorage.

Terakhir diperbarui:

Bisakah Membuat Aplikasi Catatan dengan JavaScript Tanpa Framework?

Direct Answer:Ya, sepenuhnya bisa. Dengan JavaScript Vanilla (tanpa React, Vue, atau Angular), Anda dapat membangun aplikasi catatan berfitur penuh — termasuk operasi CRUD (Create, Read, Update, Delete), penyimpanan data persisten via localStorage, pencarian real-time, filter kategori, mode gelap, hingga ekspor catatan ke format .txt — hanya dengan HTML, CSS, dan JavaScript murni.

Tutorial ini dirancang untuk pemula hingga menengah dan cocok dijadikan portofolio JavaScript yang mengesankan.

Mulai Eksplorasi & Uji Coba Proyek

Uji coba aplikasi interaktif secara langsung atau download source code lengkapnya.

⚡ Ringkasan Fungsi Utama (JavaScript API Cheatsheet)

Tabel referensi cepat fungsi dan method inti JavaScript murni yang digunakan sepanjang tutorial ini:

Method / PropertiKategoriDeskripsi SingkatContoh Implementasi
localStorage.setItem()Web StorageMenyimpan string data ke penyimpanan browser klienlocalStorage.setItem("notes", JSON.stringify(notes))
localStorage.getItem()Web StorageMengambil data tersimpan berdasarkan nama keyconst raw = localStorage.getItem("notes")
JSON.stringify()JSON SerializerMengonversi objek atau array JS menjadi string JSONconst str = JSON.stringify(data)
JSON.parse()JSON ParserMengonversi string JSON kembali ke objek/array JSconst arr = JSON.parse(str) || []
Array.prototype.unshift()State MutationMenyisipkan elemen baru di urutan paling awalnotes.unshift(newNote)
Array.prototype.filter()Array QueryMenyaring catatan sesuai kata kunci / menghapus IDnotes.filter(n => n.id !== targetId)
Array.prototype.find()Array QueryMencari satu catatan spesifik berdasarkan IDnotes.find(n => n.id === targetId)
Element.innerHTMLDOM SyncMenyuntikkan HTML hasil template literal secara massalcontainer.innerHTML = htmlString
escapeHtml()Web SecuritySanitasi karakter rentan untuk mencegah injeksi XSSescapeHtml(userInput)

🎮 Live Demo Interaktif di Dalam Artikel

Anda dapat langsung mencoba fitur tambah, cari, filter kategori, edit, dan hapus catatan pada widget interaktif di bawah ini:

Live Embedded Sandbox • NotesApp
Client-Side Only

📊 Diagram Alur CRUD dan Siklus Data localStorage

Memahami bagaimana data bergerak di antara antarmuka pengguna, memori JavaScript, dan browser storage:

Infografis: CRUD Execution Flow & LocalStorage State Lifecycle
Vector SVG • Optimized
Diagram alur kerja CRUD dan siklus data persisten localStorage pada JavaScript Vanilla
Gambar 1: Alur lengkap lifecycle catatan dari trigger event pengguna, validasi XSS, manipulasi state memori, serialisasi Web Storage API, hingga sinkronisasi UI DOM.

🗒️ Fitur Aplikasi Catatan yang Akan Dibangun

Berikut daftar fitur lengkap yang ada di aplikasi:

Tambah Catatan Baru
Buat catatan dengan judul, konten, dan tag/kategori.
Edit Catatan
Ubah isi catatan yang sudah ada dengan in-place editor.
Hapus Catatan
Hapus catatan individual dengan konfirmasi dialog.
Pencarian Real-Time
Filter catatan secara instan tanpa reload halaman.
Simpan ke localStorage
Data tersimpan permanen di browser tanpa backend.
Dark Mode
Toggle tema gelap/terang yang disimpan ke localStorage.
Export ke .txt
Unduh catatan sebagai file teks dengan satu klik.
Filter per Kategori
Kelompokkan catatan berdasarkan tag yang ditentukan.

📁 Struktur Folder Proyek

Buat folder baru bernama notes-app/ dengan 3 file utama:

notes-app/
├── index.html
├── style.css
└── app.js

1. Membuat File HTML (index.html)

File index.html sebagai struktur antarmuka yang bersih dan semantik:

index.html
HTML5
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>📒 Aplikasi Catatan – JavaScript Tanpa Framework</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="app-wrapper">
    <header class="app-header">
      <h1>📒 NotesApp</h1>
      <button id="btn-toggle-theme" title="Toggle Dark Mode">🌙</button>
    </header>
    <div class="toolbar">
      <input type="text" id="input-search" placeholder="🔍 Cari catatan..." autocomplete="off">
      <select id="select-filter">
        <option value="all">Semua Kategori</option>
        <option value="Pribadi">Pribadi</option>
        <option value="Kerja">Kerja</option>
        <option value="Belajar">Belajar</option>
        <option value="Ide">Ide</option>
      </select>
      <button id="btn-add-note">+ Catatan Baru</button>
    </div>
    <p class="note-count" id="note-count">0 catatan</p>
    <div id="modal-overlay" class="modal-overlay hidden">
      <div class="modal">
        <h2 id="modal-title">Catatan Baru</h2>
        <input type="text" id="note-title" placeholder="Judul catatan..." maxlength="100">
        <select id="note-category">
          <option value="Pribadi">Pribadi</option>
          <option value="Kerja">Kerja</option>
          <option value="Belajar">Belajar</option>
          <option value="Ide">Ide</option>
        </select>
        <textarea id="note-content" rows="6" placeholder="Tulis catatan Anda di sini..."></textarea>
        <div class="modal-actions">
          <button id="btn-save-note">💾 Simpan</button>
          <button id="btn-cancel-modal" class="btn-secondary">Batal</button>
        </div>
      </div>
    </div>
    <div id="notes-container" class="notes-grid"></div>
    <div id="empty-state" class="empty-state hidden">
      <p>📭 Belum ada catatan. Klik "+ Catatan Baru" untuk mulai!</p>
    </div>
  </div>
  <script src="app.js"></script>
</body>
</html>

2. Membuat File CSS (style.css)

CSS terstruktur dengan CSS Custom Properties untuk tema gelap/terang instan:

style.css
CSS3
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  --bg: #f0f4f8; --surface: #ffffff; --surface-hover: #f8fafc;
  --border: #e2e8f0; --text-primary: #1e293b; --text-secondary: #64748b;
  --accent: #3b82f6; --accent-hover: #2563eb; --danger: #ef4444; --success: #10b981;
  --shadow: 0 2px 8px rgba(0,0,0,0.08); --shadow-lg: 0 8px 24px rgba(0,0,0,0.12); --radius: 12px;
}
[data-theme="dark"] {
  --bg: #0f172a; --surface: #1e293b; --surface-hover: #273548;
  --border: #334155; --text-primary: #f1f5f9; --text-secondary: #94a3b8;
  --shadow: 0 2px 8px rgba(0,0,0,0.4); --shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
}
body { background: var(--bg); color: var(--text-primary); font-family: "Segoe UI", system-ui, -apple-system, sans-serif; font-size: 15px; line-height: 1.6; min-height: 100vh; transition: background 0.3s, color 0.3s; }
.app-wrapper { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
.app-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; }
.app-header h1 { font-size: 1.5rem; font-weight: 800; }
#btn-toggle-theme { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.4rem 0.8rem; cursor: pointer; font-size: 1.1rem; }
.toolbar { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.toolbar input, .toolbar select { flex: 1; min-width: 160px; padding: 0.6rem 0.9rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text-primary); font-size: 0.875rem; outline: none; }
#btn-add-note { padding: 0.6rem 1.2rem; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 0.875rem; }
.note-count { font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; }
.notes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.note-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 0.5rem; }
.note-card h3 { font-size: 0.95rem; font-weight: 700; color: var(--text-primary); word-break: break-word; }
.note-card .note-body { font-size: 0.82rem; color: var(--text-secondary); line-height: 1.55; max-height: 80px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; }
.note-card .note-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.note-card .note-badge { font-size: 0.65rem; font-weight: 700; padding: 0.2rem 0.6rem; border-radius: 999px; background: rgba(59,130,246,0.15); color: var(--accent); }
.note-card .note-actions { display: flex; gap: 0.4rem; }
.note-card .note-actions button { padding: 0.3rem 0.6rem; font-size: 0.75rem; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-hover); cursor: pointer; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 1rem; }
.modal-overlay.hidden { display: none; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 1.75rem; width: 100%; max-width: 480px; display: flex; flex-direction: column; gap: 0.9rem; }
.modal input, .modal select, .modal textarea { width: 100%; padding: 0.65rem 0.9rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text-primary); font-size: 0.9rem; outline: none; }
.modal-actions { display: flex; gap: 0.75rem; justify-content: flex-end; }
#btn-save-note { padding: 0.6rem 1.4rem; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-weight: 700; cursor: pointer; }
.btn-secondary { padding: 0.6rem 1.2rem; background: var(--surface-hover); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
.empty-state { text-align: center; padding: 4rem 1rem; color: var(--text-secondary); }
.empty-state.hidden { display: none; }

3. Membuat File JavaScript (app.js)

Logika lengkap CRUD, localStorage, filter, pencarian, dan proteksi XSS:

app.js
JavaScript Vanilla
// State & Variabel Global
let notes = [];
let editingId = null;

// Inisialisasi Aplikasi
document.addEventListener('DOMContentLoaded', () => {
  loadNotes(); loadTheme(); bindEvents(); renderNotes();
});

// Operasi LocalStorage
function loadNotes() {
  const stored = localStorage.getItem('notesapp_data');
  notes = stored ? JSON.parse(stored) : [];
}
function saveNotes() {
  localStorage.setItem('notesapp_data', JSON.stringify(notes));
}

// Tema Gelap/Terang
function loadTheme() {
  const saved = localStorage.getItem('notesapp_theme') || 'light';
  document.documentElement.setAttribute('data-theme', saved);
  document.getElementById('btn-toggle-theme').textContent = saved === 'dark' ? '☀️' : '🌙';
}
function toggleTheme() {
  const html = document.documentElement;
  const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  html.setAttribute('data-theme', next);
  localStorage.setItem('notesapp_theme', next);
  document.getElementById('btn-toggle-theme').textContent = next === 'dark' ? '☀️' : '🌙';
}

// Event Listeners
function bindEvents() {
  document.getElementById('btn-toggle-theme').addEventListener('click', toggleTheme);
  document.getElementById('btn-add-note').addEventListener('click', openModalAdd);
  document.getElementById('btn-save-note').addEventListener('click', saveNote);
  document.getElementById('btn-cancel-modal').addEventListener('click', closeModal);
  document.getElementById('input-search').addEventListener('input', renderNotes);
  document.getElementById('select-filter').addEventListener('change', renderNotes);
  document.getElementById('modal-overlay').addEventListener('click', (e) => {
    if (e.target.id === 'modal-overlay') closeModal();
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeModal();
    if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); openModalAdd(); }
  });
}

// Modal Control
function openModalAdd() {
  editingId = null;
  document.getElementById('modal-title').textContent = 'Catatan Baru';
  document.getElementById('note-title').value = '';
  document.getElementById('note-content').value = '';
  document.getElementById('note-category').value = 'Pribadi';
  document.getElementById('modal-overlay').classList.remove('hidden');
  document.getElementById('note-title').focus();
}
function openModalEdit(id) {
  const note = notes.find(n => n.id === id);
  if (!note) return;
  editingId = id;
  document.getElementById('modal-title').textContent = 'Edit Catatan';
  document.getElementById('note-title').value = note.title;
  document.getElementById('note-content').value = note.content;
  document.getElementById('note-category').value = note.category;
  document.getElementById('modal-overlay').classList.remove('hidden');
  document.getElementById('note-title').focus();
}
function closeModal() {
  document.getElementById('modal-overlay').classList.add('hidden');
  editingId = null;
}

// Simpan Catatan (Create & Update)
function saveNote() {
  const title = document.getElementById('note-title').value.trim();
  const content = document.getElementById('note-content').value.trim();
  const category = document.getElementById('note-category').value;
  if (!title || !content) { alert('⚠️ Judul dan isi catatan wajib diisi!'); return; }

  if (editingId !== null) {
    const idx = notes.findIndex(n => n.id === editingId);
    if (idx > -1) notes[idx] = { ...notes[idx], title, content, category, updatedAt: new Date().toISOString() };
  } else {
    notes.unshift({ id: Date.now(), title, content, category, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
  }
  saveNotes(); closeModal(); renderNotes();
}

// Hapus Catatan (Delete)
function deleteNote(id) {
  if (!confirm('🗑️ Hapus catatan ini secara permanen?')) return;
  notes = notes.filter(n => n.id !== id);
  saveNotes(); renderNotes();
}

// Ekspor ke .TXT
function exportNote(id) {
  const note = notes.find(n => n.id === id);
  if (!note) return;
  const text = `📒 ${note.title}\nKategori: ${note.category}\n---\n${note.content}`;
  const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = `${note.title.toLowerCase().replace(/\s+/g, '-')}.txt`;
  a.click();
  URL.revokeObjectURL(a.href);
}

// Render Data ke DOM
function renderNotes() {
  const query = document.getElementById('input-search').value.toLowerCase().trim();
  const category = document.getElementById('select-filter').value;
  const container = document.getElementById('notes-container');
  const empty = document.getElementById('empty-state');

  const filtered = notes.filter(n => {
    const matchSearch = !query || n.title.toLowerCase().includes(query) || n.content.toLowerCase().includes(query);
    const matchCat = category === 'all' || n.category === category;
    return matchSearch && matchCat;
  });

  document.getElementById('note-count').textContent = `${filtered.length} catatan`;
  if (filtered.length === 0) { container.innerHTML = ''; empty.classList.remove('hidden'); return; }
  empty.classList.add('hidden');

  container.innerHTML = filtered.map(n => `
    <article class="note-card" data-id="${n.id}">
      <h3>${escapeHtml(n.title)}</h3>
      <p class="note-body">${escapeHtml(n.content)}</p>
      <div class="note-footer">
        <span class="note-badge">${escapeHtml(n.category)}</span>
        <div class="note-actions">
          <button onclick="openModalEdit(${n.id})" title="Edit">✏️</button>
          <button onclick="exportNote(${n.id})" title="Export">📥</button>
          <button onclick="deleteNote(${n.id})" title="Hapus">🗑️</button>
        </div>
      </div>
    </article>
  `).join('');
}

// Keamanan: XSS Sanitization
function escapeHtml(str) {
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}

✅ Kesimpulan

Anda telah berhasil membangun aplikasi catatan JavaScript berfitur lengkap tanpa menggunakan satu pun framework atau library eksternal. Proyek ini membuktikan bahwa:

  • JavaScript Vanilla lebih dari cukup untuk aplikasi web fungsional dan modern
  • localStorage adalah solusi penyimpanan client-side yang mudah dan efektif
  • Pemahaman mendalam tentang DOM, event, dan array methods adalah fondasi kuat sebelum belajar framework
  • Keamanan (escapeHtml) harus selalu menjadi prioritas bahkan di proyek kecil sekalipun

FAQ: Pertanyaan Umum

Apakah data catatan hilang jika browser ditutup?

Tidak. Aplikasi menggunakan localStorage yang bersifat persisten — data tetap tersimpan meski browser ditutup atau komputer dimatikan. Data hanya hilang jika Anda menghapus data browsing atau menghapus catatan secara manual.

Bisakah aplikasi ini dipakai di perangkat mobile?

Bisa! CSS menggunakan Flexbox dan Grid dengan breakpoint responsif. Tampilan otomatis menyesuaikan layar smartphone maupun tablet.

Apakah perlu koneksi internet untuk menjalankan aplikasi ini?

Tidak perlu sama sekali. Aplikasi ini berjalan 100% di browser (client-side) tanpa memerlukan server atau koneksi internet. Semua proses terjadi secara lokal di perangkat Anda.

Bagaimana cara menyimpan catatan secara online/cloud?

Untuk penyimpanan cloud, Anda perlu backend (API). Bisa menggunakan Firebase Realtime Database (gratis untuk proyek kecil) atau membuat REST API sendiri dengan Node.js/PHP dan mengganti fungsi localStorage dengan fetch() ke API tersebut.

Rusmawan Abdullah Sani

Rusmawan Abdullah Sani

DevOps Engineer & Lead Developer at infokoding
Profil LinkedIn

Praktisi pengembangan web, DevOps, dan keamanan jaringan server Linux dengan pengalaman mengelola infrastruktur cloud server berskala produksi. Berfokus membagikan panduan teknis mendalam tentang administrasi server, otomasi deployment, dan tutorial programming di infokoding.com.