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 after the type:
<program>
<email: string req> = ""
<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: string req> = "" // must be meaningful
<middleName: string is some> = "" // empty is fine
<lastName: string req> = "" // must be meaningful
A user with no middle name leaves that field blank;
@middleName.isValid
is
true;
the form submits.
Step 3: stack predicates
The universal-core vocabulary has fourteen predicates (§55.1). Stack them by writing more than one in the decl:
<username: string req length(>=3, <=20) pattern(/^[a-z0-9_]+$/)> = ""
<age: number req gte(13) lte(120)> = 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: string req> = ""
<password: string req length(>=8)> = ""
<age: number req gte(13)> = 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.
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, write the message inline after the predicate:
<email: string req:"Please enter your email."> = ""
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:
${
@signup.email.errors.forEach(e => match e {
.Required => logEvent("email-blank")
.PatternMismatch(p)=> logEvent("email-malformed", p)
.LengthFailed(p) => logEvent("email-too-short", p)
})
}
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.
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.