diff --git a/examples/todo/main.go b/examples/todo/main.go new file mode 100644 index 0000000..46806bd --- /dev/null +++ b/examples/todo/main.go @@ -0,0 +1,205 @@ +//go:build js && wasm + +package main + +import ( + "fmt" + "syscall/js" + + "git.flowmade.one/flowmade-one/iris/internal/element" + "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 + checkbox := element.NewElement("input") + checkbox.Attr("type", "checkbox") + if todo.Completed { + checkbox.Set("checked", true) + } + checkbox.On("change", func() { + isChecked := checkbox.Get("checked").Bool() + toggleTodo(todos, todo.ID, isChecked) + }) + checkboxView := ui.NewViewFromElement(checkbox) + + // Todo text with strikethrough if completed + textView := ui.NewView() + textView.Display("flex").Width("100%") + text := element.NewElement("span") + text.Set("textContent", todo.Text) + if todo.Completed { + text.Get("style").Call("setProperty", "text-decoration", "line-through") + text.Get("style").Call("setProperty", "color", "#999") + } + textView.Child(ui.NewViewFromElement(text)) + + // Delete button + deleteBtn := ui.Button(func() { + deleteTodo(todos, todo.ID) + }, ui.TextFromString("X")) + deleteBtn.Padding("4px 8px") + + row.Child(checkboxView) + 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) +}