Build a todo list
This tutorial builds a todo list: type a task, add it, tick it off, and see how many are left. It picks up where Build a counter left off.
You’ll learn: structs, lists, text fields, checkboxes, for loops, functions and
your own views.
Create a new project to work in:
mkdir todos
touch todos/main.nf
Step 1: Describe a todo
A todo has two parts: its title, and whether it’s done. In Nifty you describe a shape of data like that with a struct:
struct Todo {
title: string
done: bool
}
This says: “a Todo has a title, which is text (string), and done, which is
true or false (bool).”
A struct is only a description. To make an actual todo, you call it like a function, naming each field:
Todo(title: "Buy milk", done: false)
Step 2: A list of todos
The app needs to remember a list of todos. Add this under the struct:
state todos: [Todo] = []
[Todo]means “a list ofTodos”.= []means it starts empty.
Why write the type here, when state count = 0 didn’t need one? Because Nifty can tell
0 is a number, but it can’t tell what an empty list [] will hold. So you say.
Step 3: Show the list
Add a window that shows every todo, using for:
struct Todo {
title: string
done: bool
}
state todos: [Todo] = [
Todo(title: "Buy milk", done: false),
Todo(title: "Write docs", done: true),
]
window("Todos", width: 360, height: 480) {
column(spacing: 12, padding: 20) {
for todo in todos {
label(todo.title)
}
}
}
We started the list with two todos so there’s something to see. for todo in todos
repeats its block once for every item; each time, todo is the current item.
todo.title reads the item’s title field.
Run it and you’ll see both titles.
Step 4: Checkboxes
Replace the label with a checkbox that’s ticked when the todo is done:
for todo in todos {
checkbox(checked: $todo.done, title: todo.title)
}
Notice the $ in $todo.done. That’s a two-way binding:
- The checkbox shows whether
todo.doneis true, and - when you click the checkbox, it changes
todo.done.
Without $, the checkbox could only show a value, never change it. Run the app and tick
some boxes. (More in Two-way bindings.)
Step 5: Typing new todos
To add todos we need somewhere to type. Add a second piece of state for the text being typed, and a text field bound to it. Start the list empty again, too:
state todos: [Todo] = []
state draft = ""
And at the top of the column:
row(spacing: 8) {
text_field($draft, placeholder: "What needs doing?")
button("Add", style: .primary) { add_todo() }
}
divider()
text_field($draft, …)showsdraftand updates it as you type.- The placeholder is the grey hint shown while the field is empty.
divider()draws a line.- The button calls
add_todo(), a function we’ll write next.
Step 6: A function to add todos
Put this function anywhere at the top level of the file (outside the window):
fn add_todo() {
if draft != "" {
todos.push(Todo(title: draft, done: false))
draft = ""
}
}
Read it out loud: “To add a todo: if the draft isn’t empty, push a new todo with that title onto the list, then clear the draft.”
fndeclares a function.!=means “is not equal to”.todos.push(…)adds an item to the end of the list.draft = ""empties the text field, because the field is bound todraft.
Run it. Type something, click Add (or press Return, since it’s the primary button), and your todo appears.
Step 7: Count what’s left
Let’s show how many todos aren’t done. Add a function that counts them and returns the answer:
fn remaining() -> int {
let count = 0
for todo in todos {
if !todo.done {
count += 1
}
}
return count
}
-> intsays this function gives back a whole number.let count = 0makes a local variable that only exists while the function runs.!todo.donemeans “not done”.return counthands the answer back.
Step 8: Your own view
The header will show a title on the left and the count on the right. You could write that inline, but giving it a name keeps the window tidy. A view is a reusable piece of UI you define yourself:
view Header(left: int) {
row(align: .center) {
label("Todos", style: .title)
spacer()
label("{left} left", style: .caption)
}
}
spacer() takes up all the free space, pushing the count to the right edge. Use the
view like any built-in element:
Header(left: remaining())
The header updates automatically whenever a todo is added or ticked, because
remaining() reads todos, and Nifty notices that.
The finished app
Here’s everything together:
struct Todo {
title: string
done: bool
}
state todos: [Todo] = []
state draft = ""
fn add_todo() {
if draft != "" {
todos.push(Todo(title: draft, done: false))
draft = ""
}
}
fn remaining() -> int {
let count = 0
for todo in todos {
if !todo.done {
count += 1
}
}
return count
}
view Header(left: int) {
row(align: .center) {
label("Todos", style: .title)
spacer()
label("{left} left", style: .caption)
}
}
window("Todos", width: 360, height: 480) {
column(spacing: 12, padding: 20) {
Header(left: remaining())
row(spacing: 8) {
text_field($draft, placeholder: "What needs doing?")
button("Add", style: .primary) { add_todo() }
}
divider()
for todo in todos {
checkbox(checked: $todo.done, title: todo.title)
}
}
}
Try this next
- Show a message when the list is empty:
if todos.is_empty() { label("All clear!") } - Add a Clear all button that sets
todos = []. - Show the total:
label("{todos.len()} total", style: .caption).
Now you know almost all of Nifty! The guide explains each idea in more depth, and the cheat sheet fits it all on one page.