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.
What you'll build
You'll need
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
Step 1 — Plan the data
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.
{
"version": 1,
"todos": [
{ "id": 1730041200000, "text": "Buy milk", "done": false },
{ "id": 1730041260000, "text": "Ship the app", "done": true }
]
}In code, that's three variables:
var FILE = "todos.json";
var dir = null; // this app's private folder, set in start()
var todos = []; // [{ id, text, done }]Step 2 — The page
A form, a list and a footer
<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>Step 3 — Load the list
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.
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
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.
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
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.
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);
}Wire up the buttons
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 …
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
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
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
// 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
important: true.Show a solution
// 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.
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.