Skip to content

Field Types & the Type Map

Every field rendered by <AsForm> is resolved against a type map — a record from string keys (the field's type / customType) to Vue components. The map is supplied as the :types prop. createDefaultTypes() ships a complete set of unstyled defaults; you override entries to plug in your own design system.

The default type map

createDefaultTypes() is a six-line factory (packages/vue-form/src/composables/create-default-types.ts) that returns a fresh map of every built-in type to a Tier-2 default component:

KeyDefault componentRenders
textAsInput<input type="text">
textareaAsInput<textarea> variant
passwordAsInput<input type="password">
numberAsNumbernumeric input with optional currency/unit chrome
decimalAsDecimalfixed-point decimal with split integer/fraction halves
selectAsSelect<select> for static option lists
radioAsRadioradio-button group
checkboxAsCheckboxsingle boolean checkbox / tri-state for optional booleans
multiselectAsMultiSelectcombobox popup with chip-style selections, bound to T[]
dateAsDatedate picker (YYYY-MM-DD)
datetimeAsDatetimedate + time picker
timeAsTimetime picker (HH:MM / HH:MM:SS)
paragraphAsParagraphread-only paragraph — value from @meta.default / fn.value
actionAsActionbutton that emits action on the form
objectAsObjectnested object renderer (collapsible at depth)
arrayAsArraydynamic +/− array of items
unionAsUnionvariant picker + dispatch to selected variant
tupleAsTuplefixed-position positional list
refAsRefsearchable FK picker (driven by @db.rel.FK + @db.http.path)

All Tier-2 defaults are exported from @atscript/vue-form and can be imported by name when you want to reuse one inside a custom renderer.

Atscript primitive → field type

createFormDef maps atscript primitive types onto these keys without any annotation:

.as type→ field typeNotes
stringtextalso string.email, string.url, string.uuid, …
numbernumberuse @db.column.precision for decimal placement
booleancheckboxoptional booleans render tri-state
Datedateflip to datetime / time with @ui.form.type
T[]arrayitem type recurses
('a' | 'b')[]multiselectarray of a literal union — options extracted from the item type
T[] + @ui.form.optionsmultiselectprimitive-item array with an option list — T is string or number
[A, B]tuplefixed-length positional
A | B | Cuniondiscriminated union by required-prop fingerprint
{ … }objectnested structure — renders via AsObject
RefToTablerefonly when @db.rel.FK + @db.http.path are present

string.email carries the email format constraint through to the atscript validator; the rendered control is still a text input. Use @ui.form.component if you want to wire a dedicated email widget — see Custom Components.

Flipping to a different built-in renderer

@ui.form.type (and the cross-cutting @ui.type) overrides the default structural lookup, but the value must be one of the built-in ids above. Use it to switch a primitive between built-in renderers:

atscript
@ui.form.type 'password'
password: string             // would default to 'text'; rendered as password

@ui.form.type 'select'
@ui.form.options 'admin' | 'user' | 'guest'
role: string                 // string union extracted to options

@ui.form.type 'textarea'
bio?: string                 // multi-line input

@ui.form.type 'datetime'
when: Date                   // upgrade Date → datetime picker

Multi-select arrays

multiselect renders a combobox popup with chip-style selected values inside the trigger — pick zero or more options from a fixed list. The structural type stays array (so @expect.minLength / @expect.maxLength still validate at the array level); only customType flips to multiselect. The two auto-dispatch cases are the last two T[] rows in the primitive-type table above. Other T[] shapes fall back to the dynamic +/- array renderer unless you tag them with @ui.form.type 'multiselect'.

atscript
colors: ('red' | 'green' | 'blue')[]       // auto → multiselect

@ui.form.options 'a' | 'b' | 'c'
tags: string[]                              // auto → multiselect (via options)

@ui.form.type 'multiselect'
@ui.form.options [1, 2, 3, 4]
sizes: number[]                             // forced

Value model is the array itself (T[]) — no wrapping, no positional sentinel. Swap the default AsMultiSelect through the :types map under the multiselect key.

For custom renderers — anything outside the built-in id list — use @ui.form.component instead, with the name registered in the :components prop map. The :types map is reserved for built-ins.

Component resolution

When <AsField> mounts it picks a component in this order (packages/vue-form/src/components/as-field.vue:373-386):

text
1. @ui.form.component  →  components[name]
2. @ui.form.type / @ui.type  →  types[customType]   (built-in id)
3. field.type  →  types[type]
  • @ui.form.component resolves a named component from the :components prop map. Highest precedence — the dedicated mechanism for custom widgets.
  • @ui.form.type (and @ui.type on structured fields) resolves a built-in type entry in the :types map. Use it to swap built-in renderers, not to wire custom components.

If no match is found, <AsField> renders a small in-place diagnostic message rather than erroring out — useful for catching typos without crashing the whole form.

Overriding by type — replace a default

The most common customization: keep the entire default map but swap one entry for your design system's component.

vue
<script setup lang="ts">
import { AsForm, createDefaultTypes, createAsFormDef } from "@atscript/vue-form";
import MyInput from "./MyInput.vue";
import { BasicForm } from "../forms/BasicForm.as";

const types = { ...createDefaultTypes(), text: MyInput, password: MyInput };
const { def, formData } = createAsFormDef(BasicForm);
</script>

<template>
  <AsForm :def="def" :form-data="formData" :types="types" @submit="onSubmit" />
</template>

MyInput is a normal Vue component that accepts the TAsComponentProps interface (model, label, error, ...). Wrapping it in <AsFieldShell> is the fastest way to inherit the label / description / error chrome — see Custom Components.

Wiring a custom component

When you have a one-off custom field (an RGB color picker, a chip selector, a map widget), give it a name and register it in the :components prop map. Tag the field with @ui.form.component:

ts
import RgbPicker from "./RgbPicker.vue";

const components = {
  "rgb-picker": RgbPicker,
};
atscript
@meta.label 'Brand color'
@ui.form.component 'rgb-picker'
logoRgb: [number, number, number]

The structural type stays tuple, so removing the @ui.form.component annotation falls back to AsTuple automatically.

@ui.form.component is the dedicated mechanism for custom renderers and the highest-precedence resolution path. It ignores the type map entirely — the same custom name will not be looked up in :types, which keeps the two maps cleanly separated.

Convention

  • :types map — built-in renderer ids only (text, textarea, date, …). Touched by @ui.form.type / @ui.type.
  • :components map — your custom renderers. Touched by @ui.form.component.

Don't mix the two: don't put custom components in :types, and don't duplicate built-in renderers in :components. The runtime allows it, but the convention keeps schemas predictable.

Built-in defaults — at a glance

ComponentUse it when
AsInputtext / password / textarea — the workhorse
AsNumberinteger-y number fields, optionally with currency/unit chrome
AsDecimalfixed-point inputs with controlled scale (money, percentages)
AsSelectsmall static option lists
AsRadiosmall option lists where seeing all options matters
AsCheckboxboolean — tri-state for optional booleans
AsMultiSelectarray of literal-union or option-list values — combobox popup with chips
AsDatedate-only
AsDatetimedate + time
AsTimetime-only
AsParagraphread-only displayed text (no input)
AsActionform-embedded button (fires action event)
AsObjectnested record fields
AsArraydynamic-length lists
AsUniondiscriminated unions / "pick one of N shapes"
AsTuplefixed-position positional arrays
AsRefFK pickers driven by @db.rel.FK + @db.http.path
AsFieldShellwrapper providing label/description/error chrome inside custom renderers

Next steps

  • Validation — how the atscript validator wires into every type.
  • Customization — bigger-picture overrides, per-form clientFactory, theme integration.
  • Custom Components — building a Tier-2 swap target from scratch.

Released under the MIT License.