Validation#
Input fields can check what the user types against a set of rules. A rule has a
type ('required', 'email', …), an optional custom message, and a
trigger that decides when it runs. Rules run in the order you add them and stop
at the first failure, and the field emits valid / invalid events you can
listen to. The same rule engine also works standalone, with no widget attached.
Adding rules to a field#
Call add_validation_rule() on a field with a rule type and its options. Add
as many as you like — they are checked in order, and the first failure wins:
email = bs.TextField(label="Email")
email.add_validation_rule("required")
email.add_validation_rule("email", message="Enter a valid email.")
name = bs.TextField(label="Name")
name.add_validation_rule("stringLength", min=2, max=50)
Because 'required' is the most common rule, fields accept it as a
required=True constructor shortcut — it adds the rule and appends an asterisk
to the label:
email = bs.TextField(label="Email", required=True) # same as add_validation_rule("required")
In a Form, set required=True on the field’s
FieldItem for the same effect — see Building Forms.
Built-in rule types#
Type |
Options |
Checks that the value… |
|---|---|---|
|
— |
is not empty or whitespace. |
|
— |
looks like an email address. |
|
|
has a length within the given bounds. |
|
|
matches the regular expression. |
|
|
equals another field’s value (confirm-password, etc.). |
|
|
satisfies your own predicate. |
Every rule also accepts message (override the default text) and trigger
(see below).
Custom rules#
A 'custom' rule runs any predicate that takes the value and returns a bool.
Pair it with a message so the user knows what went wrong:
code = bs.TextField(label="Invite code")
code.add_validation_rule(
"custom",
func=lambda v: v.isdigit() and len(v) == 6,
message="The code is 6 digits.",
)
Confirming a second field#
A 'compare' rule passes only when the value matches other_field. The
other field can be another field widget, a Signal, or any zero-argument
callable — its value is read fresh each time the rule runs, so it always
compares against the current text:
password = bs.PasswordField(label="Password")
password.add_validation_rule("stringLength", min=8)
confirm = bs.PasswordField(label="Confirm password")
confirm.add_validation_rule(
"compare",
other_field=password, # a field widget…
message="Passwords don't match.",
)
# …or compare against a Signal / callable instead of a widget
pin = bs.Signal("")
confirm.add_validation_rule("compare", other_field=pin)
confirm.add_validation_rule("compare", other_field=lambda: expected_value())
When does a rule run?#
The trigger controls when a rule fires during normal typing. Each rule type
has a sensible default, which you can override per rule:
Trigger |
Runs… |
|---|---|
|
as the user types and when the field loses focus. Default for
|
|
only as the user types. |
|
only when the field loses focus. Default for |
|
never automatically — only when you call |
Auto-validation is debounced, so a fast typist doesn’t trigger a check on every keystroke. Override the trigger when the default doesn’t fit — for example, to check a length rule live:
name.add_validation_rule("stringLength", min=2, max=50, trigger="always")
Reacting to validation#
Run every rule on demand with validate() (regardless of trigger); it returns
True when they all pass. Listen for outcomes with on_valid /
on_invalid — both receive a ValidationEvent carrying value, is_valid, and
message:
status = bs.Label("", accent="danger")
def show(e):
status.text = e.message # "" when valid
email.on_invalid(show)
email.on_valid(show)
if email.validate(): # runs every rule, returns True if all pass
submit()
Validating a whole form before submit#
A common pattern is to gate a submit button on every field passing. validate()
runs all rules and surfaces the messages through the fields’ own events, so the
guard itself stays short:
fields = [email, password, confirm]
def on_submit():
if all(f.validate() for f in fields): # each shows its own error
save(email.value, password.value)
bs.Button("Create account", on_click=on_submit)
Because all() short-circuits, call validate() on each field in a list
comprehension first if you want every field to display its error at once:
results = [f.validate() for f in fields] # validate them all
if all(results):
save(...)
Standalone rules#
The rule engine doesn’t need a widget. Construct a ValidationRule and call validate() directly — useful
for checking values from config, a CLI, or a background job:
from bootstack.validation import ValidationRule
rule = ValidationRule("stringLength", min=3, max=8)
result = rule.validate("hi")
result.is_valid # False
result.message # "Enter between 3 and 8 characters."
ValidationRule("email").validate("a@b.co").is_valid # True
ValidationRule("compare", other_field="yes").validate("no").is_valid # False
See also#
Events — the
ValidationEventpayload and theon_*model.TextField — the field widget these rules attach to.
Form Dialog — collects and validates a set of fields at once.
API reference#
The complete reference for ValidationRule and ValidationResult lives in
Validation. At a glance:
A single validation rule that can be applied to a string value. |
|
The outcome of a single validation check. |