Todo Application

A complete demonstration of state management, computed properties, data persistence, and advanced event handling in Now.js

Live Demo

Key Features

Reactive State

Automatic UI updates when todos change. No manual DOM manipulation required.

Computed Properties

Filtered todo lists that automatically update based on state changes.

Data Persistence

LocalStorage integration keeps your todos saved between sessions.

Advanced Events

Keyboard shortcuts, method parameters, and declarative event binding.

Source Code

// Todo Application Component
                  Now.getManager('component').define('todo', {
                  // Enable reactive state management
                  reactive: true,

                  // Component state
                  state: {
                  title: 'Todo Application',
                  todos: [],
                  remaining: 0,
                  newTodo: '',
                  filter: 'all'
                  },

                  // Computed properties - automatically update when dependencies change
                  computed: {
                  filteredTodos() {
                  const {todos, filter} = this.state;
                  if (filter === 'all') return todos;
                  return todos.filter(todo =>
                  filter === 'completed' ? todo.completed : !todo.completed
                  );
                  }
                  },

                  // Component methods
                  methods: {
                  addTodo() {
                  if (!this.state.newTodo.trim()) return;

                  this.state.todos.push({
                  id: Utils.generateUUID(),
                  text: this.state.newTodo,
                  completed: false
                  });

                  this.state.newTodo = '';
                  this.methods.saveTodos();
                  },

                  toggleTodo(id) {
                  const todo = this.state.todos.find(t => t.id === id);
                  if (todo) {
                  todo.completed = !todo.completed;
                  this.methods.saveTodos();
                  }
                  },

                  removeTodo(id) {
                  this.state.todos = this.state.todos.filter(t => t.id !== id);
                  this.methods.saveTodos();
                  },

                  clearCompleted() {
                  this.state.todos = this.state.todos.filter(t => !t.completed);
                  this.methods.saveTodos();
                  },

                  setFilter(filter) {
                  this.state.filter = filter;
                  },

                  loadTodos() {
                  const stored = localStorage.getItem('todos');
                  if (stored) {
                  this.state.todos = JSON.parse(stored);
                  this.methods.updateRemaining();
                  }
                  },

                  saveTodos() {
                  localStorage.setItem('todos', JSON.stringify(this.state.todos));
                  this.methods.updateRemaining();
                  },

                  updateRemaining() {
                  this.state.remaining = this.state.todos.filter(
                  todo => !todo.completed
                  ).length;
                  }
                  },

                  // Lifecycle hooks
                  mounted() {
                  this.methods.loadTodos();
                  console.log('Todo component mounted');
                  },

                  // Global event handlers
                  events: {
                  'app:cleanup:end': function() {
                  this.methods.saveTodos();
                  console.log('Todos saved on cleanup');
                  }
                  }
                  });
<div class="todo-app" data-component="todo">
                  <!-- Input section -->
                  <div class="todo-header">
                  <div class="todo-input">
                  <input type="text"
                  data-model="newTodo"
                  placeholder="What needs to be done?"
                  data-on="keyup.enter:addTodo">
                  <button data-on="click:addTodo">Add Todo</button>
                  </div>
                  </div>

                  <!-- Filter buttons -->
                  <div class="todo-filters">
                  <button data-class="active:filter === 'all'"
                  data-on="click:setFilter('all')">All</button>
                  <button data-class="active:filter === 'active'"
                  data-on="click:setFilter('active')">Active</button>
                  <button data-class="active:filter === 'completed'"
                  data-on="click:setFilter('completed')">Completed</button>
                  </div>

                  <!-- Todo list -->
                  <div class="todo-list" data-for="todo of filteredTodos">
                  <template>
                  <label class="todo-item">
                  <input type="checkbox"
                  data-checked="todo.completed"
                  data-on="change:toggleTodo(todo.id)">
                  <span data-text="todo.text"></span>
                  <button data-on="click:removeTodo(todo.id)">Delete</button>
                  </label>
                  </template>
                  </div>

                  <!-- Footer -->
                  <div class="todo-footer">
                  <span data-text="remaining + ' items left'"></span>
                  <button data-on="click:clearCompleted">Clear Completed</button>
                  </div>
                  </div>

                  .todo-app {
                  max-width: 600px;
                  margin: 0 auto;
                  display: flex;
                  flex-direction: column;
                  gap: var(--space-2);
                  }
                  .todo-input {
                  display: flex;
                  gap: var(--space-2);
                  }
                  .todo-input input {
                  flex: 1;
                  }
                  .todo-filters {
                  display: flex;
                  gap: var(--space-2);
                  }
                  .todo-filters button {
                  flex: 1;
                  padding: var(--space-2) var(--space-4);
                  background: var(--color-surface-light);
                  border: 1px solid var(--color-border);
                  border-radius: var(--border-radius);
                  color: var(--color-text-light);
                  }
                  .todo-filters button.active {
                  background: var(--color-primary);
                  color: white;
                  border-color: var(--color-primary);
                  }
                  .todo-item {
                  display: flex;
                  align-items: center;
                  gap: var(--space-3);
                  padding: var(--space-3);
                  border-bottom: 1px solid var(--color-border);
                  }
                  .todo-item:last-child {
                  border-bottom: none;
                  }
                  .todo-item input[type="checkbox"]:checked + span {
                  text-decoration: line-through;
                  color: var(--color-text-light);
                  }
                  .todo-item span {
                  flex: 1;
                  }
                  .todo-footer {
                  display: flex;
                  justify-content: space-between;
                  align-items: center;
                  color: var(--color-text-light);
                  font-size: var(--font-size-sm);
                  }
                

1. State Management

The todo app uses Now.js reactive state to track todos, filters, and input. When state changes, the UI automatically updates without manual DOM manipulation.

2. Computed Properties

The filteredTodos computed property automatically recalculates when todos or filter changes, providing efficient reactive filtering.

3. Event Handling

Click and keyboard events are handled using data-on attributes with method parameters, providing a clean, declarative way to bind events.

4. Data Persistence

LocalStorage integration automatically saves and loads todos. The component uses lifecycle hooks to ensure data is persisted correctly.

5. List Rendering

The data-for directive efficiently renders todo items, automatically updating the list when items are added, removed, or filtered.

6. Two-Way Binding

The data-model directive creates two-way binding for the input field, keeping the state and UI in perfect sync.

Next Steps