Skip to main content

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])
  1. Initially, state["rating"] is None, so the page only displays the RatingInput widget.
  2. When the user provides a rating, the page is re-evaluated.
  3. 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])

info
  • Use yield instead of return 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])
info
  • 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)

info
  • 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])

info
  • Assign an error message to error for custom validation feedback.
  • The error message will be displayed below the input field.