Example: Todos
Components, list mutation, filters and keyed lists.
- 72 lines of Python
- 1 page
- 0 server functions
- 1158 B page JS (gzip)
#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);