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.