Skip to main content

Overview

The Todo App demonstrates advanced GlyphUI features:
  • Global state management with createStore()
  • Component composition with connect()
  • Multiple stores (todo + theme)
  • Filtering and batch operations
  • Theme switching with dark mode

Live Demo

View the complete source code on GitHub

What You’ll Learn

Store Creation

Creating global stores with createStore()

Connect Pattern

Connecting components to stores

State Selectors

Selecting specific state slices

Theme Switching

Implementing dark/light mode

Complete Code

Store Definitions

vercel-todo.js
import {
  Component,
  createComponent,
  h,
  createStore,
  connect
} from "@glyphui/runtime";

// Theme Store
const themeStore = createStore((set) => ({
  isDark: window.matchMedia('(prefers-color-scheme: dark)').matches,
  toggleTheme: () => set((state) => ({ isDark: !state.isDark })),
}));

// Todo Store
const todoStore = createStore((set) => ({
  todos: [],
  newTodoText: '',
  filter: 'all', // 'all', 'active', 'completed'
  nextId: 1,

  setNewTodoText: (text) => set({ newTodoText: text }),
  
  addTodo: () => set((state) => {
    if (!state.newTodoText.trim()) return state;
    
    return {
      todos: [
        ...state.todos,
        {
          id: state.nextId,
          text: state.newTodoText,
          completed: false,
          createdAt: Date.now()
        }
      ],
      newTodoText: '',
      nextId: state.nextId + 1
    };
  }),
  
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  })),
  
  removeTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  })),
  
  clearCompleted: () => set((state) => ({
    todos: state.todos.filter(todo => !todo.completed)
  })),
  
  setFilter: (filter) => set({ filter }),
  
  getFilteredTodos: (state) => {
    switch (state.filter) {
      case 'active':
        return state.todos.filter(todo => !todo.completed);
      case 'completed':
        return state.todos.filter(todo => todo.completed);
      default:
        return state.todos;
    }
  },
  
  getStats: (state) => {
    const total = state.todos.length;
    const completed = state.todos.filter(todo => todo.completed).length;
    const active = total - completed;
    
    return { total, completed, active };
  }
}));

Connected Components

// Theme Toggle Component
class ThemeToggle extends Component {
  render(props) {
    const { store } = props;
    
    if (store.isDark) {
      document.body.setAttribute('data-theme', 'dark');
    } else {
      document.body.removeAttribute('data-theme');
    }
    
    return h('button', {
      class: 'theme-toggle',
      on: { click: () => store.toggleTheme() }
    }, [
      store.isDark ? '☀️' : '🌙'
    ]);
  }
}

const ConnectedThemeToggle = connect(themeStore)(ThemeToggle);

// Todo Input Component
class TodoInput extends Component {
  constructor(props) {
    super(props);
    this.inputRef = null;
  }
  
  handleKeyDown(e) {
    if (e.key === 'Enter') {
      this.props.store.addTodo();
      if (this.inputRef) {
        setTimeout(() => this.inputRef.focus(), 0);
      }
    }
  }
  
  render(props) {
    const { store } = props;
    
    return h('div', { class: 'todo-input' }, [
      h('input', { 
        type: 'text',
        value: store.newTodoText,
        placeholder: 'What needs to be done?',
        ref: (el) => this.inputRef = el,
        on: { 
          input: (e) => store.setNewTodoText(e.target.value),
          keydown: (e) => this.handleKeyDown(e)
        }
      }),
      h('button', { 
        on: { click: () => store.addTodo() }
      }, ['Add'])
    ]);
  }
}

const ConnectedTodoInput = connect(todoStore)(TodoInput);

