Daftar TODO
Dalam tutorial ini, Anda akan membangun aplikasi daftar TODO yang sepenuhnya fungsional. Ini langkah lebih lanjut dari tutorial Layanan QR Code — Anda akan belajar mengelola state, menangani banyak operasi, dan membuat antarmuka pengguna yang rapi.
Yang akan Anda bangun:
- Aplikasi TODO lengkap dengan fungsi tambah, selesai, dan hapus
- Manajemen state thread-safe (penting untuk aplikasi desktop)
- Desain UI glassmorphic modern
- Semua menggunakan vanilla JavaScript — tidak perlu framework
Yang akan Anda pelajari:
- Operasi CRUD (Create, Read, Update, Delete)
- Mengelola state mutable dengan aman di Go
- Menangani input pengguna dan validasi
- Membangun UI responsif yang terasa native

Waktu penyelesaian: 20 menit
Buat Proyek Anda
Section titled “Buat Proyek Anda”-
Buat proyek
Pertama, buat proyek Wails baru. Kita akan menggunakan template vanilla default yang memberikan titik awal bersih:
Terminal window wails3 init -n todo-appcd todo-appIni membuat proyek baru dengan struktur dasar: backend Go di root, kode frontend di direktori
frontend/. -
Buat layanan TODO
Layanan TODO akan mengelola state aplikasi kita dan menyediakan metode untuk operasi CRUD. Berbeda dengan web server di mana setiap permintaan terisolasi, aplikasi desktop dapat memiliki banyak operasi konkuren, jadi kita perlu manajemen state thread-safe.
Hapus
greetservice.godan buat file barutodoservice.go:todoservice.go package mainimport ("errors""sync")type Todo struct {ID int `json:"id"`Title string `json:"title"`Completed bool `json:"completed"`}type TodoService struct {todos []TodonextID intmu sync.RWMutex}func NewTodoService() *TodoService {return &TodoService{todos: []Todo{},nextID: 1,}}func (t *TodoService) GetAll() []Todo {t.mu.RLock()defer t.mu.RUnlock()return t.todos}func (t *TodoService) Add(title string) (*Todo, error) {if title == "" {return nil, errors.New("title cannot be empty")}t.mu.Lock()defer t.mu.Unlock()todo := Todo{ID: t.nextID,Title: title,Completed: false,}t.todos = append(t.todos, todo)t.nextID++return &todo, nil}func (t *TodoService) Toggle(id int) error {t.mu.Lock()defer t.mu.Unlock()for i := range t.todos {if t.todos[i].ID == id {t.todos[i].Completed = !t.todos[i].Completedreturn nil}}return errors.New("todo not found")}func (t *TodoService) Delete(id int) error {t.mu.Lock()defer t.mu.Unlock()for i, todo := range t.todos {if todo.ID == id {t.todos = append(t.todos[:i], t.todos[i+1:]...)return nil}}return errors.New("todo not found")}Apa yang terjadi di sini:
Struct
Todo:- Mendefinisikan bentuk data dengan field ID, Title, dan Completed
- Tag
json:memberi tahu Go cara mengonversi struct ini ke JSON untuk frontend - Setiap field diekspor (kapital) agar generator binding dapat melihatnya
Struct
TodoService:todos []Todo— slice yang menampung semua item TODOnextID int— melacak ID berikutnya yang akan ditetapkan (mensimulasikan auto-increment)mu sync.RWMutex— read/write mutex untuk akses thread-safe
Thread safety dengan
sync.RWMutex:- Aplikasi desktop dapat memiliki banyak operasi konkuren dari UI
RLock()memungkinkan banyak reader sekaligus (mis. beberapa pemanggilanGetAll)Lock()memberikan akses eksklusif untuk write (mis.Add,Toggle,Delete)defermemastikan lock dilepas meskipun fungsi return lebih awal atau panic
Metode:
GetAll()— Mengembalikan semua todo (menggunakan read lock karena tidak memodifikasi data)Add(title)— Membuat todo baru, memvalidasi input, menaikkan IDToggle(id)— Membalik status completed todoDelete(id)— Menghapus todo dari slice
Penanganan error:
- Kita mengembalikan
errorsebagai nilai terakhir mengikuti konvensi Go - Judul kosong ditolak
- Operasi pada todo yang tidak ada mengembalikan error
- Error ini menjadi exception JavaScript di frontend
-
Perbarui main.go
Daftarkan layanan TODO ke aplikasi Wails Anda. Temukan bagian
Servicesdimain.godan ganti GreetService dengan TodoService kita:main.go Services: []application.Service{application.NewService(NewTodoService()),},Apa yang terjadi di sini:
- Kita menghapus GreetService default dan menambahkan TodoService kita
application.NewService()membungkus service agar Wails dapat mengelolanya- Wails akan otomatis membuat binding JavaScript untuk semua metode publik pada service ini
-
Buat UI frontend
Sekarang mari bangun frontend. Di sini kita akan memanggil metode Go dan menampilkan UI. Kita menggunakan vanilla JavaScript agar sederhana dan menunjukkan cara kerja binding secara langsung.
Ganti
frontend/src/main.js:frontend/src/main.js import {TodoService} from "../bindings/changeme";async function loadTodos() {const todos = await TodoService.GetAll();const list = document.getElementById('todo-list');list.innerHTML = todos.map(todo => `<div class="todo ${todo.completed ? 'completed' : ''}"><input type="checkbox"${todo.completed ? 'checked' : ''}onchange="toggleTodo(${todo.id})"><span>${todo.title}</span><button onclick="deleteTodo(${todo.id})">Delete</button></div>`).join('');}window.addTodo = async () => {const input = document.getElementById('todo-input');const title = input.value.trim();if (title) {await TodoService.Add(title);input.value = '';await loadTodos();}}window.toggleTodo = async (id) => {await TodoService.Toggle(id);await loadTodos();}window.deleteTodo = async (id) => {await TodoService.Delete(id);await loadTodos();}// Load todos on startuploadTodos();Apa yang terjadi di sini:
Mengimpor binding:
import {TodoService} from "../bindings/changeme"— membawa binding Go yang dibuat otomatis- Catatan:
changemeakan menjadi nama modul aktual Anda darigo.mod
Fungsi
loadTodos():- Memanggil
TodoService.GetAll()untuk mengambil semua todo dari Go - Membangun HTML untuk setiap todo menggunakan template literal
- Menambah/menghapus class
completedsecara dinamis untuk styling - Menggunakan atribut
onclickuntuk menghubungkan tombol ke fungsi kita - Menggabungkan semua HTML dan menyuntikkannya ke DOM
Fungsi CRUD:
addTodo()— Memvalidasi input, memanggil metode GoAdd, me-refresh daftartoggleTodo(id)— Memanggil metode GoToggle, me-refresh daftardeleteTodo(id)— Memanggil metode GoDelete, me-refresh daftar- Semua fungsi async karena pemanggilan Go mengembalikan Promise
Mengapa attach ke window:
window.addTodo = ...membuat fungsi dapat diakses dari atributonclickHTML- Ini pola sederhana untuk vanilla JS (framework menanganinya berbeda)
- Di produksi, Anda mungkin menggunakan event delegation yang proper
Pola refresh:
- Setelah setiap mutasi (add/toggle/delete), kita panggil
loadTodos()lagi - Ini memastikan UI tetap sinkron dengan state Go
- Alternatif: Buat metode Go mengembalikan state baru untuk menghindari pemanggilan kedua
-
Perbarui HTML
HTML menyediakan struktur untuk aplikasi TODO kita. Minimal dan semantik — keajaiban terjadi di JavaScript dan CSS.
Ganti
frontend/index.html:frontend/index.html <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>TODO App</title><link rel="stylesheet" href="./style.css"/></head><body><div class="container"><h1>My TODOs</h1><div class="card"><div class="input-box"><input type="text"id="todo-input"class="input"placeholder="Add a new todo..."onkeypress="if(event.key==='Enter') addTodo()"><button class="btn" onclick="addTodo()">Add</button></div><div id="todo-list"></div></div></div><script type="module" src="./src/main.js"></script></body></html>Apa yang terjadi di sini:
Struktur:
container— menengahkan aplikasi dan membatasi lebarcard— kartu putih utama yang menampung semuanyainput-box— container flex untuk input dan tombol Addtodo-list— tempat todo individual disuntikkan oleh JavaScript
Penanganan event:
onkeypress="if(event.key==='Enter') addTodo()"— tambah todo saat Enter ditekanonclick="addTodo()"— tambah todo saat tombol diklik- Inline event handler bekerja baik untuk aplikasi vanilla JS sederhana
Module script:
<script type="module">memungkinkan kita menggunakan import ES6- File
main.jskita dapat mengimpor binding dan menggunakan JavaScript modern
-
Style aplikasi
CSS membuat desain glassmorphic modern dengan transisi halus. Kita menargetkan nuansa rapi yang membuat aplikasi menyenangkan digunakan.
Ganti
frontend/public/style.css:frontend/public/style.css :root {font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto","Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",sans-serif;font-size: 16px;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: rgba(255, 255, 255, 0.87);}body {margin: 0;display: flex;place-items: center;justify-content: center;min-height: 100vh;}.container {width: 100%;max-width: 600px;padding: 20px;}h1 {text-align: center;color: white;font-size: 2.5em;font-weight: 300;margin: 0 0 30px 0;text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);}.card {background: rgba(255, 255, 255, 0.95);backdrop-filter: blur(10px);border-radius: 16px;padding: 30px;box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);}.input-box {display: flex;gap: 10px;margin-bottom: 25px;}.input {flex: 1;border: 2px solid #e0e0e0;border-radius: 12px;height: 50px;padding: 0 20px;font-size: 16px;transition: all 0.3s ease;}.input:focus {border-color: #667eea;outline: none;box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);}.btn {height: 50px;padding: 0 30px;border: none;border-radius: 12px;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: white;font-weight: 600;cursor: pointer;transition: all 0.3s ease;box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);}.btn:hover {transform: translateY(-2px);box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);}#todo-list {display: flex;flex-direction: column;gap: 10px;}.todo {display: flex;align-items: center;padding: 18px 20px;background: white;border: 2px solid #f0f0f0;border-radius: 12px;transition: all 0.3s ease;gap: 15px;}.todo:hover {border-color: #667eea;box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);transform: translateX(4px);}.todo.completed {opacity: 0.6;}.todo.completed span {text-decoration: line-through;color: #999;}.todo input[type="checkbox"] {width: 24px;height: 24px;cursor: pointer;appearance: none;-webkit-appearance: none;border: 2px solid #667eea;border-radius: 6px;position: relative;transition: all 0.3s ease;flex-shrink: 0;}.todo input[type="checkbox"]:hover {background: rgba(102, 126, 234, 0.1);}.todo input[type="checkbox"]:checked {background: #667eea;border-color: #667eea;}.todo input[type="checkbox"]:checked::after {content: '✓';position: absolute;color: white;font-size: 16px;font-weight: bold;top: 50%;left: 50%;transform: translate(-50%, -50%);}.todo span {flex: 1;font-size: 16px;color: #333;}.todo button {padding: 8px 16px;background: #ff4757;color: white;border: none;border-radius: 8px;font-size: 14px;font-weight: 600;cursor: pointer;transition: all 0.3s ease;opacity: 0;flex-shrink: 0;}.todo:hover button {opacity: 1;}.todo button:hover {background: #ee5a6f;transform: scale(1.05);}#todo-list:empty::before {content: "No todos yet. Add one above!";display: block;text-align: center;padding: 40px 20px;color: #999;}Apa yang terjadi di sini:
Desain glassmorphic:
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)— latar belakang gradien ungubackdrop-filter: blur(10px)— efek kaca buram pada karturgba(255, 255, 255, 0.95)— putih semi-transparan untuk efek kaca
Styling checkbox kustom:
appearance: nonemenghapus checkbox browser default- Kita membuat kotak bulat kustom dengan centang menggunakan
::after - Centang muncul saat
checkedmenggunakan karakter unicode ✓
Interaksi hover:
- Todo bergeser ke kanan saat hover (
transform: translateX(4px)) - Tombol hapus tersembunyi sampai hover (
opacity: 0→opacity: 1) - Tombol sedikit membesar saat hover untuk feedback taktil
Empty state:
#todo-list:empty::beforemenampilkan pesan saat tidak ada todo- Solusi CSS-only — tidak perlu JavaScript
-
Jalankan aplikasi
Mari lihat hasilnya! Jalankan development server:
Terminal window wails3 devAplikasi akan dikompilasi dan terbuka. Coba:
- Ketik todo dan tekan Enter atau klik Add
- Klik checkbox untuk menandai selesai
- Arahkan kursor ke todo untuk melihat tombol hapus muncul
- Perhatikan UI diperbarui instan — itu pola refresh kita bekerja
Apa yang terjadi:
- Wails otomatis membuat binding untuk metode TodoService Anda
- Mode dev termasuk hot reload — coba ubah CSS dan lihat pembaruannya
- Kode Go Anda berjalan secara native — tidak perlu translasi atau interpretasi
Cara Kerjanya
Section titled “Cara Kerjanya”Manajemen State Thread-Safe
Section titled “Manajemen State Thread-Safe”sync.RWMutex menyediakan akses konkuren yang aman:
func (t *TodoService) GetAll() []Todo { t.mu.RLock() // Read lock - multiple readers allowed defer t.mu.RUnlock() return t.todos}
func (t *TodoService) Add(title string) (*Todo, error) { t.mu.Lock() // Write lock - exclusive access defer t.mu.Unlock() // ... mutations}Mengapa ini penting:
- Beberapa pemanggilan frontend dapat terjadi secara konkuren
- Operasi baca tidak saling memblokir
- Operasi tulis mendapat akses eksklusif
defermemastikan lock selalu dilepas
Penanganan Error
Section titled “Penanganan Error”Service mengembalikan error untuk operasi tidak valid:
func (t *TodoService) Add(title string) (*Todo, error) { if title == "" { return nil, errors.New("title cannot be empty") } // ...}Di frontend, Anda dapat menangkapnya:
try { await TodoService.Add(title);} catch (err) { alert('Error: ' + err);}Sinkronisasi State
Section titled “Sinkronisasi State”Setelah setiap mutasi, kita muat ulang daftar lengkap:
window.addTodo = async () => { await TodoService.Add(title); // Mutation await loadTodos(); // Refresh}Pendekatan alternatif: Kembalikan daftar yang diperbarui dari setiap metode untuk menghindari pemanggilan kedua.
Peningkatan
Section titled “Peningkatan”Tambahkan Statistik
Section titled “Tambahkan Statistik”Tambahkan ini ke todoservice.go:
type TodoStats struct { Total int `json:"total"` Completed int `json:"completed"` Active int `json:"active"`}
func (t *TodoService) GetStats() TodoStats { t.mu.RLock() defer t.mu.RUnlock()
stats := TodoStats{ Total: len(t.todos), }
for _, todo := range t.todos { if todo.Completed { stats.Completed++ } else { stats.Active++ } }
return stats}Tampilkan di frontend:
async function loadTodos() { const [todos, stats] = await Promise.all([ TodoService.GetAll(), TodoService.GetStats() ]);
// Display stats document.getElementById('stats').textContent = `${stats.active} active, ${stats.completed} completed`;
// ... render todos}Tambahkan “Clear Completed”
Section titled “Tambahkan “Clear Completed””func (t *TodoService) ClearCompleted() int { t.mu.Lock() defer t.mu.Unlock()
removed := 0 newTodos := []Todo{}
for _, todo := range t.todos { if !todo.Completed { newTodos = append(newTodos, todo) } else { removed++ } }
t.todos = newTodos return removed}Tambahkan Persistensi
Section titled “Tambahkan Persistensi”Untuk aplikasi produksi, Anda biasanya menambahkan persistensi database. Lihat panduan Integrasi Database untuk contoh dengan SQLite, PostgreSQL, dll.
Build untuk Produksi
Section titled “Build untuk Produksi”Saat siap mendistribusikan aplikasi TODO Anda, build untuk produksi:
wails3 buildIni membuat executable native yang dioptimalkan di bin/:
- Mengompilasi kode Go dengan optimasi
- Membangun frontend untuk produksi
- Mengemas semuanya ke satu executable
- Aplikasi hasilnya biasanya 10-20MB (bandingkan dengan Electron 150MB+)
Anda dapat menjalankan executable langsung — tidak perlu runtime, tidak perlu server. Ini aplikasi native sejati.
Yang Anda Bangun
Section titled “Yang Anda Bangun”Anda baru saja membangun aplikasi TODO lengkap dengan:
Implementasi CRUD penuh:
- Membuat service dengan operasi Create, Read, Update, dan Delete
- Menambahkan validasi input dan penanganan error
- Memahami bagaimana error Go menjadi exception JavaScript
Manajemen state thread-safe:
- Menggunakan
sync.RWMutexuntuk menangani akses konkuren dengan aman - Memahami perbedaan antara read lock (RLock) dan write lock (Lock)
- Melihat bagaimana
defermencegah deadlock dengan menjamin cleanup
UI modern dan rapi:
- Membangun antarmuka glassmorphic dengan gradien dan efek blur
- Membuat checkbox bergaya kustom tanpa framework
- Menambahkan interaksi hover dan transisi untuk nuansa native
- Mengimplementasikan empty state dengan CSS murni
Dasar-dasar Wails:
- Pendaftaran service dan pembuatan binding otomatis
- Memanggil metode Go dari JavaScript dengan async/await
- Sinkronisasi state antara Go dan frontend
- Membangun dan mengemas aplikasi desktop native
Langkah Selanjutnya
Section titled “Langkah Selanjutnya”Setelah memahami operasi CRUD dan manajemen state, coba:
- Tambahkan persistensi: Buat todo bertahan setelah restart aplikasi dengan SQLite
- Tambahkan fitur lain: Filtering (all/active/completed), mengedit todo yang ada, operasi bulk
- Jelajahi tutorial Notes: Lihat cara kerja operasi file di Notes
- Bangun sesuatu yang nyata: Ambil konsep ini dan bangun aplikasi Anda sendiri!