So far, most of our programs have been straightforward: the program is started, execution occurs, and then the program ends naturally. But most programs that you use every day are not like that. You start the program, a window appears, and then the program waits for you to do something. Each time you click a button, or type some text, or scroll with your mouse, the program reacts in an appropriate way. These input events trigger output, and the program allows you to do these interactions as many as you’d like. The web browser you are using right now is a great example of a program like this. You can keep scrolling and clicking and typing, until you eventually close the program. Mastering this new model will greatly enhance the kinds of programs that we can build. We will build these programs using Drafter. Before we can talk about interactivity, however, we need to talk about state.
from drafter import *
@route
def index(state: int) -> Page:
content = Div("Current number: ", bold(str(state)))
return Page(state, content)
start_server(7)
A central part of any program is the state. Simply put, the state is the value of all the variables at any given point in time. Each statement of a program is executed in the context of the current state. As your program runs, the state can change, and those changes can affect how the program behaves. Assignment statements can be used to update the state, for example, using operators and function calls.
State in Drafter is an absolutely essential part of the program. Route functions take the current state as an argument and return a new state as part of the Page (along with the content). Previously, we had no state in our Drafter programs, but now we can use state to create more dynamic and interactive applications. So now we have TWO parameters for the Page function. To make this more obvious, we have used a temporary variable content to hold the content of the page (although that could have been passed in directly).
In the Drafter program shown here, the state is represented by the state parameter in the index function. When we call the start_server function, we pass in an initial state of 7. We can use the state parameter inside of the body of the index function to customize the content of the page, in this case rendering the text “Current number: 7”.
from drafter import *
@route
def index(state: int) -> Page:
next_state = 1 + state
content = Div(
"Current Number: ", bold(str(state)),
Button("Increase", "index")
)
return Page(next_state, content)
start_server(7)
To manipulate the state in Drafter, you can use the Button component to create interactive elements. When the button is clicked, it will call the specified route function with the current state.
Drafter buttons take in the label for the button and the name of a route. Notice we said a function name, not a function call. By providing the name of the route instead of calling the route function, we delay the execution of that function until the button is clicked. When the button is clicked, Drafter will call the index function with the current state, which creates a new Page with the latest state (8) displayed and creates a new state that has been incremented by one.
The result is that every time you click the button, the state is updated and the page is re-rendered with the new state. So when the page first loads, the number 7 is displayed and the state is 8. Then the number 8 is displayed and the state is 9, and so on.
from drafter import *
@route
def index(state: int) -> Page:
content = Div(
"Current number: ",
bold(str(state)),
Button("Increase", "increase_state"),
Button("Decrease", "decrease_state"),
)
return Page(state, content)
@route
def increase_state(state: int) -> Page:
return index(state + 1)
@route
def decrease_state(state: int) -> Page:
return index(state - 1)
start_server(7)
Things might be a little clearer if we look at a more sophisticated application with multiple buttons and multiple routes. In this example, we have buttons to increase and decrease the state, each with their own route function. When the application loads, it displays the current number and the state stays the same. When you click a button, Drafter will call the corresponding route function with the current state, allowing you to modify the state in different ways. Those route functions in turn reuse the index function to generate the page content. So if you click the “Increase” button, the state will be incremented by 1, and if you click the “Decrease” button, the state will be decremented by 1.
from drafter import *
@route
def index(state: str) -> Page:
content = Div(
"Guess the password",
TextBox("guess"),
Button("Submit", "check_password")
)
return Page(state, content)
@route
def check_password(state: str, guess: str) -> Page:
if state == guess:
content = Div("Correct! The password is: ", bold(guess))
else:
content = Div("Incorrect!", Button("Try again", "index"))
return Page(state, content)
start_server("grapefruit")
Since routes are just functions, you can use them to encapsulate any logic you need for your application. This means we can use if statements, for example to return different page content based on the current state or user input. To do so, we must also learn about another new component: TextBox. The TextBox component function takes in a unique name for the textbox (which is not shown to the user). That name will be matched to a parameter in a route function, if any buttons are pressed.
In this example, the guess parameter in the check_password function will be populated with the value entered in the TextBox when the “Submit” button is clicked. This allows the check_password function to access the user’s input and use an if statement to check if they have correctly guessed the password (which is stored in state). Depending on whether the guess is correct or not, the function can return different content to the user. The incorrect guess case is handled by displaying a message and providing a button to restart the guessing process.
A confusing aspect of Drafter’s routing system is that it can be easy to lose track of the current state and the flow of data between different route functions. You have to think critically about whether you need to update the state in the current route, or in a route linked by a button. The general rule of thumb is that the index function does not update the state; instead, you should have buttons that link to other routes which update the state, possibly using parameters.
You also need to separate out the state from the other parameters being passed to the route functions. The state always represents the same type of thing, even if its specific value changes. The state is meant to be permanently available through the application. The parameters, on the other hand, are completely dependent on what the user is currently interacting with. They can change from one route to another, and are often used to capture user input or other temporary data. Data that gets captured from the user might update the current state, so it’s important to keep track of both the state and the parameters as the user interacts with the application.
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.