// Todo List Component
class TodoList extends Component {
  render(props) {
    const { store } = props;
    const filteredTodos = store.getFilteredTodos(store);
    const stats = store.getStats(store);
    
    return h('div', { class: 'todo-list-container' }, [
      h('div', { class: 'todos' }, 
        filteredTodos.map(todo =>
          h('div', {
            class: `todo-item ${todo.completed ? 'completed' : ''}`,
            key: todo.id
          }, [
            h('div', { class: 'todo-content' }, [todo.text]),
            h('div', { class: 'todo-actions' }, [
              h('button', {
                class: 'secondary small',
                on: { click: () => store.toggleTodo(todo.id) }
              }, [todo.completed ? 'Undo' : 'Done']),
              h('button', {
                class: 'secondary small remove',
                on: { click: () => store.removeTodo(todo.id) }
              }, ['Remove'])
            ])
          ])
        )
      ),
      
      store.todos.length > 0 && h('div', { class: 'stats' }, [ 
        h('span', {}, [`${stats.active} items left`]),
        h('div', { class: 'filters' }, [
          h('button', { 
            class: `filter ${store.filter === 'all' ? 'active' : ''}`,
            on: { click: () => store.setFilter('all') }
          }, ['All']),
          h('button', { 
            class: `filter ${store.filter === 'active' ? 'active' : ''}`,
            on: { click: () => store.setFilter('active') }
          }, ['Active']),
          h('button', { 
            class: `filter ${store.filter === 'completed' ? 'active' : ''}`,
            on: { click: () => store.setFilter('completed') }
          }, ['Completed'])
        ]),
        stats.completed > 0 && h('button', { 
          class: 'secondary small clear-completed',
          on: { click: () => store.clearCompleted() }
        }, ['Clear Completed'])
      ])
    ]);
  }
}

const ConnectedTodoList = connect(todoStore)(TodoList);

// Main App
class TodoApp extends Component {
  render() {
    return h('div', { class: 'todo-app' }, [
      createComponent(ConnectedTodoInput),
      createComponent(ConnectedTodoList)
    ]);
  }
}

// Mount
const themeToggle = new ConnectedThemeToggle();
themeToggle.mount(
  document.getElementById('theme-toggle').parentNode,
  document.getElementById('theme-toggle')
);
document.getElementById('theme-toggle').remove();

const todoApp = new TodoApp();
todoApp.mount(document.getElementById('app-container'));

Key Concepts

1. Creating Stores

Stores are created with createStore() and a setter function:
const todoStore = createStore((set) => ({
  todos: [],
  addTodo: () => set((state) => ({ todos: [...state.todos, newTodo] }))
}));
The set function can take an object or a function that receives the current state.

2. Connecting Components

Use connect() to subscribe components to store updates:
const ConnectedTodoInput = connect(todoStore)(TodoInput);
The store is passed as props.store to the component.

3. State Selectors

You can select specific state slices when connecting:
const ConnectedComponent = connect(todoStore, (state) => ({
  newTodoText: state.newTodoText,
  addTodo: state.addTodo
}))(Component);

4. Computed Properties

Derived state can be calculated in the store:
getFilteredTodos: (state) => {
  switch (state.filter) {
    case 'active':
      return state.todos.filter(todo => !todo.completed);
    case 'completed':
      return state.todos.filter(todo => todo.completed);
    default:
      return state.todos;
  }
}

Features Demonstrated

  • Multiple independent stores (theme + todo)
  • Immutable state updates
  • Computed properties
  • Add todos with Enter key
  • Toggle completion status
  • Delete todos
  • Filter by status (all/active/completed)
  • Clear all completed todos
  • Ref usage for input focus
  • Conditional rendering
  • Theme persistence via data attributes
  • Statistics calculation

Running the Example

1

Clone the repository

git clone https://github.com/x0bd/glyphui.git
cd glyphui/examples/vercel-todo
2

Open in browser

Open index.html in your browser or use a development server:
npx serve .
3

Try the features

  • Add some todos
  • Toggle completion status
  • Try different filters
  • Switch between light and dark themes

Next Steps

State Management

Learn more about stores and state

Lazy Loading

Optimize performance with lazy loading

Connect API

Detailed connect() documentation

Store API

Detailed createStore() documentation

Build docs developers (and LLMs) love