Todo Application

A more complex example showing state management, event handling, data persistence, and component composition in Now.js

Live Demo

Source Code

// Register 'todo' component with reactive state management Now.getManager('component').define('todo', { // Enable reactive data binding reactive: true, // Initial component state state: { title: 'Todo Application', // Application title todos: [], // Array to store todo items remaining: 0, // Number of uncompleted todos newTodo: '', // Input field for new todo text filter: 'all' // Current filter state (all/active/completed) }, // Computed properties - automatically update when dependencies change computed: { // Filter todos based on current filter selection filteredTodos() { const {todos, filter} = this.state; if (filter === 'all') { return todos; } // Return completed or active todos based on filter return todos.filter(todo => (filter === 'completed' ? todo.completed : !todo.completed)); } }, // Component methods methods: { // Add new todo item addTodo() { // Skip if input is empty or only whitespace if (!this.state.newTodo.trim()) return; // Create and add new todo object this.state.todos.push({ id: Utils.generateUUID(), // Generate unique ID text: this.state.newTodo, // Todo text completed: false // Initial completion status }); // Clear input field this.state.newTodo = ''; // Persist to localStorage this.methods.saveTodos(); }, // Toggle completion status of a todo toggleTodo(id) { const todo = this.state.todos.find(t => t.id === id); if (todo) { todo.completed = !todo.completed; this.methods.saveTodos(); } }, // Remove a todo item removeTodo(id) { this.state.todos = this.state.todos.filter(t => t.id !== id); this.methods.saveTodos(); }, // Remove all completed todos clearCompleted() { this.state.todos = this.state.todos.filter(t => !t.completed); this.methods.saveTodos(); }, // Update current filter setFilter(filter) { this.state.filter = filter; }, // Load todos from localStorage loadTodos() { const stored = localStorage.getItem('todos'); if (stored) { this.state.todos = JSON.parse(stored); this.methods.updateRemaining(); } }, // Save todos to localStorage saveTodos() { localStorage.setItem('todos', JSON.stringify(this.state.todos)); this.methods.updateRemaining(); }, // Update number of uncompleted todos updateRemaining() { this.state.remaining = this.state.todos.filter(todo => !todo.completed).length; } }, // Lifecycle hook - called after component is mounted to DOM mounted() { // Load saved todos when component is mounted this.methods.loadTodos(); }, // Event handlers events: { // Save todos when application cleanup occurs 'app:cleanup:end': function() { this.methods.saveTodos(); } } });
<div class="todo-app" data-component="todo"> <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 class="button green icon-add" data-on="click:addTodo">Add Todo</button> </div> </div> <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> <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 class="button red icon-delete" data-on="click:removeTodo(todo.id)"></button> </label> </template> </div> <div class="todo-footer"> <button class="button small red" data-on="click:clearCompleted">Clear Completed</button> <span data-text="remaining + ' items left'"></span> </div> </div>
.todo-app { max-width: 600px; margin: 0 auto; padding: var(--spacing-6); background: var(--color-surface); border-radius: var(--border-radius-lg); box-shadow: var(--shadow-md); } .todo-header { margin-bottom: var(--spacing-6); } .todo-input { display: flex; gap: var(--spacing-2); } .todo-input input { flex: 1; } .todo-filters { display: flex; gap: var(--spacing-2); margin-bottom: var(--spacing-4); } .todo-filters button { flex: 1; padding: var(--spacing-2) var(--spacing-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-list { margin-bottom: var(--spacing-6); } .todo-item { display: flex; align-items: center; gap: var(--spacing-3); padding: var(--spacing-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); }

How it Works

State & Reactivity

Uses Now.js's reactive state system to automatically update the UI when todo items change. Includes computed properties for filtered todos and remaining count.

Data Persistence

Demonstrates local storage integration to persist todos between sessions. The component automatically saves changes and loads existing todos.

Event Handling

Shows advanced event handling with method parameters, keyboard events, and click handling. Uses data-on directive for declarative event binding.

Component Composition

Illustrates proper component organization with state management, computed properties, methods, and lifecycle hooks working together.

Data Binding

Showcases two-way data binding with data-model, conditional rendering with data-if, and list rendering with data-for directives.

Styling

Demonstrates using Now.js's built-in CSS framework (gcss) along with component-specific styles that respond to state changes.

Next Steps