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