--- title: TODO List description: Build a complete TODO list application with CRUD operations sidebar: order: 2 --- import { Tabs, TabItem, Steps } from "@astrojs/starlight/components"; import { Image } from 'astro:assets'; import todoApp from "../../../assets/todo-app.png"; In this tutorial, you'll build a fully functional TODO list application. This is a step up from the QR Code Service tutorial - you'll learn how to manage state, handle multiple operations, and create a polished user interface. **What you'll build:** - A complete TODO app with add, complete, and delete functionality - Thread-safe state management (important for desktop apps) - Modern, glassmorphic UI design - All using vanilla JavaScript - no frameworks required **What you'll learn:** - CRUD operations (Create, Read, Update, Delete) - Managing mutable state safely in Go - Handling user input and validation - Building responsive UIs that feel native TODO List Application **Time to complete:** 20 minutes ## Create Your Project 1. **Generate the project** First, create a new Wails project. We'll use the default vanilla template which gives us a clean starting point: ```bash wails3 init -n todo-app cd todo-app ``` This creates a new project with the basic structure: Go backend in the root, frontend code in the `frontend/` directory. 2. **Create the TODO service** The TODO service will manage our application state and provide methods for CRUD operations. Unlike a web server where each request is isolated, desktop apps can have multiple concurrent operations, so we need thread-safe state management. Delete `greetservice.go` and create a new file `todoservice.go`: ```go title="todoservice.go" package main import ( "errors" "sync" ) type Todo struct { ID int `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } type TodoService struct { todos []Todo nextID int mu 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].Completed return 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") } ``` **What's happening here:** **The `Todo` struct:** - Defines the shape of our data with ID, Title, and Completed fields - `json:` tags tell Go how to convert this struct to JSON for the frontend - Each field is exported (capitalized) so the bindings generator can see it **The `TodoService` struct:** - `todos []Todo` - a slice holding all our TODO items - `nextID int` - tracks the next ID to assign (simulates auto-increment) - `mu sync.RWMutex` - a read/write mutex for thread-safe access **Thread safety with `sync.RWMutex`:** - Desktop apps can have multiple concurrent operations from the UI - `RLock()` allows multiple readers at once (e.g., multiple `GetAll` calls) - `Lock()` gives exclusive access for writes (e.g., `Add`, `Toggle`, `Delete`) - `defer` ensures locks are released even if the function returns early or panics **The methods:** - `GetAll()` - Returns all todos (uses read lock since we're not modifying data) - `Add(title)` - Creates a new todo, validates input, increments ID - `Toggle(id)` - Flips the completed status of a todo - `Delete(id)` - Removes a todo from the slice **Error handling:** - We return `error` as the last value following Go conventions - Empty titles are rejected - Operations on non-existent todos return errors - These errors become JavaScript exceptions in the frontend 3. **Update main.go** Register the TODO service with your Wails application. Find the `Services` section in `main.go` and replace the GreetService with our TodoService: ```go title="main.go" {5} Services: []application.Service{ application.NewService(NewTodoService()), }, ``` **What's happening here:** - We're removing the default GreetService and adding our TodoService instead - `application.NewService()` wraps our service so Wails can manage it - Wails will automatically generate JavaScript bindings for all public methods on this service 4. **Create the frontend UI** Now let's build the frontend. This is where we'll call our Go methods and display the UI. We're using vanilla JavaScript to keep things simple and show you how the bindings work directly. Replace `frontend/src/main.js`: ```javascript title="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 => `
${todo.title}
`).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 startup loadTodos(); ``` **What's happening here:** **Importing the bindings:** - `import {TodoService} from "../bindings/changeme"` - brings in the auto-generated Go bindings - Note: `changeme` will be your actual module name from `go.mod` **The `loadTodos()` function:** - Calls `TodoService.GetAll()` to fetch all todos from Go - Builds HTML for each todo using template literals - Dynamically adds/removes the `completed` class for styling - Uses `onclick` attributes to connect buttons to our functions - Joins all the HTML together and injects it into the DOM **The CRUD functions:** - `addTodo()` - Validates input, calls the Go `Add` method, refreshes the list - `toggleTodo(id)` - Calls Go's `Toggle` method, refreshes the list - `deleteTodo(id)` - Calls Go's `Delete` method, refreshes the list - All functions are async because Go calls return Promises **Why attach to window:** - `window.addTodo = ...` makes functions accessible from HTML `onclick` attributes - This is a simple pattern for vanilla JS (frameworks handle this differently) - In production, you might use proper event delegation instead **The refresh pattern:** - After each mutation (add/toggle/delete), we call `loadTodos()` again - This ensures the UI stays in sync with the Go state - Alternative: Have Go methods return the new state to avoid the second call 5. **Update the HTML** The HTML provides the structure for our TODO app. It's minimal and semantic - the magic happens in the JavaScript and CSS. Replace `frontend/index.html`: ```html title="frontend/index.html" TODO App

My TODOs

``` **What's happening here:** **The structure:** - `container` - centers our app and constrains the width - `card` - the main white card holding everything - `input-box` - flex container for the input and Add button - `todo-list` - where individual todos will be injected by JavaScript **Event handling:** - `onkeypress="if(event.key==='Enter') addTodo()"` - add todo when Enter is pressed - `onclick="addTodo()"` - add todo when button is clicked - Inline event handlers work well for simple vanilla JS apps **Module script:** - `