Lesson 2 of 6

Build a to-do list

Lesson 1 saved plain text. Real apps save structured data: lists, settings, records. Here you'll keep a list of tasks as JSON and keep the screen and the file in sync.

15 minvos.fsJSON

What you'll build

A to-do list: add tasks, tick them off, delete them and clear the finished ones. Everything is saved to one JSON file in the app's private folder.

You'll need

Lesson 1 done, or comfortable with getInfo, readFile and writeFile.

You'll learn

  • How to design the data your app saves
  • How to save and load JSON with vos.fs
  • How to show user text safely
  • The change → draw → save pattern most apps use
Peek at the end result first, or build it step by step below.

Step 1 — Plan the data

1

Decide what one file looks like

Before writing code, decide what you'll save. One JSON file with a version number and a list of tasks is plenty. The version lets a future update of your app recognise older files.

json
{
  "version": 1,
  "todos": [
    { "id": 1730041200000, "text": "Buy milk", "done": false },
    { "id": 1730041260000, "text": "Ship the app", "done": true }
  ]
}

In code, that's three variables:

javascript
var FILE = "todos.json";
var dir = null;   // this app's private folder, set in start()
var todos = [];   // [{ id, text, done }]

Step 2 — The page

2

A form, a list and a footer

html
<header><h1>To-do</h1><span id="count" class="muted"></span></header>
<form id="add">
  <input id="text" placeholder="Add a task and press Enter" autocomplete="off">
  <button class="primary">Add</button>
</form>
<ul id="list"></ul>
<p id="empty" class="muted">Nothing to do yet. Add your first task above.</p>
<footer>
  <button id="clear">Clear completed</button>
  <span id="status" class="muted">Starting…</span>
</footer>
Checkpoint: the preview shows the input, an Add button and "Nothing to do yet".

Step 3 — Load the list

3

Read todos.json when the app starts

Same as Lesson 1, with one extra step: JSON.parse turns the saved text back into a list. If there's no file yet, start with an empty list.

javascript
function start() {
  window.vos.app.getInfo().then(function (info) {
    dir = info.sandboxPath;
    return window.vos.fs.readFile(dir, FILE);
  }).then(function (text) {
    // null = the file doesn't exist yet (first run), so start empty.
    todos = text ? (JSON.parse(text).todos || []) : [];
    render();
    setStatus("Ready");
  }).catch(function (err) {
    setStatus("Couldn't load tasks: " + err.message);
  });
}

Step 4 — Draw the list

4

Rebuild the list from the data

render() clears the list and rebuilds it from todos. That sounds slow, but for a few hundred tasks it's instant, and it means the screen can never drift out of sync with the data.

javascript
function render() {
  var list = document.getElementById("list");
  list.innerHTML = "";
  todos.forEach(function (todo) {
    var li = document.createElement("li");
    if (todo.done) li.className = "done";

    var box = document.createElement("input");
    box.type = "checkbox";
    box.checked = todo.done;
    box.onchange = function () { todo.done = box.checked; changed(); };

    var label = document.createElement("span");
    label.textContent = todo.text; // textContent keeps user text safe

    var del = document.createElement("button");
    del.textContent = "×";
    del.title = "Delete";
    del.onclick = function () {
      todos = todos.filter(function (t) { return t !== todo; });
      changed();
    };

    li.append(box, label, del);
    list.appendChild(li);
  });
  var left = todos.filter(function (t) { return !t.done; }).length;
  document.getElementById("count").textContent = left + " left";
  document.getElementById("empty").hidden = todos.length > 0;
}

Use textContent for anything a person typed

label.textContent = todo.text shows the text as plain text. Using innerHTML would let a task like <img onerror=…> run code inside your app. Reviewers look for this.

Step 5 — Add, change and save

5

One function for every change

Every action (add, tick, delete, clear) changes todos and then calls changed(), which redraws straight away and saves shortly after.

javascript
var saveTimer = null;
function changed() {
  render();
  if (!dir) return;
  setStatus("Saving…");
  clearTimeout(saveTimer);
  saveTimer = setTimeout(function () {
    var json = JSON.stringify({ version: 1, todos: todos }, null, 2);
    window.vos.fs.writeFile(dir, FILE, json)
      .then(function () { setStatus("Saved"); })
      .catch(function (err) { setStatus("Save failed: " + err.message); });
  }, 300);
}
6

Wire up the buttons

javascript
document.getElementById("add").addEventListener("submit", function (e) {
  e.preventDefault();
  var input = document.getElementById("text");
  var text = input.value.trim();
  if (!text) return;
  todos.push({ id: Date.now(), text: text, done: false });
  input.value = "";
  changed();
});

document.getElementById("clear").onclick = function () {
  todos = todos.filter(function (t) { return !t.done; });
  changed();
};

Finish the script with the same two start-up lines as Lesson 1: if (window.vos) start(); else …

Checkpoint: add three tasks and tick one. Open Files, click todos.json and you'll see your tasks with "done": true on the ticked one. Click Run: the list comes back.

Practice

Edit the file by hand

In the Files tab, open todos.json, change a task's text and save. Restart the app and check your change appears. This is a quick way to test how your app handles data it didn't write itself.

Files in the IDE are editable. Save your edit, then click Run.

Stay in sync with outside changes

Make the list update by itself when todos.json changes, for example when you edit it in the Files tab, or the same app is open on another computer.

vos.fs.watch(folder, callback) calls you when files in that folder change.

Show a solution
javascript
// At the end of start(), after the first load:
window.vos.fs.watch(dir, function () {
  window.vos.fs.readFile(dir, FILE).then(function (text) {
    todos = text ? (JSON.parse(text).todos || []) : [];
    render();
  });
});

Mark important tasks

Add a star button that marks a task as important and saves it as important: true.
Show a solution
javascript
// In render(), for each todo:
var star = document.createElement("button");
star.textContent = todo.important ? "★" : "☆";
star.onclick = function () { todo.important = !todo.important; changed(); };
li.prepend(star);

Recap

  • Plan your saved data first, and include a version number.
  • Save with JSON.stringify and load with JSON.parse.
  • Keep one source of truth (todos) and redraw the screen from it.
  • Show anything a person typed with textContent, never innerHTML.
Up next · Lesson 3Markdown notesWork with real files in the user's Documents folder.

Want to see a bigger version with boards and drag-and-drop? Download the polished to-do.vibe above, or read how saving works in detail in the vos.fs reference.