scrml.dev v0.7.1
scrml.dev › Learn › Validators

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 .isValid surface — 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 req per §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:

  • req fails on "", 0, false, [], and the absence value not. "I need a meaningful value."
  • is some fails ONLY on not. "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> = expr cell 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> = 0 does NOT auto-synthesise an .isValid surface (per §55.5 Edge A). Compound cells always synthesise; single-value cells require at least one predicate to opt in.

Where to go next

← Learn