//go:build js && wasm package main import ( "fmt" "syscall/js" "git.flowmade.one/flowmade-one/iris/reactive" "git.flowmade.one/flowmade-one/iris/ui" ) // Todo represents a single todo item type Todo struct { ID int Text string Completed bool } func main() { // Signal holding the list of todos todos := reactive.NewSignal([]Todo{}) // Signal for the input field inputText := reactive.NewSignal("") // Counter for generating unique IDs nextID := reactive.NewSignal(1) // Create the main app view view := ui.VerticalGroup( // Title ui.TextFromString("Todo List").Padding("16px 0"), // Input row: text input + add button inputRow(&inputText, func() { text := inputText.Get() if text == "" { return } // Add new todo id := nextID.Get() nextID.Set(id + 1) currentTodos := todos.Get() newTodos := append(currentTodos, Todo{ ID: id, Text: text, Completed: false, }) todos.Set(newTodos) // Clear input inputText.Set("") }), // Todo list todoList(&todos), // Summary text showing completed count ui.TextFromFunction(func() string { allTodos := todos.Get() if len(allTodos) == 0 { return "No todos yet" } completed := 0 for _, t := range allTodos { if t.Completed { completed++ } } return fmt.Sprintf("%d of %d completed", completed, len(allTodos)) }).Padding("16px 0"), ).MaxWidth("400px").Padding("24px") ui.NewApp(view) select {} } // inputRow creates the input field and add button func inputRow(inputText *reactive.Signal[string], onAdd func()) ui.View { row := ui.NewView() row.Display("flex").Gap("8px").Width("100%") // Text input input := ui.TextInput(inputText, "What needs to be done?") input.Width("100%") // Handle Enter key to add todo input.Element().OnWithEvent("keypress", func(event js.Value) { if event.Get("key").String() == "Enter" { onAdd() } }) // Add button addBtn := ui.Button(onAdd, ui.TextFromString("Add")) addBtn.Padding("8px 16px") row.Child(input) row.Child(addBtn) return row } // todoList renders the list of todos reactively func todoList(todos *reactive.Signal[[]Todo]) ui.View { container := ui.NewView() container.Width("100%") // Effect that re-renders the list when todos change reactive.NewEffect(func() { allTodos := todos.Get() // Clear existing children container.Element().ClearChildren() // Render each todo item for _, todo := range allTodos { item := todoItem(todo, todos) container.Child(item) } // Show empty state if no todos if len(allTodos) == 0 { emptyText := ui.TextFromString("Add your first todo above") emptyText.Color("#666").Padding("16px 0").TextAlign("center") container.Child(emptyText) } }) return container } // todoItem renders a single todo item with toggle and delete func todoItem(todo Todo, todos *reactive.Signal[[]Todo]) ui.View { row := ui.NewView() row.Display("flex").Gap("12px").Padding("8px 0").Width("100%").AlignItems("center") row.BorderBottom("1px solid #eee") // Completed checkbox using public ui.RawCheckbox checkbox := ui.RawCheckbox(todo.Completed, func(isChecked bool) { toggleTodo(todos, todo.ID, isChecked) }) // Todo text with strikethrough if completed textView := ui.NewView() textView.Display("flex").Width("100%") text := ui.Span(todo.Text) if todo.Completed { text.TextDecoration("line-through").Color("#999") } textView.Child(text) // Delete button deleteBtn := ui.Button(func() { deleteTodo(todos, todo.ID) }, ui.TextFromString("X")) deleteBtn.Padding("4px 8px") row.Child(checkbox) row.Child(textView) row.Child(deleteBtn) return row } // toggleTodo updates the completed status of a todo func toggleTodo(todos *reactive.Signal[[]Todo], id int, completed bool) { currentTodos := todos.Get() newTodos := make([]Todo, len(currentTodos)) for i, t := range currentTodos { if t.ID == id { newTodos[i] = Todo{ID: t.ID, Text: t.Text, Completed: completed} } else { newTodos[i] = t } } todos.Set(newTodos) } // deleteTodo removes a todo from the list func deleteTodo(todos *reactive.Signal[[]Todo], id int) { currentTodos := todos.Get() newTodos := make([]Todo, 0, len(currentTodos)) for _, t := range currentTodos { if t.ID != id { newTodos = append(newTodos, t) } } todos.Set(newTodos) }