VSN.jsGitHub ↗

COOKBOOK / LIVE EXAMPLE

Todo List example with VSN.js

Real markup. Real VSN. Try it below.

← All examples · Open full screen ↗

View example source
<section id="todo-app" class="example-card">
  <header class="todo-header">
    <div>
      <h1>Todo List</h1>
      <p class="todo-subtitle">Persisted with localStorage, driven by CFS state.</p>
    </div>
    <div class="todo-stats">
      <span class="todo-count"><strong vsn-bind:from="remaining">0</strong> left</span>
      <span class="todo-total">of <span vsn-bind:from="total">0</span></span>
    </div>
  </header>

  <form class="todo-form" vsn-on:submit!prevent="submit();">
    <input
      class="todo-input"
      placeholder="Add a new task"
      autocomplete="off"
      vsn-bind="newTitle"
      vsn-on:keyup!enter="submit();"
    />
    <button class="btn" type="submit">Add</button>
  </form>

  <div class="todo-toolbar">
    <div class="todo-filters">
      <button class="chip" data-filter="all">
        All
      </button>
      <button class="chip" data-filter="active">
        Active
      </button>
      <button class="chip" data-filter="done">
        Done
      </button>
    </div>
    <button class="btn secondary" vsn-on:click="clearCompleted();">Clear done</button>
  </div>

  <ul class="todo-list">
    <template vsn-each="todos as todo, index" vsn-key="todo.0">
      <li class="todo-item">
        <button class="todo-check" vsn-on:click="toggleTodo(index);"></button>
        <span class="todo-title"></span>
        <button class="todo-delete" vsn-on:click="removeTodo(index);">Remove</button>
      </li>
    </template>
  </ul>
</section>

<script type="text/vsn">
  use localStorage as storage;
  use JSON as json;
  use Date as date;

  #todo-app {
    key: "vsn:todos";
    newTitle: "";
    filter: "all";
    todos: [];
    remaining: 0;
    total: 0;

    construct {
      load();
    }

    async load() {
      raw = storage.getItem(key);
      todos = raw ? json.parse(raw) : [];
      await sync();
    }

    async save() {
      storage.setItem(key, json.stringify(todos));
    }

    async sync() {
      total = todos.length;
      remainingList = await list.filter(todos, (todo) => !todo[2]);
      remaining = remainingList.length;
      await save();
    }

    submit() {
      title = newTitle.trim();
      return title == "" ? null : addTodo(title);
    }

    async addTodo(title) {
      id = "" + date.now();
      todos = todos.concat([[id, title, false]]);
      newTitle = "";
      await sync();
    }

    async toggleTodo(index) {
      todo = todos[index];
      return !todo ? null : toggleTodoCore(todo, index);
    }

    async toggleTodoCore(todo, index) {
      updated = [todo[0], todo[1], !todo[2]];
      todos = todos.slice(0, index).concat([updated]).concat(todos.slice(index + 1));
      await sync();
    }

    async removeTodo(index) {
      todos = todos.slice(0, index).concat(todos.slice(index + 1));
      await sync();
    }

    setFilter(value) {
      filter = value;
    }

    async clearCompleted() {
      todos = await list.filter(todos, (todo) => !todo[2]);
      await sync();
    }

    .chip {
      @data-filter :> value;
      @aria-pressed :< (filter == value);

      on click() {
        setFilter(value);
      }
    }

    .todo-item {
      @class :< {
        done: todo[2]
      };
      $display :< (
        filter == "all" ||
        (filter == "active" && !todo[2]) ||
        (filter == "done" && todo[2])
      ? "grid" : "none");
    }

    .todo-title {
      @html :< todo[1];
    }

    .todo-check {
      @html :< (todo[2] ? "Undo" : "Done");
    }
  }
</script>