Build a Todo App#

In this section, we will work on building a Todo App together so that our wind turbine technicians can keep track of their tasks. As a team, we will collaborate to create an app that provides the following functionality:

  • Adding, removing, and clearing all tasks

  • Marking a task as solved

  • Keeping track of the number of completed tasks

  • Disabling or hiding buttons when necessary

Note

When we ask everyone to run the code in the sections below, you may either execute the code directly in the Panel docs via the green run button, in a cell in a notebook, or in a file app.py that is served with panel serve app.py --dev.

Requirements
panel
Code
import panel as pn

pn.extension(sizing_mode="stretch_width")

BUTTON_WIDTH = 125

state_changed_count = pn.ui.IntInput()
tasks = pn.ui.Column()

def update_state_changed_count(*args):
    state_changed_count.value += 1

def remove_task(task, *args):
    index = tasks.index(task)
    tasks.pop(index)

def remove_all_tasks(*args):
    tasks.clear()

def create_task(text):
    state = pn.ui.Checkbox(align="center", sizing_mode="fixed")
    content = pn.ui.Markdown(text)
    remove = pn.ui.Button(width=BUTTON_WIDTH, icon="delete", sizing_mode="fixed")
    task = pn.ui.Row(state, content, remove, sizing_mode="stretch_width")

    pn.bind(remove_task, task, remove, watch=True)
    # We have to bind the below after the above!
    pn.bind(update_state_changed_count, state, remove, watch=True)

    return task

def add_task(text, *args):
    if not text:
        return

    new_task = create_task(text)
    tasks.append(new_task)

    return tasks

def get_state(*args):
    total_tasks = len(tasks)
    completed_tasks = sum(check[0].value for check in tasks)
    return f"{completed_tasks} of {total_tasks} tasks completed"

def can_add(value_input):
    return not bool(value_input)

def has_tasks(*args):
    return len(tasks) > 0


add_task("Inspect the blades")
add_task("Inspect the nacelle")
add_task("Tighten the bolts")

text_input = pn.ui.TextInput(label="Task", placeholder="Enter a task")

submit_task = pn.ui.Button(
    label="Add",
    align="center",
    color="primary",
    width=BUTTON_WIDTH,
    sizing_mode="fixed",
    disabled=pn.bind(can_add, text_input.param.value_input)
)
clear = pn.ui.Button(
    label="Remove All",
    color="primary",
    variant="outlined",
    width=BUTTON_WIDTH,
    sizing_mode="fixed",
    visible=pn.bind(has_tasks, state_changed_count)
)

def reset_text_input(*args):
    text_input.value = text_input.value_input = ""

pn.bind(add_task, text_input, submit_task, watch=True)
pn.bind(reset_text_input, text_input, submit_task, watch=True)
pn.bind(remove_all_tasks, clear, watch=True)
# We have to bind the below after the above!
pn.bind(update_state_changed_count, text_input, submit_task, clear, watch=True)

status_report = pn.bind(get_state, state_changed_count, tasks.param.objects)

pn.ui.Column(
    "## WTG Task List",
    status_report,
    pn.ui.Row(text_input, submit_task),
    tasks,
    pn.ui.Row(pn.ui.HSpacer(), clear),
    max_width=500,
).servable()

Install the Requirements#

pip install panel
conda install -y -c conda-forge panel

Explanation#

The app keeps its tasks in a single tasks column. Each task is a Row created by create_task, holding a Checkbox, the task text as Markdown and a remove Button with the delete icon. Binding remove_task to the remove button with watch=True removes that row from tasks whenever the button is clicked.

Checking a box changes a widget inside a row, which tasks.param.objects does not notice. We therefore keep a hidden state_changed_count counter and increment it whenever a task is added, removed or checked. The status report is bound to both the counter and tasks.param.objects, so get_state reruns and recounts the completed tasks on every change.

The buttons react to the state in the same way. The Add button is disabled while the text input is empty, because its disabled parameter is bound to can_add and the value_input of the TextInput, which updates on every keystroke. The Remove All button is only visible while there are tasks, because visible is bound to has_tasks and the counter. Finally, the HSpacer pushes the Remove All button to the right edge of its row.

The order of the pn.bind(..., watch=True) calls matters: callbacks run in the order they were bound, so the counter must be updated after a task has been added or removed, otherwise get_state would count the tasks before the change.

Let’s perform the following actions:

  • Remove one task

  • Remove all tasks

  • Add 3 Tasks

  • Check 1 of 3 tasks and see the status report update accordingly.

Note

A todo app can be built in many other ways. The main purpose of this example is for all of us to acquire the basic skills needed to develop stateful, dynamically updating apps like this one.

Recap#

In this section, we have built a Todo App with many features. We needed to combine many of the things we have learned so far.