Skip to content

Annotations Reference

This is the authoritative list of every annotation @atscript/vue-form reads. Keep it open while you write .as types — most form questions ("how do I hide this field on weekends?", "how do I add a prefix $ to a price input?") resolve to "find the right annotation key".

Annotations come from three families:

  • @meta.* and @expect.* — defined by atscript core. vue-form reads them; the language defines them. Full reference at atscript.dev.
  • @ui.form.* — static UI configuration, defined by @atscript/ui.
  • @ui.form.fn.* — dynamic JS-expression equivalents of the static keys. Require @atscript/ui-fns to evaluate.
  • @ui.type — cross-cutting render-type override.
  • @db.* — defined by @atscript/db. vue-form reads a few of them for measurement adornments and FK pickers. See db.atscript.dev.

Annotation key constants live in packages/ui/src/plugin/*.ts — every table below pairs the .as syntax with the underlying string key so you can grep the source.

@meta.* — semantics

Defined by the atscript core (@atscript/typescript). These describe what the field means, independent of how it's rendered.

AnnotationArgEffect in vue-form
@meta.label 'X'stringField title / structured-section title; on the type itself it titles the form.
@meta.description 'X'stringHelp text rendered below the field label.
@meta.default 'X'stringDefault value applied by createFormData at form mount.
@meta.required 'msg'stringMarks the field required + supplies the validation error message.
@meta.readonlyflagRenders read-only; value can still be set programmatically.
@meta.idflagMarks the identity field (used by atscript-db and AsRef pickers).
@meta.sensitiveflagHint to consumers that the value is sensitive (passwords, tokens).

Example:

atscript
@meta.label 'Age'
@meta.required 'Age is required'
@meta.default '18'
age: number

See atscript.dev — Annotations for the full list of meta annotations the language ships with.

@expect.* — validation

Defined by atscript core. vue-form runs them through the standard atscript validator (getFormValidator(def)) — no extra wiring on the Vue side.

AnnotationEffect
@expect.min N, 'msg'numeric lower bound
@expect.max N, 'msg'numeric upper bound
@expect.minLength N, 'msg'string / array length lower bound
@expect.maxLength N, 'msg'string / array length upper bound
@expect.pattern '/re/', 'msg'regex match

On arrays, @expect.minLength and @expect.maxLength also drive the canAdd / canRemove toggles on the rendered AsArray — see Arrays.

atscript
@meta.label 'Username'
@meta.required 'Username is required'
@expect.minLength 3, 'Must be at least 3 characters'
@expect.maxLength 20, 'Must be at most 20 characters'
username: string

@ui.type — built-in renderer override

The cross-surface render-type override. Flips which built-in renderer a field uses on whichever surface lacks its own override (@ui.form.type for forms, @ui.table.type for tables). The value must be one of the built-in type ids that the form/cell type maps know how to dispatch — text, password, textarea, number, decimal, select, radio, checkbox, multiselect, paragraph, action, date, datetime, time, etc.

For primitives it folds into the field's structural type at FormDef-build time; for structured fields (object, array, union, tuple) it lives in customType and is checked first in the lookup (see packages/vue-form/src/components/as-field.vue:373-386).

atscript
@ui.type 'password'
password: string

@ui.type 'select'
@ui.form.options 'admin' | 'user' | 'guest'
role: string

@ui.type 'textarea'
bio?: string

For custom renderers, use @ui.form.component (and @ui.table.component on the table side) instead. Those routes look up in the dedicated :components map — see Custom Components and Field Types.

@ui.form.* — static UI config

The bulk of form customization lives here. Every key is read once at field-setup time; values are inlined into the rendered component's props. For values that depend on other field data, use the matching @ui.form.fn.* key (next section).

Display

AnnotationTypeControls
@ui.form.placeholder 'X'stringinput placeholder
@ui.form.hint 'X'stringinline hint below the input (overrides meta.description when both present and the field is in error state)
@ui.form.label.singular 'tag'stringitem-noun for arrays — drives "Add tag" / "Remove tag" buttons
@ui.form.submit.text 'X'stringsubmit button text (on the type, not on a field)

Field descriptions come from @meta.description — there is no @ui.form.description static counterpart. For dynamic descriptions use @ui.form.fn.description (see Dynamic Fields).

Behaviour

AnnotationControls
@ui.form.hiddenhide field — phantom-renders, never visible
@ui.form.disableddisable input
@ui.form.autocomplete 'email'sets HTML autocomplete= attribute

For read-only, use @meta.readonly — it is the single source of truth that both forms and other surfaces read. For the dynamic counterpart use @ui.form.fn.readonly.

Layout

AnnotationControls
@ui.form.order Nsort order within siblings
@ui.form.grid.colSpan 'full' | 'half' | 'third' | '1'..'12'grid column width (12-col grid)
@ui.form.grid.rowSpan '1'..'4'grid row span
@ui.form.classes 'a b'extra classes on the field wrapper
@ui.form.styles { ... }inline style object
@ui.form.attr { ... }extra HTML attributes forwarded to the input element

See Grid Layout for the 12-column grid and the full/half/third aliases.

Adornments

Read together to drive the merged prefix / suffix chrome inside numeric and decimal inputs. See as-field.vue:199-307 for the full resolution chain.

AnnotationEffect
@ui.form.prefix '$'literal prefix string
@ui.form.prefix.ref 'currency'resolve prefix from a sibling field's current value
@ui.form.prefix.icon 'icon-credit-card'render an icon glyph as the prefix
@ui.form.suffix '%'literal suffix string
@ui.form.suffix.ref 'unit'resolve suffix from a sibling field
@ui.form.suffix.icon 'icon-percent'suffix icon glyph

Currency and unit values are also pulled from @db.amount.currency* and @db.unit* — see DB annotations below.

Actions

AnnotationEffect
@ui.type 'action'renders the field as an action button (uses AsAction)
@ui.form.action { id, label }declares the action's id + label; AsForm emits action with the form data when fired
@ui.form.submit.text 'Save'on the type itself — submit button text

See Actions for the action lifecycle and how AsWfForm extends this with workflow round-trips.

Choice fields

AnnotationEffect
@ui.form.options 'a' | 'b' | 'c'static option list for select / radio
@ui.form.options [{ value, label }]richer static option list (objects)
@ui.form.fn.options '(v, data) => [...]'dynamic options (requires @atscript/ui-fns)

@ui.form.options accepts either a string-literal-union shorthand or an explicit array. Inline string unions ('admin' | 'user' directly on the field type) are extracted automatically — no @ui.form.options needed:

atscript
role: 'admin' | 'user' | 'guest'

The same option sources also feed the multiselect renderer on array fields — see Field Types — Multi-select arrays for the dispatch rules.

Custom validation

AnnotationEffect
@ui.form.validate '(v) => !!v || "required"'custom validator expression (requires @atscript/ui-fns). Returns true on pass or a string error.

The validator receives (value, data, context). See Validation.

Component swap

AnnotationEffect
@ui.form.type 'textarea'Flips to a different built-in renderer (text, password, textarea, number, decimal, select, radio, checkbox, multiselect, date, datetime, time, paragraph, action). Reserved for built-ins.
@ui.form.component 'MyInput'Resolves a named component from the :components map (highest precedence). The dedicated mechanism for custom renderers.

Resolution precedence (see as-field.vue:373-386):

text
@ui.form.component  →  components[name]
@ui.form.type       →  types[customType]
field.type          →  types[type]

@ui.form.type is preferred for structured types where you want the type-map (so the swap also applies to nested same-typed fields); @ui.form.component is preferred when you have a one-off swap or multiple shapes of the same structural type that need different renderers. See Custom Components.

@ui.form.fn.* — dynamic UI config

Identical key namespace to @ui.form.* but the value is a JS function expression. The expression is parsed and evaluated through @atscript/ui-fns against a per-field scope:

ts
type TFnScope = {
  v: unknown; // current field value
  data: Record<string, unknown>; // entire form domain data
  context: Record<string, unknown>; // form-level context
  entry: { type; component; name; optional; disabled; hidden; readonly };
};

Field-level (declared on a prop):

AnnotationComputes
@ui.form.fn.labellabel string
@ui.form.fn.descriptiondescription string
@ui.form.fn.hinthint string
@ui.form.fn.placeholderplaceholder string
@ui.form.fn.hiddenboolean
@ui.form.fn.disabledboolean
@ui.form.fn.readonlyboolean
@ui.form.fn.classesclass object / string
@ui.form.fn.stylesstyle object
@ui.form.fn.attrHTML attribute map
@ui.form.fn.optionsdynamic option list
@ui.form.fn.valuederived value (readonly fields are auto-synced; phantom paragraph/action display)

Top-level (declared on the root interface / type, receives (data, context)):

AnnotationComputes
@ui.form.fn.titledynamic form title
@ui.form.fn.submit.textdynamic submit button text
@ui.form.fn.submit.disableddynamic submit-disabled boolean

Example — hide a "Room" field when no Floor has been picked:

atscript
@meta.label 'Room'
@ui.form.fn.hidden '(v, data) => !data.contact?.department?.floor'
room?: string

Full coverage in Dynamic Fields.

Trade-offs

Every @ui.form.fn.* key on a field promotes it from the static fast-path to the dynamic path (as-field.vue:409 vs :446). The dynamic path builds a reactive TFnScope per evaluation, so reach for fn.* when the value depends on other form state and stick with the static keys when it doesn't.

DB annotations (cross-cutting)

vue-form reads these even though they're defined by @atscript/db. Use them on .as types that are shared between the form and the database layer — you write the metadata once and both surfaces pick it up. Full coverage at db.atscript.dev.

Currency & units

AnnotationEffect on form
@db.amount.currency 'USD'hard-code the currency — drives AsDecimal's prefix glyph + Intl.NumberFormat decimals
@db.amount.currency.ref 'cur'resolve currency from a sibling field at runtime
@db.unit 'kg'hard-code a measurement unit — drives suffix
@db.unit.ref 'unit'resolve unit from a sibling
@db.column.precision { precision, scale }numeric precision/scale; scale caps the displayed decimals

Foreign keys (AsRef value-help)

AnnotationEffect
@db.rel.FK 'OtherTable'declares a foreign-key relation
@db.http.path '/api/customers'URL the AsRef picker queries for options

When both are present the field renders an AsRef picker by default (searchable, paged, server-driven). See References.

Cross-cutting reads

A few patterns you'll see again and again:

  • Optional + requiredfirstName?: string with @meta.required means "if you provide it, you must provide a value". In practice vue-form treats this as not-required for the field marker (the * would mislead) — see as-field.vue:167-181.
  • Phantom fields — fields with no input element (paragraphs, actions) read @meta.default (or @ui.form.fn.value dynamically) as the displayed value.
  • Order resolution@ui.form.order is numeric; ties fall through in declaration order. Mixing some-ordered + some-unordered fields works.

Next steps

Released under the MIT License.