Validators & the validity surface
Tutorial — build a signup form with rich
per-field validation in five steps. By the end you
will have used five universal-core predicates and the
auto-synthesised
.isValid
surface.
What you'll learn:
-
The three "absence" predicates —
req,is some,not— and when each one is the right tool. - Predicate composition: stacking multiple validators on one cell.
-
The auto-synthesised
.isValidsurface — how to gate a submit button on it. -
The
<errors of=…/>element for rendering failure messages.
Step 1: the simplest predicate
Start with one cell and one predicate. Per SPEC §55.2, a validator attaches to a state declaration as a bare attribute inside the angle brackets, after the name. An optional type annotation goes after the closing bracket:
<program>
<email req>: string = ""
<input bind:value=@email placeholder="Email">
<errors of=@email/>
<button disabled=!@email.isValid>Sign up</button>
</program>
Save and look at the browser:
-
The button is disabled (empty string fails
reqper §42.2.5). - As soon as you type one character, the button enables.
-
The
<errors of=@email/>element is empty when the field is valid; on failure it renders the localised error message (default "This field is required.").
That's the validity surface in 6 lines: declaration, predicate, input, error-renderer, submit-gate.
Step 2: choose between
req
and
is some
Per §42.2.5, the two are NOT synonyms:
-
reqfails on"",0,false,[], and the absence valuenot. "I need a meaningful value." -
is somefails ONLY onnot. "I need this to be a defined value, but empty is fine."
Apply the right one per field:
<firstName req>: string = "" // must be meaningful
<middleName is some>: string = "" // empty is fine
<lastName req>: string = "" // must be meaningful
A user with no middle name leaves that field blank;
@middleName.isValid
is
true;
the form submits.
Not in the compiler yet (as of v0.8.0):
is some
on a cell declaration is specified (§55.1) but the current
compiler does not declare a cell written that way — reading
@middleName
fails with
E-STATE-UNDECLARED.
req
and the other predicates work today.
Step 3: stack predicates
The universal-core vocabulary has fourteen predicates (§55.1). Stack them by writing more than one in the decl:
<username req length(>=3, <=20) pattern(/^[a-z0-9_]+$/)>: string = ""
<age req gte(13) lte(120)>: number = 0
Per §55.12 short-circuit composition:
req
(or
is some)
is evaluated first; if it fails, the remaining predicates
are skipped and the error is reported as
.Required
(or
.NotSome).
Otherwise all subsequent predicates run.
The full catalog (§55.1):
req,
is some,
length,
pattern,
min,
max,
gt,
lt,
gte,
lte,
eq,
neq,
oneOf,
notIn.
Step 4: compound validity composes upward
Group the signup fields into a compound cell. The
compound's
.isValid
is auto-synthesised per §55.5: it is
false
if ANY field's validators fail.
<signup>
<email req>: string = ""
<password req length(>=8)>: string = ""
<age req gte(13)>: number = 0
</>
<form>
<input bind:value=@signup.email placeholder="Email">
<errors of=@signup.email/>
<input bind:value=@signup.password type="password">
<errors of=@signup.password/>
<input bind:value=@signup.age type="number">
<errors of=@signup.age/>
<button disabled=!@signup.isValid>Sign up</button>
</form>
Per §55.5/§55.6, the compound carries
@signup.isValid
AND each field carries its own
@signup.email.isValid
/
.password.isValid
/
.age.isValid.
All synthesised at compile time. None of these properties
were declared by you.
Or generate the form from a type
When the fields already live on a struct, the
<formFor> element (from
scrml:data) builds the form, its state cell and its
submit path from the type:
<program>
import { formFor } from "scrml:data"
type Signup:struct = {
email: string req
password: string req length(>=8)
}
function saveSignup(s) { console.log(s) }
<formFor for=Signup onsubmit=saveSignup/>
</program>
Its output-side twin, <tableFor for=User rows=@users/>,
renders a table from the same kind of struct.
Step 5: customise the error messages
Per §55.10, error messages resolve through a 4-level chain. The first that matches wins. For per-decl customisation, pass the message as a string argument to the predicate:
<email req("Please enter your email.")>: string = ""
For project-level messages (shared across many fields),
register a catalogue via
scrml:data's
registerMessages
API (§41.12) — import it and call it from a top-level
${ }
block. The compiler resolves per-field at compile time.
Errors are typed values, not strings
Per §55.9, the per-field
.errors
array is typed as
ValidationError[]
— a tagged enum whose variants name the predicate
that failed:
function report(e: ValidationError) {
match e {
.Required :> logEvent("email-blank")
.PatternMismatch :> logEvent("email-malformed")
.LengthFailed :> logEvent("email-too-short")
else :> logEvent("email-other")
}
}
Consumers pattern-match on the tag; the message string is one rendering. Analytics, custom UIs, and accessibility layers all read from the same typed surface.
Not reliable yet (as of v0.8.0): the block
above compiles, but in the current compiler a
match
over a
ValidationError
value does not match the error objects the runtime produces, so
every error falls to the
else
arm. Render errors with
<errors of=…/>,
which does work, until that is fixed.
Where validators don't apply
-
Derived cells. Per §55.14, a
const <x> = exprcell carrying validators fires E-DERIVED-WITH-VALIDATORS . Validators are for inputs, not for outputs derived from inputs. -
Single-value top-level cells without validators.
A cell like
<count> = 0does NOT auto-synthesise an.isValidsurface (per §55.5 Edge A). Compound cells always synthesise; single-value cells require at least one predicate to opt in.
Where to go next
- <errors of=…/> — full reference for the element you used in every step.
- req / is some — the two predicates this tutorial leans on.
- Server-boundary tutorial — the partner walkthrough on the full-stack side.