Prototyper UI

DX Helpers

Convenience builders for actions, validation checks, and visibility conditions.

Stream UI specs are plain JSON objects, so you can always write them by hand. The helper functions below are entirely optional — they provide TypeScript type safety, sensible default messages, and a more compact syntax. Every helper returns the same JSON shape you would write manually.

import { actionBinding, check, visibility } from "@prototyperai/stream-ui/core"

Action Helpers

The actionBinding object provides constructors for ActionBinding objects used in on event handlers and watch bindings.

Reference

MethodSignatureDescription
simple(action, params?)Simple action with optional params
withConfirm(action, confirm, params?)Action with confirmation dialog
withSuccess(action, onSuccess, params?)Action with success handler
withError(action, onError, params?)Action with error handler
setState(path, value)Set a state value at a path
pushState(path, value)Append a value to a state array
removeState(path, index)Remove an element from a state array by index
toggleState(path)Toggle a boolean at a state path
navigate(url)Navigate to a URL

Simple action

The most common case — fire a named action, optionally passing params:

actionBinding.simple("submit")
// → { action: "submit" }

actionBinding.simple("submit", { id: 1 })
// → { action: "submit", params: { id: 1 } }

With confirmation

Show a confirmation dialog before executing the action. The confirm object accepts title, message, confirmLabel, and cancelLabel:

actionBinding.withConfirm("delete", {
  title: "Delete?",
  message: "This cannot be undone.",
  confirmLabel: "Delete",
  cancelLabel: "Keep",
})
// → { action: "delete", confirm: { title: "Delete?", message: "This cannot be undone.", ... } }

With success or error handler

Chain a follow-up action after the primary action succeeds or fails:

actionBinding.withSuccess("save", { navigate: "/dashboard" })
// → { action: "save", onSuccess: { navigate: "/dashboard" } }

actionBinding.withError("save", { set: { "/error": "Save failed" } })
// → { action: "save", onError: { set: { "/error": "Save failed" } } }

Built-in action shorthands

The most common state mutations have dedicated helpers that produce the correct action and params shape automatically.

setState — Set a value at a state path:

// Before (manual)
{ action: "setState", params: { path: "/count", value: 0 } }

// After (helper)
actionBinding.setState("/count", 0)

pushState — Append to a state array:

// Before
{ action: "pushState", params: { path: "/items", value: { name: "New item" } } }

// After
actionBinding.pushState("/items", { name: "New item" })

removeState — Remove from a state array by index:

// Before
{ action: "removeState", params: { path: "/items", index: 2 } }

// After
actionBinding.removeState("/items", 2)

toggleState — Toggle a boolean:

// Before
{ action: "toggleState", params: { path: "/sidebar/open" } }

// After
actionBinding.toggleState("/sidebar/open")

navigate — Navigate to a URL:

// Before
{ action: "navigate", params: { url: "/dashboard" } }

// After
actionBinding.navigate("/dashboard")

Validation Helpers

The check object provides constructors for ValidationCheck objects. Each helper returns a check with a sensible default error message that you can override with the optional message parameter.

Reference

MethodSignatureDefault message
required(message?)"This field is required"
email(message?)"Invalid email address"
numeric(message?)"Must be a number"
url(message?)"Invalid URL"
minLength(min, message?)"Must be at least {min} characters"
maxLength(max, message?)"Must be at most {max} characters"
min(min, message?)"Must be at least {min}"
max(max, message?)"Must be at most {max}"
pattern(pattern, message?)"Invalid format"
matches(statePath, message?)"Fields must match"
equalTo(value, message?)"Values must be equal"
lessThan(statePath, message?)"Must be less than reference"
greaterThan(statePath, message?)"Must be greater than reference"
requiredIf(fieldPath, message?)"This field is required"

Basic validators

No-argument validators that check value format:

check.required()
// → { validator: "required", message: "This field is required" }

check.email()
// → { validator: "email", message: "Invalid email address" }

check.numeric()
// → { validator: "numeric", message: "Must be a number" }

check.url()
// → { validator: "url", message: "Invalid URL" }

Length and range

Validators that accept a numeric threshold:

check.minLength(3)
// → { validator: "minLength", message: "Must be at least 3 characters", args: { min: 3 } }

check.maxLength(100)
// → { validator: "maxLength", message: "Must be at most 100 characters", args: { max: 100 } }

check.min(0)
// → { validator: "min", message: "Must be at least 0", args: { min: 0 } }

check.max(999)
// → { validator: "max", message: "Must be at most 999", args: { max: 999 } }

Pattern

Match a value against a regular expression:

check.pattern("^[A-Z]")
// → { validator: "pattern", message: "Invalid format", args: { pattern: "^[A-Z]" } }

Cross-field validation

Compare a value against another field in the state model. These helpers automatically wrap the path in a { $state } reference:

check.matches("/password")
// → { validator: "matches", message: "Fields must match", args: { path: { $state: "/password" } } }

check.lessThan("/max")
// → { validator: "lessThan", message: "Must be less than reference", args: { path: { $state: "/max" } } }

check.greaterThan("/min")
// → { validator: "greaterThan", message: "Must be greater than reference", args: { path: { $state: "/min" } } }

check.equalTo(true)
// → { validator: "equalTo", message: "Values must be equal", args: { value: true } }

Conditional requirement

Require a field only when another field is truthy:

check.requiredIf("/otherField")
// → { validator: "requiredIf", message: "This field is required", args: { path: { $state: "/otherField" } } }

Dynamic args

The min and max helpers accept a { $state } reference instead of a literal number, allowing the threshold to be read from state at validation time:

check.min({ $state: "/settings/minPrice" })
// → { validator: "min", message: "Must be at least the minimum", args: { min: { $state: "/settings/minPrice" } } }

