@foormjs/atscript
v0.2.5
Published
ATScript-first form model with validation
Downloads
48
Readme
@foormjs/atscript
ATScript-first form model with validation. Define forms declaratively in .as files -- fields, labels, validators, computed properties, and options -- then create type-safe form definitions and validators at runtime.
Install
pnpm add @foormjs/atscript @atscript/typescript
# or
npm install @foormjs/atscript @atscript/typescript@atscript/typescript is a peer dependency required for type inference and validation utilities.
Quick Start
1. Configure ATScript
// atscript.config.ts
import { defineConfig } from '@atscript/core'
import ts from '@atscript/typescript'
import { foormPlugin } from '@foormjs/atscript/plugin'
export default defineConfig({
rootDir: 'src',
plugins: [ts(), foormPlugin()],
})2. Define a form schema
Create a .as file with ATScript annotations:
@foorm.title 'Registration'
@foorm.submit.text 'Register'
export interface RegistrationForm {
@meta.label 'First Name'
@meta.placeholder 'John'
@foorm.autocomplete 'given-name'
@meta.required 'First name is required'
@foorm.order 1
firstName: string
@meta.label 'Email'
@foorm.autocomplete 'email'
@foorm.order 2
email?: string.email
@meta.label 'Country'
@meta.placeholder 'Select a country'
@foorm.options 'United States', 'us'
@foorm.options 'Canada', 'ca'
@foorm.order 3
country?: foorm.select
@meta.label 'I agree to terms'
@foorm.order 4
agreeToTerms: foorm.checkbox
}3. Create a form definition and validate
import { createFoormDef, createFormData, getFormValidator } from '@foormjs/atscript'
import { RegistrationForm } from './registration-form.as'
// Create the form definition
const def = createFoormDef(RegistrationForm)
// def.rootField is the root field representing the entire form
// For object types, rootField.type === 'object'
// Create data container with defaults
const formData = createFormData(RegistrationForm, def.fields)
// formData = { value: { firstName: '', email: '', country: '' } }
// Domain data is at formData.value — OoForm handles this wrapping automatically
// Optionally skip default values for optional fields
const sparseData = createFormData(RegistrationForm, def.fields, { skipOptional: true })
// Validate (pass the unwrapped domain data)
const validate = getFormValidator(def)
const errors = validate({ data: formData.value })
// errors: Record<string, string> — empty object means passedArrays and Nested Structures
Array Fields
Declare array fields with TypeScript array syntax. foorm automatically handles add/remove buttons, item rendering, and array-level validation:
@meta.label 'Tags'
@foorm.array.add.label 'Add tag'
@foorm.array.remove.label 'x'
@expect.maxLength 5, 'Maximum 5 tags'
tags: string[]
@meta.label 'Addresses'
@foorm.title 'Addresses'
@foorm.array.add.label 'Add address'
@foorm.array.remove.label 'Remove address'
addresses: {
@meta.label 'Street'
@meta.required 'Street is required'
street: string
@meta.label 'City'
@meta.required 'City is required'
city: string
@meta.label 'ZIP'
zip?: string
}[]Supported item types: primitives (string[], number[], boolean[]), objects ({ ... }[]), and unions ((ObjectType | string)[]).
Array Annotations
| Annotation | Description |
| ------------------------------- | ----------------------------------------------- |
| @foorm.array.add.label 'text' | Label for the add button (default: "Add item") |
| @foorm.array.remove.label | Label for the remove button (default: "Remove") |
| @foorm.array.sortable | Enable drag-to-reorder for array items |
| @expect.minLength N, 'msg' | Minimum number of items |
| @expect.maxLength N, 'msg' | Maximum number of items |
Nested Objects
Use @foorm.title on an object field to render it as a titled section:
@foorm.title 'Settings'
settings: {
@meta.label 'Notify by email'
emailNotify: foorm.checkbox
@meta.label 'Max items per page'
@foorm.type 'number'
pageSize?: number
}Without @foorm.title, nested object fields are flattened into the parent form. With @foorm.title, they render as a distinct visual section with a title header.
Union Arrays
Arrays can contain multiple types. Each union member becomes a selectable variant:
@meta.label 'Contacts'
@foorm.array.add.label 'Add contact'
contacts: ({
@meta.label 'Full Name'
@meta.required 'Name is required'
fullName: string
@meta.label 'Email'
email?: string.email
} | string)[]The add button offers one option per variant. Items include a variant selector dropdown.
Advanced Usage
Computed Properties
Any field property can be made dynamic with @foorm.fn.* annotations. The function receives (value, data, context, entry) where entry is a TFoormFieldEvaluated snapshot of the field's current state:
@meta.label 'Email'
@foorm.fn.label '(v, data) => data.firstName ? data.firstName + "s Email" : "Email"'
@foorm.fn.disabled '(v, data) => !data.firstName'
@foorm.fn.placeholder '(v, data) => data.firstName ? data.firstName + "@example.com" : "[email protected]"'
email?: string.emailForm-level computed properties receive (data, context):
@foorm.fn.title '(data) => "Welcome, " + (data.firstName || "stranger")'
@foorm.fn.submit.disabled '(data) => !data.firstName || !data.email'
export interface MyForm {
...
}Validation
Use @meta.required to require non-blank strings (rejects empty and whitespace-only values). For booleans, it enforces true:
@meta.required 'Name is required'
name: string
// Or use the string.required primitive (implicitly adds @meta.required)
email: string.required
// For boolean fields, @meta.required means "must be true"
agreeToTerms: boolean.requiredUse @foorm.validate for custom validation logic. The function returns true for pass or an error message string:
@foorm.validate '(v) => v.length >= 3 || "Must be at least 3 characters"'
firstName: stringValidators can also access the full form data and context:
@foorm.validate '(v, data) => v !== data.firstName || "Cannot match first name"'
lastName: stringATScript's built-in @expect.* validators also work alongside foorm validators:
@expect.min 18, 'Must be 18 or older'
@expect.maxLength 100
age: numberForm Context
Pass external data to computed functions and validators via context:
const validate = getFormValidator(def)
const errors = validate({
data,
context: { maxAge: 120, allowedDomains: ['example.com'] },
})Context is available as the third argument in function strings:
@foorm.fn.options '(v, data, ctx) => ctx.cityOptions || []'
city?: foorm.selectCustom Attributes
Pass arbitrary attributes to rendered components:
// Static attributes
@foorm.attr 'data-testid', 'username-input'
@foorm.attr 'aria-label', 'Username field'
// Computed attributes
@foorm.fn.attr 'data-valid', '(v) => v && v.length >= 3 ? "true" : "false"'
username?: stringFoorm Primitives
Foorm provides special primitive types for non-data UI elements:
| Primitive | Description |
| ----------------- | ----------------------------------------------------------- |
| foorm.action | Button that triggers an alternate action (not a data field) |
| foorm.paragraph | Read-only text content |
| foorm.select | Dropdown select (string value) |
| foorm.radio | Radio button group (string value) |
| foorm.checkbox | Boolean checkbox toggle |
@meta.label 'Reset Password'
@foorm.altAction 'reset-password', 'Reset Password'
resetBtn: foorm.action
@foorm.value 'Please fill out the form below.'
info: foorm.paragraphPlugin Configuration
Register extra field types and custom component names for IDE autocomplete:
foormPlugin({
extraTypes: ['color', 'rating', 'date-range'],
components: ['CustomStarInput', 'ColorPicker'],
})Resolving Field Properties at Runtime
Use resolve utilities to read metadata on demand from a field's ATScript prop:
import { resolveFieldProp, resolveOptions, resolveAttrs } from '@foormjs/atscript'
const scope = { v: currentValue, data: formData, context, entry }
// Resolve a single property (checks fn first, then static)
const label = resolveFieldProp<string>(field.prop, 'foorm.fn.label', 'meta.label', scope)
// Resolve select/radio options
const options = resolveOptions(field.prop, scope)
// Resolve custom attributes
const attrs = resolveAttrs(field.prop, scope)API Reference
Core
| Export | Description |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| createFoormDef(type) | Converts an ATScript annotated type into a FoormDef with ordered fields |
| createFormData(type, fields, opts?) | Creates a { value: T } data container with defaults from the schema |
| createDefaultValue(type) | Returns a default value for any ATScript annotated type |
| createItemData(variant) | Creates a default data value for a union variant |
| detectUnionVariant(value, variants) | Detects which union variant an existing value matches |
| getFormValidator(def, opts?) | Returns a reusable ({ data, context? }) => Record<string, string> validator |
| createFieldValidator(prop, opts?) | Creates a cached validator for a single field prop |
| supportsAltAction(def, action) | Checks if any field supports a given alternate action |
| isArrayField(field) | Type guard: returns true if the field is an array field |
| isObjectField(field) | Type guard: returns true if the field is an object field |
| isUnionField(field) | Type guard: returns true if the field is a union field |
| isTupleField(field) | Type guard: returns true if the field is a tuple field |
Resolve Utilities
| Export | Description |
| -------------------------------------------------------- | ------------------------------------------------------------ |
| resolveFieldProp(prop, fnKey, staticKey, scope, opts?) | Resolves a field-level metadata value (fn or static) |
| resolveFormProp(type, fnKey, staticKey, scope, opts?) | Resolves a form-level metadata value (fn or static) |
| resolveOptions(prop, scope) | Resolves @foorm.options / @foorm.fn.options |
| resolveAttrs(prop, scope) | Resolves @foorm.attr / @foorm.fn.attr |
| getFieldMeta(prop, key) | Reads a static metadata value from a field prop |
| buildFieldEntry(prop, baseScope, path, opts?) | Builds a TFoormFieldEvaluated entry and returns full scope |
| optKey(opt) | Extracts the key from a TFoormEntryOptions entry |
| optLabel(opt) | Extracts the display label from a TFoormEntryOptions entry |
General Utilities
| Export | Description |
| ----------------------------- | ----------------------------------------- |
| getByPath(obj, path) | Gets a nested value by dot-separated path |
| setByPath(obj, path, value) | Sets a nested value by dot-separated path |
Validator Plugin
| Export | Description |
| ------------------------ | ---------------------------------------------------------------------- |
| foormValidatorPlugin() | Creates an ATScript validator plugin for @foorm.validate annotations |
Types
| Export | Description |
| --------------------------- | -------------------------------------------------------------------------------------------- |
| FoormDef | Complete form definition (type, rootField, fields, flatMap) |
| FoormFieldDef | Single field definition (path, prop, type, phantom, name, allStatic) |
| FoormArrayFieldDef | Array field definition (extends FoormFieldDef with itemType, itemField) |
| FoormObjectFieldDef | Object field definition (extends FoormFieldDef with objectDef: FoormDef) |
| FoormUnionFieldDef | Union field definition (extends FoormFieldDef with unionVariants) |
| FoormTupleFieldDef | Tuple field definition (extends FoormFieldDef with itemFields) |
| FoormUnionVariant | Variant definition for union branches (label, type, def?, itemField?, designType?) |
| TFoormFnScope | Scope object passed to computed functions (v, data, context, entry) |
| TFoormFieldEvaluated | Evaluated field snapshot passed as entry in scope (see below) |
| TFoormEntryOptions | Option for select/radio fields (string or { key, label }) |
| TFoormAltAction | Alt-action metadata (id, label) |
| TComputed<T> | A value that is either static or a function of scope |
| TResolveOptions<T> | Options for resolve utilities (staticAsBoolean, transform) |
| TBuildFieldEntryOpts | Options for buildFieldEntry (pre-resolved overrides for field info and constraints) |
| TFormValidatorCallOptions | Per-call options for getFormValidator return fn (data, context?) |
| TFieldValidatorOptions | Options for createFieldValidator (rootOnly?) |
| TFoormValidatorContext | Per-call validator context (data, context) |
TFoormFieldEvaluated
Evaluated snapshot of a field's current state, passed as entry to @foorm.fn.* and @foorm.validate function strings:
interface TFoormFieldEvaluated {
field: string // Field path (e.g., 'address.city') — required, '' for root
type: string // Resolved input type ('text', 'select', etc.)
component?: string // Named component from @foorm.component
name: string // Field name (last segment of path)
disabled?: boolean // Whether the field is disabled
optional?: boolean // Whether the field is optional
hidden?: boolean // Whether the field is hidden
readonly?: boolean // Whether the field is read-only
options?: TFoormEntryOptions[] // Resolved options for select/radio fields
}Build-time Plugin
| Export | Description |
| --------------------- | --------------------------------------------------------- |
| foormPlugin(opts?) | ATScript plugin for @foorm.* annotations and primitives |
| TFoormPluginOptions | Plugin options ({ extraTypes?, components? }) |
For ATScript documentation, see atscript.moost.org.
AI Agent Skills
@foormjs/atscript ships an AI agent skill for Claude Code, Cursor, Windsurf, Codex, and other compatible agents. The skill teaches your agent the library's APIs, patterns, and best practices so it can help you write correct code without hallucinating.
Install the skill into your agent:
# Project-local (recommended — version-locked, commits with your repo)
npx @foormjs/atscript setup-skills
# Global (available across all your projects)
npx @foormjs/atscript setup-skills --globalRestart your agent after installing.
Auto-update on install — to keep the skill in sync whenever you upgrade the package, add this to your project's package.json:
{
"scripts": {
"postinstall": "npx @foormjs/atscript setup-skills --postinstall",
},
}License
MIT
