Example: Showcase
Reactive state, derived values and a server-backed search on one page.
- 70 lines of Python
- 1 page
- 1 server function
- 929 B page JS (gzip)
This example needs its server (database, sessions or server functions). Run it locally:
Terminal
pyweb dev examples/showcase/app.pyweb#Source
examples/showcase/app.pyweb
"""One page, every layer: reactive state, derived values, server search."""
from pyweb import App, server
app = App(title="PyWeb showcase", stylesheets=["/static/style.css"])
PRODUCTS = [
{"name": "Keyboard", "price": 89},
{"name": "Monitor", "price": 249},
{"name": "Mouse", "price": 39},
{"name": "Headphones", "price": 129},
{"name": "Webcam", "price": 69},
]
@server
def search_products(q: str) -> list:
q = q.strip().lower()
return [p for p in PRODUCTS if q in p["name"].lower()]
@app.page("/")
def Home():
count = 0
price = 100
quantity = 2
total = price * quantity
q = ""
results = search_products("")
def increment():
count += 1
def reset():
count = 0
def search():
results = search_products(q)
<main class="wrap">
<nav class="nav">
<span class="logo">PyWeb</span>
<span class="tag">Python from browser to database</span>
</nav>
<section class="hero">
<h1>One language. Every layer.</h1>
<p>Plain Python variables become reactive state. Server functions become typed RPC.</p>
</section>
<section class="card">
<h2>Reactive counter</h2>
<button id="inc" onclick={increment} class="btn">Count: {count}</button>
<button onclick={reset} class="btn ghost" disabled={count == 0}>Reset</button>
<p class="hint">count is a signal: only this button's text node updates.</p>
</section>
<section class="card">
<h2>Derived values</h2>
<label>Quantity <input id="qty" type="number" bind={quantity} /></label>
<p id="total">Total: ${total}</p>
<p class="hint">total = price * quantity recomputes when quantity changes.</p>
</section>
<section class="card">
<h2>Server search</h2>
<input id="q" bind={q} oninput={search} placeholder="Search products…" />
<ul id="results">
for p in results:
<li>{p["name"]} <span class="hint">${p["price"]}</span></li>
</ul>
<p class="hint">Each keystroke calls search_products() on the server over RPC.</p>
</section>
</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 count reactive state: assigned in increment(); literal initial value
browser price constant
browser quantity reactive state: bound to an input (line 57); literal initial value
browser total derived from quantity; recomputed when they change
browser q reactive state: bound to an input (line 63); literal initial value
browser results reactive state: assigned in search(); initial value computed on the server and sent
browser increment event handler (compiled to JavaScript)
browser reset event handler (compiled to JavaScript)
browser search event handler (compiled to JavaScript)
rpc POST /__pyweb/rpc/search_products (q: str) -> list#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";
function Home($s) {
const count = $signal("count" in $s ? $s["count"] : 0);
const price = 100;
const quantity = $signal("quantity" in $s ? $s["quantity"] : 2);
const total = $computed(() => $py.mul(price, quantity()));
const q = $signal("q" in $s ? $s["q"] : "");
const results = $signal("results" in $s ? $s["results"] : null);
function increment() {
count($py.add(count(), 1));
}
function reset() {
count(0);
}
async function search() {
results((await $rpc("search_products", {"q": q()})));
}
return [$h("main", {"class": "wrap"}, () => [
$h("nav", {"class": "nav"}, () => [
$h("span", {"class": "logo"}, () => [$t("PyWeb")]),
$h("span", {"class": "tag"}, () => [$t("Python from browser to database")])
]),
$h("section", {"class": "hero"}, () => [
$h("h1", null, () => [$t("One language. Every layer.")]),
$h("p", null, () => [$t("Plain Python variables become reactive state. Server functions become typed RPC.")])
]),
$h("section", {"class": "card"}, () => [
$h("h2", null, () => [$t("Reactive counter")]),
$h("button", {"id": "inc", "onclick": increment, "class": "btn"}, () => [
$t("Count: "),
$dyn(() => count())
]),
$h("button", {"onclick": reset, "class": "btn ghost", "disabled": () => (count() === 0)}, () => [$t("Reset")]),
$h("p", {"class": "hint"}, () => [$t("count is a signal: only this button's text node updates.")])
]),
$h("section", {"class": "card"}, () => [
$h("h2", null, () => [$t("Derived values")]),
$h("label", null, () => [
$t("Quantity "),
$h("input", {"id": "qty", "type": "number", "$bind": quantity})
]),
$h("p", {"id": "total"}, () => [
$t("Total: $"),
$dyn(() => total())
]),
$h("p", {"class": "hint"}, () => [$t("total = price * quantity recomputes when quantity changes.")])
]),
$h("section", {"class": "card"}, () => [
$h("h2", null, () => [$t("Server search")]),
$h("input", {"id": "q", "$bind": q, "oninput": search, "placeholder": "Search products\u2026"}),
$h("ul", {"id": "results"}, () => [$list(() => results(), (p) => [$h("li", null, () => [
$t($py.text($py.at(p, "name"))),
$t(" "),
$h("span", {"class": "hint"}, () => [
$t("$"),
$t($py.text($py.at(p, "price")))
])
])])]),
$h("p", {"class": "hint"}, () => [$t("Each keystroke calls search_products() on the server over RPC.")])
])
])];
}
$mount("Home", Home);