scrml.dev v0.8.0
Validators & the validity surface · Learn scrml
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 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 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 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> = 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