from drafter import *
@route
def index(state: str) -> Page:
return Page(
state+"!",
Div(state, Button("Make it louder.", "index"))
)
start_server("Hey!")
We have previously used Drafter to make simple interactive applications with buttons and textboxes. A central premise in those applications is the current state, which at that point could only be a single value (e.g., a string or an integer). The example Drafter application shown here has a state that is a string, and clicking the button reloads the route with that string plus an exclamation mark. This is a simple example of how Drafter applications can have state, but it is not very useful. With dataclasses, we can represent more complex state, such as a combination of multiple values. This allows us to create more sophisticated applications that can manage and manipulate state more easily.
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
name: str
age: int
@route
def index(state: State) -> Page:
return Page(state, Div(
TextBox("name", state.name),
TextBox("age", state.age),
Button("Change", "update")
))
@route
def update(state: State, name: str, age: int) -> Page:
state.name = name
state.age = age
return index(state)
start_server(State("Alice", 30))
The primary usage of dataclasses with Drafter is that they allow us to define complex state, usually by creating a new dataclass named State. This dataclass can have multiple fields, each representing a different aspect of the application’s state. By using a dataclass for state, we can easily pass around and manipulate complex state objects within our Drafter application.
In the example shown here, we define a State dataclass with two fields: name and age. The name field is a string representing the user’s name, while the age field is an integer representing the user’s age. The initial state is created by instantiating the State dataclass with the user’s name and age and passing it to the start_server function.
Then, each route gets the current state as a parameter, allowing it to access and modify the state as needed.
The index route uses the current state to populate the form fields with the user’s name and age, allowing them to edit their information. When the user submits the form, the update route is called with the new values, which updates the state and returns the updated page.
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
name: str
age: int
@route
def index(state: State) -> Page:
return Page(state, Div(
TextBox("new_name", state.name),
TextBox("new_age", state.age),
Button("Change", "update")
))
@route
def update(state: State, new_name: str, new_age: int) -> Page:
return index(State(new_name, new_age))
start_server(State("Alice", 30))
Drafter is designed to handle Dataclass instances both mutably and immutably, for convenience. This means that you can choose to modify the state directly or create a new instance with the updated values. This flexibility allows for different programming styles and can make it easier to reason about state changes in your application.
In this slightly modified example, we can see the immutability in action. Instead of changing the existing State instance, we create a new one with the updated values. We also renamed the parameters to clearly highlight the difference between the old and new values (although this does not actually change the behavior of the program).
Both the old and new versions work the same way, so it is really up to you which style you prefer.
This flexibility is unusual for most applications, and can give you a false confidence about state management. For non-route functions, you should generally prefer immutability to avoid unintended side effects. If you are mutating state, you need to be careful about when you choose to mutate the state and ensure that you are not introducing bugs or inconsistencies.
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
player1: int
player2: int
@route
def index(state: State) -> Page:
return Page(state, Div(
Button("Player 1: " + str(state.player1), "plus_player1"),
Button("Player 2: " + str(state.player2), "plus_player2"),
))
@route
def plus_player1(state: State) -> Page:
state.player1 += 1
return index(state)
@route
def plus_player2(state: State) -> Page:
state.player2 += 1
return index(state)
start_server(State(0, 0))
Let’s look at another complicated state example. Here we have a State dataclass that keeps track of two players’ scores in a game. The index route displays the current scores and provides buttons to increment each player’s score. The plus_player1 and plus_player2 routes handle the button clicks and update the state accordingly.
There is no limit to how many times the players can be incremented, and the state will always reflect the current scores accurately.
Page(state, content)Button(label, url)TextBox(name, default_value)It turns out that Drafter has been using Dataclasses all along. The Page function is really the Page dataclass, which has fields for the state and content.
Similarly, the various components that we’ve seen so far (e.g., Div, Button, TextBox) are also dataclasses with their own fields.
This is why their names began with a capital letter, since that’s the convention for dataclass names in Python.
from drafter import *
from dataclasses import dataclass
from bakery import assert_equal
@dataclass
class State:
player1: int
player2: int
@route
def index(state: State) -> Page:
return Page(state, Div(
Button("Player 1: " + str(state.player1), "plus_player1"),
Button("Player 2: " + str(state.player2), "plus_player2"),
))
@route
def plus_player1(state: State) -> Page:
state.player1 += 1
return index(state)
@route
def plus_player2(state: State) -> Page:
state.player2 += 1
return index(state)
assert_equal(plus_player1(State(0, 0)).state, State(1, 0))
Because Page is a dataclass, we can easily create instances of it with different state and content values.
Similarly, we can access the state and content of a Page instance using dot notation.
This means that we do not have to write long tests that check the entire structure of the Page instance.
For example, the test shown here only checks the state, without worrying about the content.
State dataclass to encapsulate all relevant state information.Drafter is not installed on Dodona, so there is no exercise to submit here: this page is for reading and studying the code, not for running it.