Forms and User Input
Building interactive forms with validation and collecting user input.
Available Input Components
| Type | Component | Use Case |
|---|---|---|
text | TextInput | Single-line text (names, emails) |
textarea | TextareaInput | Multi-line text (comments, descriptions) |
number | NumberInput | Numeric values with min/max/step |
date | DateInput | Date selection |
search | SearchInput | Search with debounce |
secret | SecretInput | Passwords, API keys (value hidden) |
select | Select | Single choice dropdown |
multiselect | MultiSelect | Multiple choices |
radio | RadioGroup | Visible single choice (small set) |
checkbox | Checkbox | Boolean toggle |
toggle | Toggle | On/off switch |
slider | SliderInput | Single value in range |
range | RangeInput | Two values (min/max range) |
Basic Form Pattern
create_widget(
html='<div id="results">Fill out the form and click Submit</div>',
toolbars=[{
"position": "left",
"items": [
{"type": "text", "label": "Name", "event": "form:name", "placeholder": "Enter your name"},
{"type": "text", "label": "Email", "event": "form:email", "placeholder": "email@example.com"},
{"type": "number", "label": "Age", "event": "form:age", "min": 0, "max": 120, "value": 25},
{"type": "select", "label": "Country", "event": "form:country",
"options": [
{"label": "United States", "value": "us"},
{"label": "United Kingdom", "value": "uk"},
{"label": "Canada", "value": "ca"},
]},
{"type": "button", "label": "Submit", "event": "form:submit", "variant": "primary"},
]
}]
)
Collecting Form Data
From Events
events = get_events(widget_id, clear=True)
# Collect latest values
form_data = {}
for e in events:
if e["event_type"].startswith("form:"):
field = e["event_type"].replace("form:", "")
form_data[field] = e["data"].get("value")
# On submit
if "submit" in form_data:
process_form(form_data)
Tracking State
# Keep running state across event polls
form_state = {}
while True:
events = get_events(widget_id, clear=True)
for e in events:
if e["event_type"].startswith("form:"):
field = e["event_type"].replace("form:", "")
if field != "submit":
form_state[field] = e["data"].get("value")
else:
# Submit with current state
result = process_form(form_state)
set_content(widget_id, component_id="results", text=result)
time.sleep(0.1)
Validation Patterns
Show Error Toast
def validate_form(data):
if not data.get("name"):
show_toast(widget_id, message="Name is required", type="error")
return False
if not data.get("email") or "@" not in data["email"]:
show_toast(widget_id, message="Valid email is required", type="error")
return False
return True
Highlight Invalid Fields
def highlight_error(component_id):
set_style(widget_id, component_id=component_id, styles={
"borderColor": "var(--error-color)",
"boxShadow": "0 0 0 2px rgba(239, 68, 68, 0.2)",
})
def clear_error(component_id):
set_style(widget_id, component_id=component_id, styles={
"borderColor": "var(--border-color)",
"boxShadow": "none",
})
Show Inline Errors
# Create form with error placeholders
html = """
<div class="form-group">
<div id="name-error" class="error-message" style="display: none; color: var(--error-color);"></div>
</div>
"""
# Show error
set_content(widget_id, component_id="name-error", text="Name is required")
set_style(widget_id, component_id="name-error", styles={"display": "block"})
# Clear error
set_style(widget_id, component_id="name-error", styles={"display": "none"})
Input Component Details
Text Input
{"type": "text",
"label": "Username",
"event": "username",
"placeholder": "Enter username",
"value": "", # Default value
}
# Event data: {"value": "entered_text"}
Number Input
{"type": "number",
"label": "Quantity",
"event": "quantity",
"min": 1,
"max": 100,
"step": 1,
"value": 10,
}
# Event data: {"value": 10} # Always a number
Select
{"type": "select",
"label": "Category",
"event": "category",
"options": [
{"label": "Option A", "value": "a"},
{"label": "Option B", "value": "b"},
{"label": "Option C", "value": "c"},
],
"value": "a", # Default selection
}
# Event data: {"value": "a", "label": "Option A"}
MultiSelect
{"type": "multiselect",
"label": "Tags",
"event": "tags",
"options": [
{"label": "Important", "value": "important"},
{"label": "Urgent", "value": "urgent"},
{"label": "Review", "value": "review"},
],
"value": ["important"], # Default selections (array)
}
# Event data: {"value": ["important", "urgent"]}
Radio Group
{"type": "radio",
"label": "Priority",
"event": "priority",
"options": [
{"label": "Low", "value": "low"},
{"label": "Medium", "value": "medium"},
{"label": "High", "value": "high"},
],
"value": "medium",
}
# Event data: {"value": "medium"}
Toggle / Checkbox
{"type": "toggle", # or "checkbox"
"label": "Enable notifications",
"event": "notifications",
"value": True, # Default state
}
# Event data: {"value": true} # Boolean
Slider
{"type": "slider",
"label": "Volume",
"event": "volume",
"min": 0,
"max": 100,
"step": 5,
"value": 50,
}
# Event data: {"value": 50}
Range (Two Values)
{"type": "range",
"label": "Price Range",
"event": "price_range",
"min": 0,
"max": 1000,
"step": 10,
"value": [100, 500], # [min_value, max_value]
}
# Event data: {"value": [100, 500]}
Date
{"type": "date",
"label": "Start Date",
"event": "start_date",
"value": "2024-01-01", # ISO format
}
# Event data: {"value": "2024-01-15"}
Search (with debounce)
{"type": "search",
"label": "Search",
"event": "search",
"placeholder": "Type to search...",
"debounce": 300, # ms delay before firing event
}
# Event data: {"value": "search query"}
Secret Input (Passwords / API Keys)
{"type": "secret",
"label": "API Key",
"event": "api_key",
"placeholder": "Enter your API key",
}
# Events emitted:
# "api_key" - Value changed (value in event data, but NEVER rendered in HTML)
# "api_key:reveal" - User clicked reveal button
# "api_key:copy" - User clicked copy button
Handling Secret Events
events = get_events(widget_id, clear=True)
for e in events:
if e["event_type"] == "api_key":
# Store securely - never log or display
api_key = e["data"]["value"]
save_api_key_securely(api_key)
elif e["event_type"] == "api_key:reveal":
# User wants to see the key - show toast or handle appropriately
show_toast(widget_id, message="Key revealed in input", type="info")
elif e["event_type"] == "api_key:copy":
show_toast(widget_id, message="API key copied to clipboard", type="success")
Complete Form Example
from pywry import PyWry
from pywry.toolbar import Div, Button, Toolbar
app = PyWry()
content = Div(
content="""
<div style="padding: 20px;">
<h2>Registration Form</h2>
<div id="message" style="margin: 16px 0; padding: 12px; display: none;"></div>
</div>
""",
component_id="form-container",
)
widget = app.show(
html=content.build_html(),
height=500,
toolbars=[
Toolbar(
position="left",
items=[
{"type": "text", "label": "Full Name", "event": "form:name"},
{"type": "text", "lab