All Articles

Build a Todo App With PyScript

We can run native Python code in the browser using PyScript. In this tutorial we’ll build a Todo app.

Introduction

pyscript 1

This walkthrough is based upon the todo app in the examples folder of the PyScript repository.

The app itself is fairly simple:

  • An input box to add a new todo along with a button to submit it
  • A list of todos
  • A checkbox for each todo to mark it as complete

The boilerplate

Before we can use PyScript in the browser, we must include the PyScript “runtime” in our HTML file.

The runtime consists of a JavaScript file which translates Python to the WebAssembly runtime of the browser and a set of standard CSS styles.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <title>Todo App</title>

    <link rel="icon" type="image/png" href="favicon.png" />
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  </head>

  <body class="container">
  </body>
</html>

Once these files are loaded, we can use the <py-script> HTML tag and include our Python code right there.

Our Building Blocks

Let’s start with building our interface.

First, we create the input box with the submit button at the top of the page.

<div>
  <input id="new-task-content" class="border flex-1 mr-3 border-gray-300 p-2 rounded" type="text">
  <button id="new-task-btn" class="p-2 text-white bg-blue-600 border border-blue-600 rounded" type="submit" pys-onClick="add_task">
    Add task
  </button>
</div>

Note that there is barely anything special about it. It’s just regular HTML with a few CSS classes (based on TailwindCSS) coming from the pyscript.css file we loaded in the boiler plate.

The only special thing here is the binding of the onClick event to a function on PyScript:

<button id="new-task-btn" class="[…]" type="submit" pys-onClick="add_task">

This pys-onClick attribute is usable once we’ve included the pyscript.js file from the boilerplate. Whenever the button is clicked, a function called add_task is called in our PyScript code. We will cover that code later.

Next, we need a list of our Todos. So, we create a template for them:

<template id="task-template">
    <section class="task bg-white my-1">
        <label for="flex items-center p-2 ">
            <input class="mr-2" type="checkbox" class="task-check">
            <p class="m-0 inline"></p>
        </label>
    </section>
</template>

Anything inside the <template> tag is used as a template – obviously. That means that we can re-use that HTML snippet for each todo the user will add. We will see how we can access and use the template in just a second.

The Python Code

Now, the interesting part happens: We’re writing Python code. Inside the browser.

Let’s start with accessing elements of the DOM. In PyScript we can use the Element class for it. We do not have to import this class as it is added to the global namespace by the PyScript runtime.

task_template = Element("task-template").select(".task", from_content=True)
task_list = Element("list-tasks-container")
new_task_content = Element("new-task-content")

We now have access to:

  • The input field for new tasks (new_task_content)
  • The container for the todos (task_list)
  • The template for a single todo (task_template)

Note that the Element class takes the id attribute of the DOM element to find it.

tasks = []

# define the task template that will be use to render new templates to the page
task_template = Element("task-template").select(".task", from_content=True)
task_list = Element("list-tasks-container")
new_task_content = Element("new-task-content")

Remember the add_task function we added to our pys-onClick attribute on the input field? Now it’s time to actually implement this function.

Let’s first think about what this function should do:

  1. It needs to gather the text from the input field
  2. It needs to add this text as a new todo to an existing list of todos
  3. It should get the template for a todo, add the content and append the new todo to our list container
  4. It should clear the input field once the todo is added

Here is how to do it:

def add_task(*ags, **kws):

    # 1. It needs to gather the text from the input field
    todo_text = new_task_content.element.value
    # ignore empty task
    if not todo_text:
        return None

    # 2. It needs to add this text as a new todo to an existing list of todos
    task_id = f"task-{len(tasks)}"
    task = {
        "id": task_id,
        "content": todo_text,
        "done": False,
        "created_at": dt.now(),
    }

    tasks.append(task)

    # 3. It should get the template for a todo, add the content and append the new todo to our list container
    task_html = task_template.clone(task_id, to=task_list)
    task_html_content = task_html.select("p")
    task_html_content.element.innerText = task["content"]
    task_html_check = task_html.select("input")
    task_list.element.appendChild(task_html.element)

    # 4. It should clear the input field once the todo is added
    new_task_content.clear()

Well done! Our todo app is working! We can add todos to our list by just using Python!

Marking todos as done

Only one thing is missing: We need to be able to mark todos as done.

Remember that little checkbox in front of each todo in our template?

To do so, we just have to add another event listener (just like pys-onClick) on the checkbox.

Due to the dynamic nature of the template (we copy the template for each new todo), we cannot use pys-onClick inside our template code. Instead, we must create a new event listener for each new todo item.

So, we can simply add a function which crosses out the todos marked as done for us and bind it to our checkbox element.

def add_task(*ags, **kws):

    # 1. It needs to gather the text from the input field
    todo_text = new_task_content.element.value
    # ignore empty task
    if not todo_text:
        return None

    # 2. It needs to add this text as a new todo to an existing list of todos
    task_id = f"task-{len(tasks)}"
    task = {
        "id": task_id,
        "content": todo_text,
        "done": False,
        "created_at": dt.now(),
    }

    tasks.append(task)

    # 3. It should get the template for a todo, add the content and append the new todo to our list container
    task_html = task_template.clone(task_id, to=task_list)
    task_html_content = task_html.select("p")
    task_html_content.element.innerText = task["content"]
    task_html_check = task_html.select("input")
    task_list.element.appendChild(task_html.element)

    # 4. It should clear the input field once the todo is added
    new_task_content.clear()

    # Create an event listener for our todo
    def check_task(evt=None):
        task["done"] = not task["done"]
        if task["done"]:
            task_html_content.element.classList.add("line-through")
        else:
            task_html_content.element.classList.remove("line-through")
    # bind the new event listener to our checkbox
    task_html_check.element.onclick = check_task

Try it yourself

You can try out the todo.html file yourself. When you click on View Source in your browser, you will get the whole file to play around.

What do you think about PyScript? Will it play a role on the JS world? What would you like to build with it?