Todo List Example
A comprehensive todo list application that demonstrates advanced TachUI patterns including state management, form handling, filtering, and persistence.
Overview
This example showcases:
- Complex state management with multiple signals
- Form handling with validation
- List manipulation (add, remove, update)
- Filtering and search functionality
- Local storage persistence
- Component composition with reusable components
- Advanced styling with conditional modifiers
Live Demo
typescript
import {
createSignal,
createMemo,
createEffect,
Text,
Button,
TextField,
VStack,
HStack,
Form,
Toggle,
renderComponent
} from '@tachui/core'
interface Todo {
id: number
text: string
completed: boolean
createdAt: Date
priority: 'low' | 'medium' | 'high'
}
function TodoApp() {
// State management
const [todos, setTodos] = createSignal<Todo[]>([])
const [newTodoText, setNewTodoText] = createSignal('')
const [filter, setFilter] = createSignal<'all' | 'active' | 'completed'>('all')
const [searchQuery, setSearchQuery] = createSignal('')
const [nextId, setNextId] = createSignal(1)
// Load todos from localStorage on startup
createEffect(() => {
const saved = localStorage.getItem('tachui-todos')
if (saved) {
try {
const parsed = JSON.parse(saved)
setTodos(parsed.map(todo => ({
...todo,
createdAt: new Date(todo.createdAt)
})))
setNextId(Math.max(...parsed.map(t => t.id), 0) + 1)
} catch (error) {
console.error('Failed to load todos:', error)
}
}
})
// Save todos to localStorage when they change
createEffect(() => {
localStorage.setItem('tachui-todos', JSON.stringify(todos()))
})
// Computed values
const filteredTodos = createMemo(() => {
let filtered = todos()
// Apply search filter
const query = searchQuery().toLowerCase()
if (query) {
filtered = filtered.filter(todo =>
todo.text.toLowerCase().includes(query)
)
}
// Apply status filter
const currentFilter = filter()
if (currentFilter === 'active') {
filtered = filtered.filter(todo => !todo.completed)
} else if (currentFilter === 'completed') {
filtered = filtered.filter(todo => todo.completed)
}
return filtered.sort((a, b) => {
// Sort by priority first, then by creation date
const priorityOrder = { high: 3, medium: 2, low: 1 }
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
return priorityOrder[b.priority] - priorityOrder[a.priority]
}
return b.createdAt.getTime() - a.createdAt.getTime()
})
})
const stats = createMemo(() => ({
total: todos().length,
active: todos().filter(t => !t.completed).length,
completed: todos().filter(t => t.completed).length
}))
// Actions
const addTodo = () => {
const text = newTodoText().trim()
if (!text) return
const newTodo: Todo = {
id: nextId(),
text,
completed: false,
createdAt: new Date(),
priority: 'medium'
}
setTodos(prev => [...prev, newTodo])
setNewTodoText('')
setNextId(prev => prev + 1)
}
const toggleTodo = (id: number) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
const deleteTodo = (id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}
const updateTodoText = (id: number, newText: string) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
))
}
const updateTodoPriority = (id: number, priority: Todo['priority']) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, priority } : todo
))
}
const clearCompleted = () => {
setTodos(prev => prev.filter(todo => !todo.completed))
}
const toggleAll = () => {
const allCompleted = todos().every(todo => todo.completed)
setTodos(prev => prev.map(todo => ({
...todo,
completed: !allCompleted
})))
}
return VStack({
children: [
// Header
VStack({
children: [
Text("Todo List")
.modifier
.fontSize(32)
.fontWeight('bold')
.foregroundColor('#2c3e50')
.textAlign('center')
.build(),
Text(() => {
const { total, active, completed } = stats()
return `${total} total • ${active} active • ${completed} completed`
})
.modifier
.fontSize(14)
.foregroundColor('#7f8c8d')
.textAlign('center')
.build()
],
spacing: 8
})
.modifier
.padding({ bottom: 24 })
.build(),
// Add todo form
Form({
onSubmit: addTodo,
children: [
HStack({
children: [
TextField(newTodoText, setNewTodoText, "What needs to be done?")
.modifier
.flexGrow(1)
.padding(12)
.border(1, '#ddd')
.cornerRadius(8)
.fontSize(16)
.build(),
Button("Add", addTodo)
.modifier
.backgroundColor('#3498db')
.foregroundColor('white')
.padding({ horizontal: 20, vertical: 12 })
.cornerRadius(8)
.disabled(() => !newTodoText().trim())
.fontWeight('600')
.build()
],
spacing: 12,
alignment: 'center'
})
]
})
.modifier
.padding({ bottom: 24 })
.build(),
// Search and filters
VStack({
children: [
// Search
TextField(searchQuery, setSearchQuery, "Search todos...")
.modifier
.padding(10)
.border(1, '#ddd')
.cornerRadius(6)
.backgroundColor('#f8f9fa')
.build(),
// Filter buttons
HStack({
children: [
...(['all', 'active', 'completed'] as const).map(filterType =>
Button(
filterType.charAt(0).toUpperCase() + filterType.slice(1),
() => setFilter(filterType)
)
.modifier
.backgroundColor(() =>
filter() === filterType ? '#3498db' : '#ecf0f1'
)
.foregroundColor(() =>
filter() === filterType ? 'white' : '#2c3e50'
)
.padding({ horizontal: 16, vertical: 8 })
.cornerRadius(6)
.fontSize(14)
.build()
),
// Bulk actions
Button("Toggle All", toggleAll)
.modifier
.backgroundColor('#f39c12')
.foregroundColor('white')
.padding({ horizontal: 16, vertical: 8 })
.cornerRadius(6)
.fontSize(14)
.disabled(() => todos().length === 0)
.build(),
Button("Clear Completed", clearCompleted)
.modifier
.backgroundColor('#e74c3c')
.foregroundColor('white')
.padding({ horizontal: 16, vertical: 8 })
.cornerRadius(6)
.fontSize(14)
.disabled(() => stats().completed === 0)
.build()
],
spacing: 8,
alignment: 'center'
})
],
spacing: 16
})
.modifier
.padding({ bottom: 24 })
.build(),
// Todo list
VStack({
children: [
() => filteredTodos().length === 0 ?
Text(() => {
if (todos().length === 0) {
return "No todos yet. Add one above!"
}
if (searchQuery()) {
return `No todos match "${searchQuery()}"`
}
return `No ${filter()} todos`
})
.modifier
.fontSize(16)
.foregroundColor('#7f8c8d')
.textAlign('center')
.padding(40)
.backgroundColor('#f8f9fa')
.cornerRadius(8)
.build()
: null,
...filteredTodos().map(todo => TodoItem({
todo,
onToggle: toggleTodo,
onDelete: deleteTodo,
onUpdateText: updateTodoText,
onUpdatePriority: updateTodoPriority
}))
],
spacing: 8
})
],
spacing: 0,
alignment: 'stretch'
})
.modifier
.padding(24)
.backgroundColor('#ffffff')
.minHeight('100vh')
.build()
}
// Todo item component
function TodoItem({
todo,
onToggle,
onDelete,
onUpdateText,
onUpdatePriority
}: {
todo: Todo
onToggle: (id: number) => void
onDelete: (id: number) => void
onUpdateText: (id: number, text: string) => void
onUpdatePriority: (id: number, priority: Todo['priority']) => void
}) {
const [isEditing, setIsEditing] = createSignal(false)
const [editText, setEditText] = createSignal(todo.text)
const startEdit = () => {
setEditText(todo.text)
setIsEditing(true)
}
const saveEdit = () => {
const newText = editText().trim()
if (newText) {
onUpdateText(todo.id, newText)
}
setIsEditing(false)
}
const cancelEdit = () => {
setEditText(todo.text)
setIsEditing(false)
}
const priorityColors = {
high: '#e74c3c',
medium: '#f39c12',
low: '#95a5a6'
}
return VStack({
children: [
HStack({
children: [
// Priority indicator
VStack({
children: []
})
.modifier
.width(4)
.height(20)
.backgroundColor(priorityColors[todo.priority])
.cornerRadius(2)
.build(),
// Checkbox
Toggle({
isOn: () => todo.completed,
onToggle: () => onToggle(todo.id),
variant: 'checkbox'
}),
// Todo text or edit field
() => isEditing() ?
HStack({
children: [
TextField(editText, setEditText, "Todo text")
.modifier
.flexGrow(1)
.padding(8)
.border(1, '#3498db')
.cornerRadius(4)
.build(),
Button("Save", saveEdit)
.modifier
.backgroundColor('#27ae60')
.foregroundColor('white')
.padding({ horizontal: 12, vertical: 6 })
.cornerRadius(4)
.fontSize(12)
.build(),
Button("Cancel", cancelEdit)
.modifier
.backgroundColor('#95a5a6')
.foregroundColor('white')
.padding({ horizontal: 12, vertical: 6 })
.cornerRadius(4)
.fontSize(12)
.build()
],
spacing: 8,
alignment: 'center'
})
:
HStack({
children: [
Text(todo.text)
.modifier
.flexGrow(1)
.fontSize(16)
.foregroundColor(() => todo.completed ? '#7f8c8d' : '#2c3e50')
.textDecoration(() => todo.completed ? 'line-through' : 'none')
.onTap(startEdit)
.build(),
// Priority selector
HStack({
children: (['high', 'medium', 'low'] as const).map(priority =>
Button(priority[0].toUpperCase(), () => onUpdatePriority(todo.id, priority))
.modifier
.width(24)
.height(24)
.backgroundColor(() =>
todo.priority === priority ? priorityColors[priority] : '#ecf0f1'
)
.foregroundColor(() =>
todo.priority === priority ? 'white' : '#7f8c8d'
)
.cornerRadius(12)
.fontSize(10)
.fontWeight('bold')
.build()
),
spacing: 4,
alignment: 'center'
}),
Button("Delete", () => onDelete(todo.id))
.modifier
.backgroundColor('#e74c3c')
.foregroundColor('white')
.padding({ horizontal: 12, vertical: 6 })
.cornerRadius(4)
.fontSize(12)
.build()
],
spacing: 12,
alignment: 'center'
}),
],
spacing: 12,
alignment: 'center'
}),
// Creation date
Text(() => `Created ${todo.createdAt.toLocaleDateString()}`)
.modifier
.fontSize(11)
.foregroundColor('#bdc3c7')
.padding({ left: 32, top: 4 })
.build()
],
spacing: 0,
alignment: 'stretch'
})
.modifier
.padding(16)
.backgroundColor(() => todo.completed ? '#f8f9fa' : 'white')
.border(1, '#e9ecef')
.cornerRadius(8)
.shadow(() => todo.completed ?
{ x: 0, y: 1, radius: 2, color: 'rgba(0,0,0,0.05)' } :
{ x: 0, y: 2, radius: 4, color: 'rgba(0,0,0,0.1)' }
)
.build()
}
// Render the app
const container = document.getElementById('app')
renderComponent(TodoApp(), container)Key Features Demonstrated
1. Complex State Management
typescript
const [todos, setTodos] = createSignal<Todo[]>([])
const [filter, setFilter] = createSignal<'all' | 'active' | 'completed'>('all')
const [searchQuery, setSearchQuery] = createSignal('')Multiple interconnected signals managing different aspects of the application state.
2. Computed Values for Derived State
typescript
const filteredTodos = createMemo(() => {
let filtered = todos()
// Chain multiple filters
if (searchQuery()) {
filtered = filtered.filter(todo =>
todo.text.toLowerCase().includes(searchQuery().toLowerCase())
)
}
if (filter() === 'active') {
filtered = filtered.filter(todo => !todo.completed)
}
return filtered.sort(sortByPriorityAndDate)
})Efficient filtering and sorting that only recalculates when dependencies change.
3. Persistent Storage
typescript
// Save to localStorage when todos change
createEffect(() => {
localStorage.setItem('tachui-todos', JSON.stringify(todos()))
})
// Load from localStorage on startup
createEffect(() => {
const saved = localStorage.getItem('tachui-todos')
if (saved) {
setTodos(JSON.parse(saved))
}
})Automatic persistence with localStorage integration.
4. Form Handling and Validation
typescript
const addTodo = () => {
const text = newTodoText().trim()
if (!text) return // Validation
const newTodo = {
id: nextId(),
text,
completed: false,
createdAt: new Date(),
priority: 'medium'
}
setTodos(prev => [...prev, newTodo])
setNewTodoText('') // Clear form
}Form submission with validation and state updates.
5. Inline Editing
typescript
const [isEditing, setIsEditing] = createSignal(false)
const [editText, setEditText] = createSignal('')
// Toggle between display and edit modes
() => isEditing() ?
EditingInterface() :
DisplayInterface()Conditional rendering for inline editing functionality.
Advanced Patterns
Optimistic Updates
typescript
const deleteTodoOptimistic = async (id: number) => {
// Optimistically remove from UI
const originalTodos = todos()
setTodos(prev => prev.filter(todo => todo.id !== id))
try {
await deleteTodoAPI(id)
} catch (error) {
// Revert on error
setTodos(originalTodos)
showError('Failed to delete todo')
}
}Keyboard Shortcuts
typescript
function TodoAppWithKeyboards() {
const [todos, setTodos] = createSignal([])
const [selectedIndex, setSelectedIndex] = createSignal(-1)
createEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
switch (event.key) {
case 'j': // Next todo
setSelectedIndex(prev =>
Math.min(prev + 1, filteredTodos().length - 1)
)
break
case 'k': // Previous todo
setSelectedIndex(prev => Math.max(prev - 1, 0))
break
case ' ': // Toggle selected
if (selectedIndex() >= 0) {
toggleTodo(filteredTodos()[selectedIndex()].id)
}
break
case 'x': // Delete selected
if (selectedIndex() >= 0) {
deleteTodo(filteredTodos()[selectedIndex()].id)
}
break
}
}
document.addEventListener('keydown', handleKeyPress)
return () => document.removeEventListener('keydown', handleKeyPress)
})
// Rest of component...
}Drag and Drop Reordering
typescript
function DragDropTodos() {
const [todos, setTodos] = createSignal([])
const [draggedIndex, setDraggedIndex] = createSignal(-1)
const handleDragStart = (index: number) => {
setDraggedIndex(index)
}
const handleDrop = (targetIndex: number) => {
const dragIndex = draggedIndex()
if (dragIndex === -1 || dragIndex === targetIndex) return
setTodos(prev => {
const newTodos = [...prev]
const [draggedTodo] = newTodos.splice(dragIndex, 1)
newTodos.splice(targetIndex, 0, draggedTodo)
return newTodos
})
setDraggedIndex(-1)
}
// Todo items with drag handlers
return VStack({
children: todos().map((todo, index) =>
TodoItem({ todo })
.modifier
.draggable(true)
.onDragStart(() => handleDragStart(index))
.onDrop(() => handleDrop(index))
.opacity(() => draggedIndex() === index ? 0.5 : 1)
.build()
)
})
}Real-time Collaboration
typescript
function CollaborativeTodos() {
const [todos, setTodos] = createSignal([])
const [socket, setSocket] = createSignal(null)
// WebSocket connection
createEffect(() => {
const ws = new WebSocket('ws://localhost:8080')
setSocket(ws)
ws.onmessage = (event) => {
const { type, data } = JSON.parse(event.data)
switch (type) {
case 'TODO_ADDED':
setTodos(prev => [...prev, data])
break
case 'TODO_UPDATED':
setTodos(prev => prev.map(todo =>
todo.id === data.id ? data : todo
))
break
case 'TODO_DELETED':
setTodos(prev => prev.filter(todo => todo.id !== data.id))
break
}
}
return () => ws.close()
})
const addTodo = (text: string) => {
const newTodo = {
id: generateId(),
text,
completed: false,
createdAt: new Date()
}
// Optimistically add to local state
setTodos(prev => [...prev, newTodo])
// Send to server
socket()?.send(JSON.stringify({
type: 'ADD_TODO',
data: newTodo
}))
}
// Rest of component...
}Testing the Todo App
Component Testing
typescript
import { describe, it, expect, beforeEach } from 'vitest'
import { createSignal } from '@tachui/core'
describe('Todo App Logic', () => {
let todos, setTodos
beforeEach(() => {
[todos, setTodos] = createSignal([])
})
it('should add new todos', () => {
const newTodo = {
id: 1,
text: 'Test todo',
completed: false,
createdAt: new Date()
}
setTodos(prev => [...prev, newTodo])
expect(todos()).toHaveLength(1)
expect(todos()[0].text).toBe('Test todo')
})
it('should toggle todo completion', () => {
setTodos([{
id: 1,
text: 'Test',
completed: false,
createdAt: new Date()
}])
setTodos(prev => prev.map(todo =>
todo.id === 1 ? { ...todo, completed: !todo.completed } : todo
))
expect(todos()[0].completed).toBe(true)
})
it('should filter todos correctly', () => {
const testTodos = [
{ id: 1, text: 'Active', completed: false, createdAt: new Date() },
{ id: 2, text: 'Completed', completed: true, createdAt: new Date() }
]
setTodos(testTodos)
const activeTodos = todos().filter(t => !t.completed)
const completedTodos = todos().filter(t => t.completed)
expect(activeTodos).toHaveLength(1)
expect(completedTodos).toHaveLength(1)
})
})What's Next?
This todo list demonstrates advanced TachUI patterns. To continue learning:
- Data Fetching Example - API integration patterns
- State Management Guide - Advanced state patterns
- Form Components - Form handling best practices
- Testing Guide - Testing reactive applications
The todo list showcases how TachUI's reactive system naturally handles complex application state while maintaining clean, readable code.