Forms and User Input
SkillMediaPyWry is a cross-platform app factory, rendering engine and UI toolkit for Python that produces native desktop, web, and notebook experiences from a single API.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Forms and User Input skill
What this skill tells your AI
The instructions your AI receives, as published by deeleeramone/pywry in pywry/pywry/mcp/skills/forms_and_inputs/SKILL.md and read by ahel’s review.
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", "label": "Email", "event": "form:email"},
{"type": "secret", "label": "Password", "event": "form:password"},
{"type": "select", "label": "Role", "event": "form:role", "options": [
{"label": "User", "value": "user"},
{"label": "Admin", "value": "admin"},
]},
{"type": "toggle", "label": "Accept Terms", "event": "form:terms"},
{"type": "button", "label": "Register", "event": "form:submit", "variant": "primary"},
]
)
]
)
# Handle form
form_state = {}
while True:
events = widget.get_events(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:
# Validate
if not form_state.get("name"):
widget.show_toast("Name is required", type="error")
elif not form_state.get("terms"):
widget.show_toast("Please accept terms", type="error")
else:
# Success
widget.set_content(
component_id="message",
html=f"<p style='color: var(--success-color);'>Welcome, {form_state['name']}!</p>"
)
widget.set_style(component_id="message", styles={"display": "block"})
time.sleep(0.1)
Signals
- GitHub stars
- 93
- Forks
- 6
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
forms-and-inputs- Source
- github.com/deeleeramone/pywry