@atscript/vue-form
Vue 3 form library backed by @atscript/ui. Three tiers of components, ~30 composables, two factories, and one auto-resolver entry. Every Tier-1 component you tag (<AsForm>, <AsField>, <AsIterator>) wraps a public composable, so apps can drop the renderer and rebuild their own form root without losing functionality.
Contents
- Tier 1 — Primary components
- Tier 2 — Default field components
- Factories
- Composables — form / state
- Composables — field & structure
- Composables — value help / dropdown
- Composables — choreography
- Composables — date / number / decimal
- Composables — context / utility
- Component prop & emit types
- Other types
Tier 1 — Primary components
Imported automatically by AsResolver() (see @atscript/ui-styles).
AsForm
Top-level form renderer.
Props (subset — see useAsForm for full reactive contract):
interface AsFormProps {
def: FormDef;
formData?: { value: unknown };
formContext?: unknown;
types: TAsTypeComponents;
components?: Record<string, Component>;
errors?: Record<string, string | undefined>;
firstValidation?: "on-change" | "touched-on-blur" | "on-blur" | "on-submit";
hideRootTitle?: boolean;
loading?: boolean;
clientFactory?: ClientFactory;
}Emits:
submit(data)— validation passed, data ready to ship.error(errors)— validation failed; payload is{ path, message }[].action(name, data)— phantom<AsAction>button dispatched.unsupportedAction(name, data)— action name not declared on the form's type.change(type, path, value, data)— granular per-field change;typeisTAsChangeType.
Slots: form.header, form.before, form.after, form.submit, form.footer.
AsField
Renders a single field given a FormFieldDef. Use when laying out form fields manually instead of letting AsForm iterate.
interface AsFieldProps {
field: FormFieldDef;
/** External validation error to surface on this field. */
error?: string;
/** Wires the array-item remove affordance when rendered inside an array. */
onRemove?: () => void;
canRemove?: boolean;
removeLabel?: string;
/** Index of this field within a parent array — drives the `#N` suffix on labels. */
arrayIndex?: number;
}AsIterator
Iterates the fields of a FormDef and renders each through AsField. Used by AsObject for nested objects and exposed for advanced compositions.
interface AsIteratorProps {
def: FormDef;
/** Optional dotted segment to prepend to every child field's path. */
pathPrefix?: string;
onRemove?: () => void;
canRemove?: boolean;
removeLabel?: string;
}Tier 2 — Default field components
Default swap targets for the types map. Importable both via the package root and the kebab subpath used by AsResolver.
import { AsInput, AsSelect } from "@atscript/vue-form";
// or, for tree-shake-friendly single imports
import AsInput from "@atscript/vue-form/as-input";Every component implements TAsComponentProps. The columns below list the @ui.type value that maps to each component in createDefaultTypes().
| Component | Default type keys | Notes |
|---|---|---|
AsFieldShell | (all) | Chrome wrapper — title, description, error, hint, prefix/suffix. Used internally by every other default. |
AsInput | text, textarea, password | Plain string input. |
AsNumber | number | Integer input with prefix/suffix, currency-aware. |
AsDecimal | decimal | Decimal-string input with precisionScale enforcement. |
AsSelect | select | <select> element fed by options / value-help. |
AsRadio | radio | Radio group. |
AsCheckbox | checkbox | Boolean checkbox or tri-state. |
AsDate | date | Date-only input. |
AsDatetime | datetime | Date-time input. |
AsTime | time | Time-of-day input. |
AsParagraph | paragraph | Phantom read-only field; renders value as text. |
AsAction | action | Phantom button — emits action. |
AsObject | object | Iterates over a nested object's fields. |
AsArray | array | Renders array items with add/remove affordances. |
AsUnion | union | Variant picker + nested fields for the active branch. |
AsTuple | tuple | Positional fields, fixed length. |
AsRef | ref | FK input with value-help dropdown. |
All defaults accept the full TAsComponentProps contract — see Component prop & emit types.
Factories
createDefaultTypes()
Returns a pre-populated TAsTypeComponents map with every default in the table above.
function createDefaultTypes(): TAsTypeComponents;import { createDefaultTypes } from "@atscript/vue-form";
const types = createDefaultTypes();
types.text = MyCustomTextInput; // swap one entrycreateAsFormDef(type, context?)
Builds a FormDef from an .as annotated type and a fresh reactive formData container with @meta.default values applied. When context is provided and @atscript/ui-fns is installed, @ui.form.fn.value annotations evaluate against it during default-value resolution.
function createAsFormDef<T extends TAtscriptAnnotatedType>(
type: T,
context?: Record<string, unknown>,
): { def: FormDef; formData: { value: unknown } };formatIndexedLabelParts(label, arrayIndex)
Splits a label into base + optional #N suffix for two-part rendering by AsCollapsible / AsFieldShell. Returns undefined when both the base and suffix are absent.
function formatIndexedLabelParts(
label: string | undefined,
arrayIndex: number | undefined,
): { base: string; suffix?: string } | undefined;Composables — form / state
useAsForm(options)
Powers <AsForm>. Owns the data container, internal validator, external-error dismissal, action routing, change merging, descendant counts, auto-open, and all provide/inject wiring. Use this when building a custom form root.
function useAsForm<TFormData = unknown, TFormContext = unknown>(
options: UseAsFormOptions<TFormData, TFormContext>,
): UseAsFormReturn<TFormData, TFormContext>;UseAsFormOptions accepts getters for every reactive prop so defineProps() accessors thread in without an extra ref:
interface UseAsFormOptions<TFormData, TFormContext> {
def: () => FormDef;
formData?: () => TFormData | undefined;
formContext?: () => TFormContext | undefined;
firstValidation?: () => TFormState["firstValidation"] | undefined;
components?: () => Record<string, Component<TAsComponentProps>> | undefined;
types: () => TAsTypeComponents;
errors?: () => Record<string, string | undefined> | undefined;
clientFactory?: () => ClientFactory | undefined;
hideRootTitle?: () => boolean | undefined;
emits?: {
submit?: (data: TFormData) => void;
error?: (errors: { path: string; message: string }[]) => void;
action?: (name: string, data: TFormData) => void;
unsupportedAction?: (name: string, data: TFormData) => void;
change?: (type: TAsChangeType, path: string, value: unknown, formData: TFormData) => void;
};
}Returns:
interface UseAsFormReturn<TFormData, TFormContext> {
data: ComputedRef<TFormData>;
errors: ComputedRef<Record<string, string | undefined> | undefined>;
formError: ComputedRef<string | undefined>;
internalErrors: Ref<Record<string, string>>;
reset: () => Promise<void>;
clearErrors: () => void;
setErrors: (errors: Record<string, string>) => void;
onSubmit: () => void;
submitText: ComputedRef<string>;
submitDisabled: ComputedRef<boolean>;
invokeAction: (name: string) => void;
dismissError: (path: string) => void;
dismissFormError: () => void;
formContext: ComputedRef<TFormContext | undefined>;
handleChange: (type: TAsChangeType, path: string, value: unknown) => void;
}useAsState(options)
Lower-level state plumbing: validators, error maps, first-validation strategy, freshness tracking. useAsForm builds on top of this.
function useAsState<TFormData, TFormContext>(options: {
formData: ComputedRef<TFormData>;
formContext: ComputedRef<TFormContext | undefined>;
firstValidation: ComputedRef<TFormState["firstValidation"] | undefined>;
submitValidator: TFormSubmitValidator;
}): UseAsStateReturn;TFormSubmitValidator returns true for success or an { path, message }[] error list.
useAsExternalErrors(options)
Reactive merge + per-path dismissal for server-supplied errors. useAsForm mounts this internally; pull it directly when consuming <AsForm> from a wrapper that owns its own error state.
function useAsExternalErrors(options: UseAsExternalErrorsOptions): UseAsExternalErrorsReturn;
interface UseAsExternalErrorsOptions {
source: () => Record<string, string | undefined> | undefined;
}
interface UseAsExternalErrorsReturn {
effective: ComputedRef<Record<string, string | undefined> | undefined>;
formError: ComputedRef<string | undefined>;
dismissAt: (path: string) => void;
dismissForm: () => void;
}Composables — field & structure
useAsField(opts)
Field-level state machine — model wrapper, validator pipeline, error resolution, blur tracking, and registration with the parent form. Call from a custom field that owns its own commit path instead of routing through AsField.
interface UseAsFieldOptions<TValue, TFormData, TContext> {
getValue: () => TValue;
setValue: (v: TValue) => void;
rules?: TFormRule<TValue, TFormData, TContext>[];
path: () => string;
/** Value to set on form reset. Defaults to `''`. Use `[]` for arrays, `{}` for objects. */
resetValue?: TValue;
}
interface UseAsFieldReturn<TValue> {
model: WritableComputedRef<TValue>;
error: ComputedRef<string | undefined>;
onBlur: () => void;
}
function useAsField<TValue, TFormData, TContext>(
opts: UseAsFieldOptions<TValue, TFormData, TContext>,
): UseAsFieldReturn<TValue>;useAsArray(field, disabled?)
Powers AsArray. Manages stable item keys, add/remove respecting @expect.minLength / @expect.maxLength, and union-item variant resolution.
function useAsArray(field: FormArrayFieldDef, disabled?: ComputedRef<boolean>): UseAsArrayReturn;
interface UseAsArrayReturn {
arrayValue: ComputedRef<unknown[]>;
itemKeys: string[];
isUnion: boolean;
unionVariants: FormUnionVariant[];
isOptional: boolean;
isEmpty: ComputedRef<boolean>;
getItemField: (index: number, name?: string) => FormFieldDef;
addItem: (variantIndex?: number) => void;
removeItem: (index: number) => void;
clear: () => void;
canAdd: ComputedRef<boolean>;
canRemove: ComputedRef<boolean>;
}useAsTuple(field)
Powers AsTuple. Fixed-length positional fields; auto-fills missing positions on mount unless the tuple is optional.
function useAsTuple(field: FormTupleFieldDef): UseAsTupleReturn;
interface UseAsTupleReturn {
itemFields: FormFieldDef[];
positionLabeled: boolean[];
isOptional: boolean;
isEmpty: ComputedRef<boolean>;
clear: () => void;
fillMissing: () => void;
}useAsUnion(props)
Powers AsUnion. Owns the locally-selected variant index and stashes per-variant data so toggling back restores prior input. Pass the component's resolved TAsComponentProps (AsUnion reads props.field + props.model).
function useAsUnion(props: TAsComponentProps): UseAsUnionReturn;
interface UseAsUnionReturn {
unionField: ComputedRef<FormUnionFieldDef | undefined>;
hasMultipleVariants: ComputedRef<boolean>;
localUnionIndex: Ref<number>;
innerField: ComputedRef<FormFieldDef | undefined>;
changeVariant: (newIndex: number) => void;
optionalEnabled: ComputedRef<boolean>;
}useAsUnionVariant()
Reads the current union context (TAsUnionContext) injected by the closest AsUnion. Returns undefined outside a union.
function useAsUnionVariant(): TAsUnionContext | undefined;Composables — value help / dropdown
useAsValueHelp(options)
Lazily resolves a ValueHelpInfo descriptor on mount, then exposes a debounced search API for FK pickers. Reads the active ClientFactory from the nearest <AsForm :client-factory> (or the global default) automatically.
interface UseAsValueHelpOptions {
/** Resolved value-help descriptor (read from `props.valueHelp`). */
info: ValueHelpInfo;
/** The model whose `.value` the picker writes to on select. */
model: { value: unknown };
/** Called after a selection commits so AsField can run blur-time validation. */
onBlur: () => void;
}
interface UseAsValueHelpReturn {
resolved: ShallowRef<ResolvedValueHelp | null>;
status: Ref<"loading" | "ready" | "error">;
searchText: Ref<string>;
results: ShallowRef<Record<string, unknown>[]>;
searching: Ref<boolean>;
labelIsFkValue: ComputedRef<boolean>;
kickoff: () => Promise<void>;
selectItem: (item: Record<string, unknown>) => void;
clear: () => void;
}
function useAsValueHelp(options: UseAsValueHelpOptions): UseAsValueHelpReturn;useAsDropdown(containerRef)
Click-outside-aware dropdown state. The listener is lazy — attached only while open.
function useAsDropdown(containerRef: Ref<HTMLElement | null>): {
isOpen: Ref<boolean>;
toggle: () => void;
close: () => void;
/** Run the callback and close immediately — convenience for option-click handlers. */
select: (callback: () => void) => void;
};Composables — choreography
useAsOptionalAddFlow(options)
Implements the "Add" affordance for an optional field. When the user clicks "Add", the value is set to its default; when they clear it, the field is set to undefined.
function useAsOptionalAddFlow(options: UseAsOptionalAddFlowOptions): UseAsOptionalAddFlowReturn;useAsTriStateCheckbox(options)
Three-state checkbox (true / false / undefined) with keyboard support.
function useAsTriStateCheckbox(options: UseAsTriStateCheckboxOptions): UseAsTriStateCheckboxReturn;useAsFocusFirstAfter() / focusFirstAfter(...) / focusNewFocusableAfter(...)
Focus utilities for "after-toggle" flows — moves DOM focus to the first focusable element added to a region after a toggle.
function useAsFocusFirstAfter(): { focusAfter: () => void };
function focusFirstAfter(host: HTMLElement | undefined): void;
function focusNewFocusableAfter(prev: HTMLElement[], host: HTMLElement | undefined): void;Composables — date / number / decimal
useAsDate(options)
Handles ISO ↔ display conversion for <AsDate> / <AsDatetime> / <AsTime>.
function useAsDate(options: UseAsDateOptions): UseAsDateReturn;useAsNumber(options)
Integer input model — handles select-all on focus, prefix/suffix, optional toggle.
function useAsNumber(options: UseAsNumberOptions): UseAsNumberReturn;useAsDecimal(options)
Decimal-string input model — enforces precisionScale, currency formatting, locale separators. Stores as string, never bounces through float.
function useAsDecimal(options: UseAsDecimalOptions): UseAsDecimalReturn;useAsDualInput(options)
Powers the merged-shell AsDecimal / AsNumber layout — two visual inputs (integer and fraction) sharing a single model.
function useAsDualInput(options: UseAsDualInputOptions): UseAsDualInputReturn;Composables — context / utility
useAsLocale() / provideAsLocale(getter)
Provide / inject the BCP-47 locale used by date and decimal composables. The getter shape lets reactive sources flow through without an extra computed() at the call site.
function provideAsLocale(getter: () => string | undefined): void;
interface UseAsLocaleReturn {
/** Resolved locale; `undefined` when no provider is mounted. */
locale: ComputedRef<string | undefined>;
}
function useAsLocale(): UseAsLocaleReturn;useAsPath() / useAsTypeMap() / useAsData()
Read-only context wrappers. Useful in deeply nested custom components that need the current field path, the form's :types map, or reactive read access to form data.
function useAsPath(): UseAsPathReturn; // { path: ComputedRef<string> }
function useAsTypeMap(): UseAsTypeMapReturn; // { types: ComputedRef<TAsTypeComponents> }
interface UseAsDataReturn {
/** Domain data — the unwrapped inner value of the form's `{ value }` container. */
rootData: ComputedRef<unknown>;
/** Read the value at an absolute dotted path inside the form. */
getValueAt: (path: string) => ComputedRef<unknown>;
/** Read a sibling field relative to the current `useAsPath()` prefix. */
siblingValue: <T = unknown>(name: string) => ComputedRef<T | undefined>;
}
function useAsData(): UseAsDataReturn;useAsErrorDismiss()
Returns the dismissal callback bound by the closest AsForm. Lets a custom error-banner component dismiss its own path without bubbling through props.
function useAsErrorDismiss(): AsErrorDismiss; // (path: string) => voiduseAsNestedSectionsStore() / provideAsNestedSectionsStore()
Shared collapsible-section store used by AsObject. Provide your own when wiring "Expand all" / "Collapse all" controls outside AsForm.
interface AsNestedSectionsStore {
isOpen(path: string): boolean;
setOpen(path: string, open: boolean): void;
toggle(path: string): void;
expandAll(paths: Iterable<string>): void;
collapseAll(paths: Iterable<string>): void;
}
function provideAsNestedSectionsStore(): AsNestedSectionsStore;
function useAsNestedSectionsStore(): AsNestedSectionsStore | undefined;Component prop & emit types
TAsComponentProps<V>
The full prop contract for custom field components. Implement this so AsField can pass every resolved field state.
Key fields (excerpt — full type lives in components/types.ts):
interface TAsBaseComponentProps {
disabled?: boolean;
hidden?: boolean;
}
interface TAsComponentProps<V = unknown> extends TAsBaseComponentProps {
onBlur: () => void;
error?: string;
model: { value: V };
/** Phantom display value (paragraph/action). */
value?: unknown;
label?: string;
description?: string;
hint?: string;
placeholder?: string;
prefixIcon?: string;
suffixIcon?: string;
prefix?: string;
suffix?: string;
class?: Record<string, boolean> | string;
style?: Record<string, string> | string;
optional?: boolean;
onToggleOptional?: (enabled: boolean) => void;
required?: boolean;
readonly?: boolean;
type: string;
formAction?: TFormAction;
name?: string;
field?: FormFieldDef;
options?: TFormEntryOptions[];
maxLength?: number;
autocomplete?: string;
title?: string;
level?: number;
onRemove?: () => void;
canRemove?: boolean;
removeLabel?: string;
arrayIndex?: number;
path: string;
valueHelp?: ValueHelpInfo;
singularLabel?: string;
inputId: string;
errorId: string;
descId: string;
ariaDescribedBy?: string;
currencyCode?: string;
unitCode?: string;
precisionScale?: number;
scale?: number;
hasAdornment?: boolean;
}TAsComponentEmits<V>
interface TAsComponentEmits<_V = unknown> {
(e: "action", name: string): void;
}TAsChangeType
type TAsChangeType = "update" | "array-add" | "array-remove" | "union-switch";TAsTypeComponents
Required keys for built-in types plus an open index signature.
type TAsTypeComponents = {
text: Component;
select: Component;
radio: Component;
checkbox: Component;
paragraph: Component;
action: Component;
object: Component;
array: Component;
union: Component;
tuple: Component;
ref: Component;
decimal: Component;
number: Component;
date: Component;
datetime: Component;
time: Component;
} & Record<string, Component>;TAsUnionContext
interface TAsUnionContext {
variants: FormUnionVariant[];
currentIndex: Ref<number>;
changeVariant: (index: number) => void;
}Other types
TFormState / TFormRule / TFormFieldCallbacks / TFormFieldRegistration
Internal-ish but exported for advanced wrappers building their own field renderer. TFormRule is the per-field validator signature:
type TFormRule<TValue, TFormData, TContext> = (
value: TValue,
data: TFormData,
context: TContext,
) => true | string;Re-exports from @atscript/ui
setDefaultClientFactory, getDefaultClientFactory, resetDefaultClientFactory, ClientFactory — see @atscript/ui — Client factory.
Cross-links
- Forms — Hello World
- Forms — Annotations Reference
- Forms — Field Types & Type Map
- Forms — Validation
- Forms — Arrays, Nested Objects, Unions, Tuples
- Forms — Dynamic Fields
- Forms — Grid Layout
- Forms — Actions
- Forms — References (FK)
- Forms — Three Levels of Override, Custom Components, Locale & Currency
- @atscript/ui, @atscript/ui-fns