Overview
Basics
Defining a simple form
Forms are defined as lists of widgets. Each widget represents an input field or a display element.
To create a form, define your pages as lists of widgets and use the run
function to execute the form.
from abstra.forms import NumberInput, TextInput, run
# Define personal details page
personal_details = [
TextInput("First Name"),
NumberInput("Age")
]
# Define company details page
company_details = [
TextInput("Company"),
TextInput("Job Title")
]
# Run the form with the defined pages
state = run([personal_details, company_details])
# Access the form results
print(state)
# Result:
# {'First Name': 'John', 'Age': 30, 'Company': 'Abstra', 'Job Title': 'Manager'}
In this example, the form consists of two pages: one for personal details and another for company details. The run
function processes the form and returns a state
dictionary containing the user's inputs.
Using Keys for Better Code Readability
To improve code readability and avoid relying on long labels, you can use the key
parameter in input widgets. This allows you to reference the input values using shorter, more meaningful keys.
from abstra.forms import NumberInput, TextInput, run
# Define personal details page with keys
personal_details = [
TextInput("Hello, what is your name?", key="name"),
NumberInput("And how old are you?", key="age")
]
# Define company details page with keys
company_details = [
TextInput("What is the name of your company?", key="company_name"),
TextInput("What is your job title in the company?", key="job_title")
]
# Run the form and access results using keys
state = run([personal_details, company_details])
job_title = state["job_title"] # Accessing the job title using the key
By using keys, you can make your code cleaner and easier to maintain.
Reactive Pages
Reactive pages allow you to dynamically update the form based on user input. This is achieved by defining a page as a function that returns a list of widgets. The function can access the
current state
and modify the page accordingly.
Example: Reactive Rating Page
from abstra.forms import RatingInput, TextOutput, run
def rating_page(state):
page = [RatingInput("How would you rate your experience?", key="rating")]
if state["rating"] is None:
return page # Show only the rating input initially
if state["rating"] < 3:
page.append(TextOutput("We're sorry 😢")) # Add a message for low ratings
return page
# Run the reactive form
run([rating_page])
- Initially,
state["rating"]
isNone
, so the page only displays theRatingInput
widget. - When the user provides a rating, the page is re-evaluated.
- If the rating is less than 3, a message is appended to the page and displayed immediately.
Sending and receiving data
Before or after the user fills the form
You can perform API calls or other operations before or after the form is displayed to the user.
from abstra.forms import NumberInput, run
import my_api # Example API module
# Fetch data before showing the form
username = my_api.get_username()
# Define the form
personal_details = [NumberInput(f"How old are you, {username}?", key="age")]
# Run the form
state = run([personal_details])
# Save data after the form is submitted
my_api.set_age(state["age"])
After a page is advanced
You can also load or save data dynamically as the user progresses through the form. Use functions to perform these operations and pass them to run
.
from abstra.forms import NumberInput, TextInput, run
import my_api # Example API module
def email_page(state):
return [TextInput("Enter email", key="email")]
def load_user_data(state):
# Use previous page data to load the user
state["user"] = my_api.load_user(state["email"])
return # This function won't be shown as a form step
def age_page(state):
username = state["user"].get("name", "User")
return [NumberInput(f"How old are you, {username}?", key="age")]
def save_user_data(state):
# Save the age answer from previous page
my_api.save_age(state["email"], state["age"])
return # This function won't be shown as a form step
# Run the form with dynamic data handling
run([email_page, load_user_data, age_page, save_user_data])
While a page is being filled
You can also load data dynamically while the user is filling out a page. This is useful for pre-filling fields or validating inputs.
from abstra.forms import NumberInput, TextInput, run
import my_api # Example API module
def reactive_page(state):
company_id = state.get("company_id")
if company_id and len(company_id) == 10:
# Fetch company name only once for each company_id
state["company_name"] = reuse(my_api.fetch, company_id)
return [
TextInput("Company ID", key="company_id"),
TextInput("Company Name", key="company_name"),
]
# Run the reactive form
run([reactive_page])
In this example, “company name” field is automatically filled based on API data when “company id” is filled by the user.
Note the use of reuse
wrapping this the API call. Without it, each time the user pressed a key (and the corresponding state changed) an API call would be made slowing things down. This guarantees that my_api.fetch
is only called once for each company_id
.
Displaying a Progress Bar for Long-Running Operations
Sometimes, data processing may take a long time to load, process, or save. A progress bar provides useful feedback to the user. Here’s an example:
from time import sleep
from abstra.forms import run, ProgressOutput
def show_progress_bar(state):
for i in range(1, 11):
sleep(1) # Simulating a time-consuming process
yield [ProgressOutput(current=i, total=10)]
run([show_progress_bar])
- Use
yield
instead ofreturn
to allow the function to continue running. - You can yield any type of output widget dynamically.
Custom Buttons
For complex interactions, default "Next" and "Back" buttons may not be sufficient. You can define custom buttons as shown below:
from abstra.forms import run, TextInput, Button
import my_api
def deal_decision(state):
if state.get("Approve"):
my_api.approve(state["deal_id"])
return
if state.get("Reject"):
my_api.reject(state["deal_id"])
return
return [
TextInput(label="Enter deal ID", key="deal_id")
],
[Button("Approve"), Button("Reject")]
state = run([deal_decision])
- When a button is pressed, the function runs with its value set to
True
. - Returning widgets advances the page automatically.
Start form with pre-filled fields
If you want fields to be pre-filled with existing values, pass an initial state
dictionary to run
:
import my_api
from abstra.forms import NumberInput, TextInput, run
initial_data = my_api.get_data()
# Example: {'name': 'John Doe', 'company': 'ACME'}
personal_details = [
TextInput("First Name", key="name"),
NumberInput("Age", key="age")
]
company_details = [
TextInput("Company", key="company"),
TextInput("Job Title", key="job_title")
]
# Run form with predefined data; missing fields will be left empty
state = run([personal_details, company_details], initial_data)
- Predefined values appear in the form automatically.
- Any missing data will leave the corresponding input empty for user input.
Custom Validation
Some input widgets have built-in validation (e.g., EmailInput
, NumberInput
). However, you can also implement custom validation logic by setting the error
property.
from abstra.forms import TextInput, run
def email_validation_page(state):
email_input = TextInput("Email", key="email")
if state.get("email") == "some@spam.com":
email_input.errors = "We don't allow spam emails!"
# Or
# email_input.errors = ["We don't allow spam emails!", "Please use a different email."]
return [email_input]
run([email_validation_page])
- Assign an error message to
error
for custom validation feedback. - The error message will be displayed below the input field.