Remember a score or choice
Goal
You want your app to keep a value between clicks and across pages: a score, a name, or a chosen option.
Before you start
You can build pages with buttons. Everything your app remembers lives
in your State dataclass, so remembering something new always starts
the same way: add a field.
The smallest version
To remember one more thing, add one more field, give it a starting value
in start_server(...), and assign it in the routes that change it:
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
score: int
best: int
@route
def index(state: State) -> Page:
return Page(state, [
"Score: " + str(state.score) + "\n",
"Best so far: " + str(state.best) + "\n",
Button("Score a point", "add_point"),
Button("Lose a point", "lose_point")
])
@route
def add_point(state: State) -> Page:
state.score = state.score + 1
if state.score > state.best:
state.best = state.score
return index(state)
@route
def lose_point(state: State) -> Page:
state.score = state.score - 1
if state.score < 0:
state.score = 0
return index(state)
assert_state(add_point(State(0, 0)), State(1, 1))
assert_state(add_point(State(2, 9)), State(3, 9))
assert_state(lose_point(State(3, 9)), State(2, 9))
start_server(State(0, 0))
Score a few points, then lose a few, and notice that best never goes
down. It only ever grows, because add_point is the only route that
assigns it, and that route compares the score first. Deciding which
routes may change a field is most of the work of state design.
Recipe: save a choice, use it on another page
A value saved on one page is available on every page, because every route receives the same state:
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
difficulty: str
@route
def index(state: State) -> Page:
return Page(state, [
"Pick a difficulty:",
SelectBox("choice", ["easy", "normal", "hard"], state.difficulty),
"\n",
Button("Save and continue", "save_difficulty")
])
@route
def save_difficulty(state: State, choice: str) -> Page:
state.difficulty = choice
return game(state)
@route
def game(state: State) -> Page:
return Page(state, [
"Now playing on " + state.difficulty + " mode.\n",
Button("Change difficulty", "index")
])
assert_state(save_difficulty(State("easy"), "hard"), State("hard"))
start_server(State("normal"))
Two details here are worth copying. The SelectBox receives
state.difficulty as its starting value, so the form shows the current
choice. And save_difficulty only does the saving, then calls game
to build the page, which keeps each route to one job.
Recipe: reset to the starting values
A reset is a route that assigns every field back to its starting value.
Write the starting values once, in a helper, so the reset and
start_server can never disagree:
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
score: int
round_number: int
def new_game() -> State:
return State(0, 1)
@route
def index(state: State) -> Page:
return Page(state, [
"Round " + str(state.round_number) + ", score " + str(state.score) + ".\n",
Button("Win the round", "win"),
Button("Start over", "reset")
])
@route
def win(state: State) -> Page:
state.score = state.score + 10
state.round_number = state.round_number + 1
return index(state)
@route
def reset(state: State) -> Page:
fresh = new_game()
state.score = fresh.score
state.round_number = fresh.round_number
return index(state)
assert_state(reset(State(70, 8)), State(0, 1))
start_server(new_game())
What survives a reload
Nothing does. State lives in the browser's memory, and reloading the
tab starts the app again from the values in start_server(...). For
course projects, that behavior is normal and expected. If your users
might be surprised by it, say so on the page. The full story is in
How Drafter works.
Common problems
- The value resets when you did not expect it: you reloaded the tab, or a route assigns the field unconditionally. Check the debug panel's History tab to see which route changed it.
- A field never changes: no route assigns it, or the route that should assign it is never reached. The History tab shows which routes actually ran.
- You added a field and old tests broke: every
State(...)call in your tests needs the new field's value too. The test failure lists the difference. - Two pages disagree about a value: two pages that both read state
cannot actually disagree. If one looks stale, it is showing text that
was built from an old value instead of reading
statefresh in the route.
Understand it
State explains the state loop, what belongs in fields, and what the back button does to them.
See another example
The virtual pet manages three fields and four routes, with rules that keep values in range.
Look it up
start_server for initial state, and
Testing functions for
assert_state.