GitHub

Example: Todos

Components, list mutation, filters and keyed lists.

  • 72 lines of Python
  • 1 page
  • 0 server functions
  • 1158 B page JS (gzip)
localhost:8000 · live, 1158 B of page codeOpen ↗

#Source

examples/todo/app.pyweb
"""Todo list: client-side state, a component, computed values, filters."""

from pyweb import App, component

app = App(title="Todos", stylesheets=["/static/app.css"])

FILTERS = ["all", "active", "done"]


@component
def TodoItem(todo, on_toggle, on_remove):
    <li class={{"done": todo["done"]}}>
        <input type="checkbox" checked={todo["done"]} onchange={on_toggle} />
        <span>{todo["title"]}</span>
        <button class="remove" onclick={on_remove} aria-label="Remove">×</button>
    </li>


@app.page("/")
def Home():
    todos = []
    draft = ""
    show = "all"
    next_id = 1
    visible = [t for t in todos if show == "all" or (show == "done") == t["done"]]
    remaining = len([t for t in todos if not t["done"]])

    def add():
        title = draft.strip()
        if not title:
            return
        todos.append({"id": next_id, "title": title, "done": False})
        next_id += 1
        draft = ""

    def toggle(todo_id):
        for i, t in enumerate(todos):
            if t["id"] == todo_id:
                todos[i]["done"] = not t["done"]

    def remove(todo_id):
        todos = [t for t in todos if t["id"] != todo_id]

    def set_filter(name):
        show = name

    def clear_done():
        todos = [t for t in todos if not t["done"]]

    <main>
        <h1>Todos</h1>
        <form onsubmit={add}>
            <input id="new" bind={draft} placeholder="What needs doing?" autofocus />
            <button id="add" disabled={not draft.strip()}>Add</button>
        </form>
        <ul id="list">
            for todo in visible:
                <TodoItem
                    todo={todo}
                    on_toggle={lambda: toggle(todo["id"])}
                    on_remove={lambda: remove(todo["id"])} />
        </ul>
        if todos:
            <footer>
                <span id="left">{remaining} {"item" if remaining == 1 else "items"} left</span>
                for name in FILTERS:
                    <button class={{"active": show == name}} onclick={set_filter(name)}>{name}</button>
                <button id="clear" onclick={clear_done}>Clear done</button>
            </footer>
        else:
            <p class="empty">Nothing to do yet.</p>
    </main>

#What runs where

Output of pyweb inspect: every page variable, handler and server function, where it runs and why.

Output
page Home  route=/
  browser  todos          reactive state: .append() in add(); literal initial value
  browser  draft          reactive state: bound to an input (line 53); literal initial value
  browser  show           reactive state: assigned in set_filter(); literal initial value
  browser  next_id        reactive state: assigned in add(); literal initial value
  browser  visible        derived from show, todos; recomputed when they change
  browser  remaining      derived from todos; recomputed when they change
  browser  add            event handler (compiled to JavaScript)
  browser  toggle         event handler (compiled to JavaScript)
  browser  remove         event handler (compiled to JavaScript)
  browser  set_filter     event handler (compiled to JavaScript)
  browser  clear_done     event handler (compiled to JavaScript)

#Generated JavaScript

The browser modules the compiler wrote (before minification). They import the shared runtime.

JavaScript
// Home.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
const FILTERS = ["all", "active", "done"];
function TodoItem($p) {
  const todo = $p["todo"] || (() => null);
  const on_toggle = $p["on_toggle"] || (() => null);
  const on_remove = $p["on_remove"] || (() => null);
  return [$h("li", {"class": () => ({"done": $py.at(todo(), "done")})}, () => [
      $h("input", {"type": "checkbox", "checked": () => $py.at(todo(), "done"), "onchange": (e) => on_toggle()(e)}),
      $h("span", null, () => [$dyn(() => $py.at(todo(), "title"))]),
      $h("button", {"class": "remove", "onclick": (e) => on_remove()(e), "aria-label": "Remove"}, () => [$t("\u00d7")])
    ])];
}
function Home($s) {
  const todos = $signal("todos" in $s ? $s["todos"] : [{"id": 1, "title": "Write one Python file", "done": true}, {"id": 2, "title": "Run pyweb dev", "done": false}, {"id": 3, "title": "Ship it", "done": false}]);
  const draft = $signal("draft" in $s ? $s["draft"] : "");
  const show = $signal("show" in $s ? $s["show"] : "all");
  const next_id = $signal("next_id" in $s ? $s["next_id"] : 4);
  const visible = $computed(() => $py.iter(todos()).filter((t) => ((show() === "all") || $py.eq((show() === "done"), $py.at(t, "done")))));
  const remaining = $computed(() => $py.len($py.iter(todos()).filter((t) => !$py.truth($py.at(t, "done")))));
  function add() {
    let title;
    title = $py.m(draft(), "strip");
    if (!$py.truth(title)) {
      return;
    }
    $py.mut(todos, [], ($v) => $py.m($v, "append", {"id": next_id(), "title": title, "done": false}));
    next_id($py.add(next_id(), 1));
    draft("");
  }
  function toggle(todo_id) {
    let i, t;
    for ([i, t] of $py.iter($py.enumerate(todos()))) {
      if ($py.eq($py.at(t, "id"), todo_id)) {
        $py.setp(todos, [i, "done"], !$py.truth($py.at(t, "done")));
      }
    }
  }
  function remove(todo_id) {
    todos($py.iter(todos()).filter((t) => !$py.eq($py.at(t, "id"), todo_id)));
  }
  function set_filter(name) {
    show(name);
  }
  function clear_done() {
    todos($py.iter(todos()).filter((t) => !$py.truth($py.at(t, "done"))));
  }
  return [$h("main", null, () => [
      $h("h1", null, () => [$t("Todos")]),
      $h("form", {"onsubmit": add}, () => [
        $h("input", {"id": "new", "$bind": draft, "placeholder": "What needs doing?"}),
        $h("button", {"id": "add", "disabled": () => !$py.truth($py.m(draft(), "strip"))}, () => [$t("Add")])
      ]),
      $h("ul", {"id": "list"}, () => [$list(() => visible(), (todo) => [TodoItem({"todo": () => todo, "on_toggle": () => (() => (toggle($py.at(todo, "id")))), "on_remove": () => (() => (remove($py.at(todo, "id"))))})])]),
      $when(() => $py.truth(todos()), () => [$h("footer", null, () => [
          $h("span", {"id": "left"}, () => [
            $dyn(() => remaining()),
            $t(" "),
            $dyn(() => ((remaining() === 1) ? "item" : "items")),
            $t(" left")
          ]),
          $list(() => FILTERS, (name) => [$h("button", {"class": () => ({"active": $py.eq(show(), name)}), "onclick": (() => set_filter(name))}, () => [$t($py.text(name))])]),
          $h("button", {"id": "clear", "onclick": clear_done}, () => [$t("Clear done")])
        ])], () => [$h("p", {"class": "empty"}, () => [$t("Nothing to do yet.")])])
    ])];
}
$mount("Home", Home);
Edit this page on GitHub