check.max({ $state: "/settings/maxPrice" })
// → { validator: "max", message: "Must be at most the maximum", args: { max: { $state: "/settings/maxPrice" } } }

Custom messages

Every helper accepts an optional message override as its last argument:

check.required("Please enter your name")
// → { validator: "required", message: "Please enter your name" }

check.minLength(8, "Password must be at least 8 characters")
// → { validator: "minLength", message: "Password must be at least 8 characters", args: { min: 8 } }

Visibility Helpers

The visibility object provides constructors for VisibilityCondition objects used in the visible property of elements and the enabled property of validation checks.

Constants

visibility.always  // → true
visibility.never   // → false

State conditions

Check values in the global state model by JSON Pointer path:

visibility.when("/user/isLoggedIn")
// → { $state: "/user/isLoggedIn" }  (truthy check)

visibility.unless("/ui/isLoading")
// → { $state: "/ui/isLoading", not: true }  (falsy check)

visibility.eq("/user/role", "admin")
// → { $state: "/user/role", eq: "admin" }

visibility.neq("/status", "archived")
// → { $state: "/status", neq: "archived" }

visibility.gt("/cart/total", 100)
// → { $state: "/cart/total", gt: 100 }

visibility.gte("/age", 18)
// → { $state: "/age", gte: 18 }

visibility.lt("/inventory", 5)
// → { $state: "/inventory", lt: 5 }

visibility.lte("/score", 50)
// → { $state: "/score", lte: 50 }

Dynamic comparisons

Numeric comparison helpers (gt, gte, lt, lte) accept a { $state } reference to compare against another state value instead of a literal number:

visibility.gt("/price", { $state: "/budget" })
// → { $state: "/price", gt: { $state: "/budget" } }

visibility.lte("/currentStep", { $state: "/maxStep" })
// → { $state: "/currentStep", lte: { $state: "/maxStep" } }

Logical combinators

Combine multiple conditions with AND or OR:

visibility.and(
  visibility.when("/user/isLoggedIn"),
  visibility.eq("/user/role", "admin"),
)
// → { $and: [{ $state: "/user/isLoggedIn" }, { $state: "/user/role", eq: "admin" }] }

visibility.or(
  visibility.eq("/status", "active"),
  visibility.eq("/status", "pending"),
)
// → { $or: [{ $state: "/status", eq: "active" }, { $state: "/status", eq: "pending" }] }

Repeat item conditions

Inside a repeat, use visibility.item to check fields on the current repeat item:

visibility.item.when("completed")
// → { $item: "completed" }  (truthy check on item.completed)

visibility.item.unless("deleted")
// → { $item: "deleted", not: true }

visibility.item.eq("status", "active")
// → { $item: "status", eq: "active" }

visibility.item.neq("status", "archived")
// → { $item: "status", neq: "archived" }

visibility.item.gt("price", 50)
// → { $item: "price", gt: 50 }

The item sub-object supports the same operators as state conditions: when, unless, eq, neq, gt, gte, lt, lte.

Repeat index conditions

Inside a repeat, use visibility.index to check the current zero-based index:

visibility.index.eq(0)
// → { $index: true, eq: 0 }  (first item only)

visibility.index.lt(3)
// → { $index: true, lt: 3 }  (first three items)

visibility.index.neq(0)
// → { $index: true, neq: 0 }  (skip first item)

The index sub-object supports: eq, neq, gt, gte, lt, lte.

Combining conditions

A more complex example showing nested combinators with item and state conditions:

// Show the "remove" button only for non-first completed items when editing is enabled
visibility.and(
  visibility.when("/editing"),
  visibility.index.neq(0),
  visibility.item.eq("status", "completed"),
)

Complete Example

A registration form spec built entirely with helpers:

import { actionBinding, check, visibility } from "@prototyperai/stream-ui/core"
import type { Spec } from "@prototyperai/stream-ui/core"

const spec: Spec = {
  root: "form",
  elements: {
    form: {
      type: "Card",
      props: { title: "Register" },
      children: ["email", "pass", "confirm", "accountType", "company", "submit"],
    },
    email: {
      type: "Input",
      props: {
        label: "Email",
        value: { $bindState: "/email" },
        validation: {
          checks: [check.required(), check.email()],
          validateOn: "blur",
        },
      },
    },
    pass: {
      type: "Input",
      props: {
        label: "Password",
        type: "password",
        value: { $bindState: "/password" },
        validation: {
          checks: [
            check.required(),
            check.minLength(8),
            check.pattern("\\d", "Must contain a number"),
          ],
          validateOn: "change",
        },
      },
    },
    confirm: {
      type: "Input",
      props: {
        label: "Confirm Password",
        type: "password",
        value: { $bindState: "/confirmPassword" },
        validation: {
          checks: [
            check.required("Please confirm your password"),
            check.matches("/password"),
          ],
          validateOn: "change",
        },
      },
    },
    accountType: {
      type: "Select",
      props: {
        label: "Account Type",
        value: { $bindState: "/accountType" },
        options: ["personal", "business"],
      },
    },
    company: {
      type: "Input",
      props: {
        label: "Company Name",
        value: { $bindState: "/company" },
        validation: {
          checks: [check.requiredIf("/accountType")],
          validateOn: "blur",
        },
      },
      visible: visibility.eq("/accountType", "business"),
    },
    submit: {
      type: "Button",
      props: { label: "Register" },
      on: {
        press: actionBinding.simple("register"),
      },
    },
  },
  state: {
    email: "",
    password: "",
    confirmPassword: "",
    accountType: "personal",
    company: "",
  },
}

Compare this to the equivalent raw JSON in the Validation page — the helper version is more compact, fully type-checked, and the default messages are generated automatically.

On this page