@atscript/ui
Framework-agnostic core for type-driven UIs. Reads compiled Atscript metadata, builds form and table definitions, exposes a pluggable field resolver, validators, value-help, and a battery of pure utilities consumed by every Vue package. No Vue, no React — plain TypeScript.
Contents
- Plugin
- Form types
- Form factories
- Table types
- Table factory
- Field resolver
- Annotation key constants
- Validators
- Path utilities
- Value-help
- Grid layout
- Decimal helpers
- Column helpers
- Error map utilities
- Type guards
- Misc utilities
- Client factory
- Meta cache
Plugin
@atscript/ui/plugin exposes the build-time plugin that registers every static @ui.* annotation key. Wire it in atscript.config.ts:
import uiPlugin from "@atscript/ui/plugin";
export default {
plugins: [uiPlugin()],
};For dynamic @ui.fn.* and @ui.form.validate, also register @atscript/ui-fns/plugin.
Form types
FormDef
Complete form definition produced by createFormDef(type).
interface FormDef {
type: TAtscriptAnnotatedType;
rootField: FormFieldDef;
fields: FormFieldDef[];
flatMap: Map<string, TAtscriptAnnotatedType>;
}FormFieldDef
Thin pointer to one atscript prop. Metadata lives on prop.metadata and is resolved on demand.
interface FormFieldDef {
/** Dot-separated path relative to the parent data container. `""` = root. */
path: string;
prop: TAtscriptAnnotatedType;
/** Render-kind: structural names (`array`, `object`, `union`, `tuple`) or primitive override from `@ui.form.type`. */
type: string;
/**
* Optional `@ui.form.type` / `@ui.type` override that lets a structured kind
* (`array`, `object`, `union`, `tuple`) dispatch to a different built-in
* renderer in the `:types` map. Reserved for built-in ids — custom
* components are wired via `@ui.form.component` + `:components`.
*/
customType?: string;
phantom: boolean;
name: string;
/** True when no `ui.fn.*` keys exist on the prop — perf flag. */
allStatic: boolean;
}FormArrayFieldDef, FormObjectFieldDef, FormUnionFieldDef, FormTupleFieldDef
Extended field defs for structural kinds.
interface FormArrayFieldDef extends FormFieldDef {
itemType: TAtscriptAnnotatedType;
itemField: FormFieldDef;
}
interface FormObjectFieldDef extends FormFieldDef {
objectDef: FormDef;
}
interface FormUnionFieldDef extends FormFieldDef {
unionVariants: FormUnionVariant[];
}
interface FormTupleFieldDef extends FormFieldDef {
itemFields: FormFieldDef[];
}FormUnionVariant
One branch of a union.
interface FormUnionVariant {
label: string;
type: TAtscriptAnnotatedType;
/** Pre-built FormDef for object variants. */
def?: FormDef;
/** Pre-built field def for primitive variants. */
itemField?: FormFieldDef;
/** "string" | "number" | "boolean" for primitive variants. */
designType?: string;
}TFormAction
interface TFormAction {
id: string;
label: string;
}TFormEntryOptions
A select/radio option — plain string or { key, label } pair.
type TFormEntryOptions = { key: string; label: string } | string;Form factories
createFormDef(type, opts?)
Builds a FormDef from an annotated type. Walks props, pre-resolves structural sub-defs, and caches the flat map.
function createFormDef(type: TAtscriptAnnotatedType, opts?: { versionColumn?: string }): FormDef;When opts.versionColumn is supplied, the matching prop is excluded from the returned fields[] (so AsForm's renderer doesn't paint it) but remains in flatMap and in the form's underlying data wrapper. This is the contract OCC needs: hide the version input from users while keeping the value in the wire payload so the server can lift it into $cas. Pass meta.versionColumn directly.
formDef.value = createFormDef(deserializeAnnotatedType(meta.type), {
versionColumn: meta.versionColumn,
});See the Edit forms with optimistic concurrency pattern guide for the end-to-end flow.
buildUnionVariants(type)
Returns the variant list for a union prop — used internally by createFormDef for union fields and union array items.
function buildUnionVariants(type: TAtscriptAnnotatedType): FormUnionVariant[];createFormData(type, resolver?)
Creates the wrapped data container { value: domainData } populated from atscript defaults and @meta.default. The optional resolver (TFormValueResolver) overrides the per-field defaulting strategy.
function createFormData<T extends TAtscriptAnnotatedType>(
type: T,
resolver?: TFormValueResolver,
): { value: TAtscriptDataType<T> };createFormValueResolver(data?, context?)
Returns a value resolver that reads @meta.default or @ui.form.fn.value, given a scope.
type TFormValueResolver = (prop: TAtscriptAnnotatedType, path: string) => unknown;
function createFormValueResolver(
data?: Record<string, unknown>,
context?: Record<string, unknown>,
): TFormValueResolver;Table types
TableDef
Complete table definition built by createTableDef(meta, type).
interface TableDef {
type: TAtscriptAnnotatedType;
columns: ColumnDef[];
flatMap: Map<string, TAtscriptAnnotatedType>;
primaryKeys: string[];
/** Preferred row identifier for URL/wire addressing. */
preferredId: string[];
/** Mirrors `MetaResponse.versionColumn` — name of the OCC version column when the table opts into `@db.column.version`. */
versionColumn?: string;
crud: TCrudPermissions;
canRemove: boolean;
actions: TableActionsModel;
searchable: boolean;
vectorSearchable: boolean;
searchIndexes: SearchIndexInfo[];
relations: RelationInfo[];
}createTableDef propagates versionColumn from the meta envelope and skips the matching entry from def.columns, so filter/sort/column-picker dialogs ignore the version column automatically — see Edit forms with optimistic concurrency.
ColumnDef
A single column definition built from field metadata + annotations.
interface ColumnDef {
path: string;
label: string;
type: string;
component?: string;
/** Extra sibling leaf paths to fetch when this column is visible — see [Custom Cells](/tables/custom-cells). */
selectWith?: string[];
sortable: boolean;
filterable: boolean;
nullable: boolean;
visible: boolean;
width?: string;
maxLen?: number;
order: number;
options?: { key: string; label: string }[];
valueHelpInfo?: ValueHelpInfo;
currencyCode?: string;
currencyRefField?: string;
unitCode?: string;
unitRefField?: string;
precisionScale?: number;
/** Synthesised columns (e.g. row-actions) — locked chrome, excluded from `columnNames`. */
fixed?: boolean;
}MetaResponse
The payload returned by the moost-db /meta endpoint.
interface MetaResponse {
searchable: boolean;
vectorSearchable: boolean;
searchIndexes: SearchIndexInfo[];
primaryKeys: string[];
preferredId: string[];
versionColumn?: string;
crud: TCrudPermissions;
actions: TDbActionInfo[];
relations: RelationInfo[];
fields: Record<string, FieldMeta>;
type: TSerializedAnnotatedType;
}
interface FieldMeta {
sortable: boolean;
filterable: boolean;
}
interface SearchIndexInfo {
name: string;
description?: string;
type?: "text" | "vector";
}
interface RelationInfo {
name: string;
direction: "to" | "from" | "via";
isArray: boolean;
}versionColumn?: string — name of the server-managed row version column on OCC-protected tables (declared with @db.column.version in your .as schema). Absent on tables that don't opt into OCC. Consumer code should pass this through to createFormDef so the version field doesn't render as an editable input, while still riding in the form data for the server's $cas round-trip. See the Edit forms with optimistic concurrency pattern guide for the full flow.
TableActionsModel
Server-declared actions grouped by level.
interface TableActionsModel {
table: TDbActionInfo[];
row: TDbActionInfo[];
rows: TDbActionInfo[];
default: {
table?: TDbActionInfo;
row?: TDbActionInfo;
rows?: TDbActionInfo;
};
}TableQueryState, SortControl, PaginationControl
interface SortControl {
field: string;
direction: "asc" | "desc";
}
interface PaginationControl {
page: number;
itemsPerPage: number;
}
interface TableQueryState {
sort?: SortControl[];
pagination?: PaginationControl;
search?: string;
filters?: Record<string, unknown>;
}Table factory
createTableDef(meta, type)
Builds a TableDef from a MetaResponse plus the deserialized atscript type.
function createTableDef(meta: MetaResponse, type: TAtscriptAnnotatedType): TableDef;See Annotations Reference for how @ui.table.* and @db.* annotations populate ColumnDef.
Field resolver
The resolver is pluggable so static-only consumers ship with zero new Function overhead and dynamic consumers (@atscript/ui-fns) opt in by replacing the global instance.
FieldResolver
interface FieldResolver {
resolveFieldProp<T>(
prop: TAtscriptAnnotatedType,
fnKey: string,
staticKey: string | undefined,
scope: Record<string, unknown>,
opts?: TResolveOptions<T>,
): T | undefined;
resolveFormProp<T>(
type: TAtscriptAnnotatedType,
fnKey: string,
staticKey: string | undefined,
scope: Record<string, unknown>,
opts?: TResolveOptions<T>,
): T | undefined;
hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean;
}
interface TResolveOptions<T> {
staticAsBoolean?: boolean;
transform?: (raw: unknown) => T;
}StaticFieldResolver
Class implementing FieldResolver with static-only semantics — fn keys are ignored.
setResolver(resolver) / getResolver() / defaultResolver
function setResolver(resolver: FieldResolver): void;
function getResolver(): FieldResolver;
const defaultResolver: StaticFieldResolver;Standalone helpers
function resolveFieldProp<T>(prop, fnKey, staticKey, scope, opts?): T | undefined;
function resolveFormProp<T>(type, fnKey, staticKey, scope, opts?): T | undefined;
function resolveStatic<T>(metadata, staticKey, opts?): T | undefined;
function hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean;
function getFieldMeta<K extends keyof AtscriptMetadata>(
prop: TAtscriptAnnotatedType,
key: K,
): AtscriptMetadata[K] | undefined;resolveStatic is exposed so dynamic resolvers (in ui-fns) can fall back to it without duplicating logic.
parseStaticAttrs(value) / resolveAttrs(prop, scope, keys?)
Resolve @ui.form.attr and the dynamic @ui.form.fn.attr counterpart into a single record. keys overrides the static/fn key pair for @ui.table.attr and friends.
function parseStaticAttrs(value: unknown): Record<string, unknown> | undefined;
function resolveAttrs(
prop: TAtscriptAnnotatedType,
scope: Record<string, unknown>,
keys?: { staticKey?: string; fnKey?: string },
): Record<string, unknown> | undefined;Annotation key constants
Every supported annotation has a stringly-typed constant exported from @atscript/ui so consumers can build resolvers, mappers, or codegen against named keys instead of magic strings.
Cross-surface
| Name | Value |
|---|---|
UI_TYPE | "ui.type" |
Form static keys
UI_FORM_PLACEHOLDER, UI_FORM_HINT, UI_FORM_CLASSES, UI_FORM_STYLES, UI_FORM_AUTOCOMPLETE, UI_FORM_DISABLED, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_TYPE, UI_FORM_COMPONENT, UI_FORM_HIDDEN, UI_FORM_ATTR, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_SUBMIT_TEXT, UI_FORM_LABEL_SINGULAR, UI_FORM_ACTION, UI_FORM_PREFIX, UI_FORM_PREFIX_REF, UI_FORM_PREFIX_ICON, UI_FORM_SUFFIX, UI_FORM_SUFFIX_REF, UI_FORM_SUFFIX_ICON, UI_FORM_VALIDATE.
Form dynamic keys
UI_FORM_FN_PREFIX, UI_FORM_FN_LABEL, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_HINT, UI_FORM_FN_HIDDEN, UI_FORM_FN_DISABLED, UI_FORM_FN_READONLY, UI_FORM_FN_OPTIONS, UI_FORM_FN_ATTR, UI_FORM_FN_VALUE, UI_FORM_FN_CLASSES, UI_FORM_FN_STYLES, UI_FORM_FN_TITLE, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_SUBMIT_DISABLED.
Table static keys
UI_TABLE_WIDTH, UI_TABLE_COMPONENT, UI_TABLE_SELECT_WITH, UI_TABLE_HIDDEN, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_ORDER.
Table dynamic keys
UI_TABLE_FN_PREFIX, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_STYLES.
Dictionary
UI_DICT_LABEL, UI_DICT_DESCR, UI_DICT_ATTR, UI_DICT_FILTERABLE, UI_DICT_SORTABLE, UI_DICT_SEARCHABLE.
DB-aware
DB_REL_FK, DB_HTTP_PATH, DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_UNIT, DB_UNIT_REF, DB_COLUMN_PRECISION.
Workflow
WF_ACTION_WITH_DATA.
Meta / expect
META_LABEL, META_ID, META_DESCRIPTION, META_READONLY, META_REQUIRED, META_DEFAULT, META_SENSITIVE, EXPECT_MAX_LENGTH.
Validators
getFormValidator(def, opts?)
Returns a reusable validator function bound to a FormDef. Built once, called per submit.
function getFormValidator(
def: FormDef,
opts?: Partial<TValidatorOptions>,
): (callOpts: {
data: Record<string, unknown>;
context?: Record<string, unknown>;
}) => Record<string, string>;Returns an errors record keyed by dot-separated field path (empty when valid).
createFieldValidator(prop, opts?)
Field-level validator with internal Validator caching. Returns true on success or the first error message string.
function createFieldValidator(
prop: TAtscriptAnnotatedType,
opts?: { rootOnly?: boolean },
): (value: unknown, externalCtx?: { data: unknown; context: unknown }) => true | string;Default validator plugins
setDefaultValidatorPlugins(plugins) installs validator plugins globally; both getFormValidator and createFieldValidator apply them. getDefaultValidatorPlugins() returns the current list. @atscript/ui-fns uses this to wire its uiFnsValidatorPlugin().
function setDefaultValidatorPlugins(plugins: TValidatorPlugin[]): void;
function getDefaultValidatorPlugins(): TValidatorPlugin[];See the Validation guide for end-to-end usage.
Path utilities
Form data is wrapped: { value: domainData }. These helpers de-reference the wrapper automatically.
getByPath(data, path) / setByPath(data, path, value)
function getByPath(obj: Record<string, unknown>, path: string): unknown;
function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void;path === "" returns or replaces the entire obj.value. Intermediate objects are auto-created on set.
detectUnionVariant(value, variants)
Returns the index of the variant matching value. Uses a discriminator when atscript detects one, otherwise probes each variant's validator. Falls back to 0.
function detectUnionVariant(value: unknown, variants: FormUnionVariant[]): number;Value-help
ValueHelpInfo
Sync probe for a value-help-eligible prop (FK or ref).
interface ValueHelpInfo {
/** HTTP path of the value-help target. */
url: string;
/** Field on the target that this FK references. */
targetField: string;
}extractValueHelp(prop) / extractLiteralOptions(prop) / isPureLiteralUnion(prop)
function extractValueHelp(prop: TAtscriptAnnotatedType): ValueHelpInfo | undefined;
function extractLiteralOptions(
prop: TAtscriptAnnotatedType,
): { key: string; label: string }[] | undefined;
function isPureLiteralUnion(prop: TAtscriptAnnotatedType): boolean;ValueHelpClient
Thin wrapper over a pre-built Client (from @atscript/db-client) that runs FK-flavoured searches against the value-help target. Most consumers stick to resolveValueHelp() plus their own Client; this class formalises the search semantics (full-text $search when the target is searchable, $or-regex fallback otherwise) so picker UIs don't reimplement them.
class ValueHelpClient {
constructor(client: Client);
search(resolved: ResolvedValueHelp, opts?: ValueHelpSearchOptions): Promise<ValueHelpResult>;
}
interface ValueHelpSearchOptions {
/** Search term. Empty / undefined returns all records. */
text?: string;
/** `"form"` = PK + label + descr; `"filter"` = all dict fields including attrs. Default: `"form"`. */
mode?: "form" | "filter";
/** Max results. Default: 20. */
limit?: number;
/** Override the computed `$select` fields. */
select?: string[];
}
interface ValueHelpResult {
items: Record<string, unknown>[];
}resolveValueHelp(url) / resetValueHelpCache()
Globally caches resolved value-help endpoints by URL so multiple fields pointing at the same target share one /meta fetch.
function resolveValueHelp(url: string): Promise<ResolvedValueHelp>;
function resetValueHelpCache(): void;
interface ResolvedValueHelp {
url: string;
primaryKeys: string[];
labelField: string;
descrField: string | undefined;
attrFields: string[];
filterableFields: string[];
sortableFields: string[];
searchable: boolean;
targetType: TAtscriptAnnotatedType;
}valueHelpDictPaths(resolved)
Returns the dict-view path set of a resolved value-help target (PKs + label + descr + attr fields). Filter dialogs use it to clamp visible columns to the dictionary subset.
function valueHelpDictPaths(resolved: ResolvedValueHelp): Set<string>;Option helpers
function optKey(opt: TFormEntryOptions): string;
function optLabel(opt: TFormEntryOptions): string;
function parseStaticOptions(value: unknown): TFormEntryOptions[] | undefined;
function resolveOptions(
prop: TAtscriptAnnotatedType,
scope: Record<string, unknown>,
): TFormEntryOptions[] | undefined;resolveOptions checks @ui.form.options, then @ui.form.fn.options via the active resolver, then literal-union extraction.
Grid layout
Framework-agnostic helpers for @ui.form.grid.colSpan / @ui.form.grid.rowSpan. Each annotation has the shape { desktop, narrow? } — getFieldMeta(prop, UI_FORM_GRID_COL_SPAN) returns a GridSpanArgs.
const DEFAULT_COL_SPAN: number; // 12
const DEFAULT_ROW_SPAN: number; // 1
interface GridSpec {
col: { desktop: number; narrow: number };
row: { desktop: number; narrow: number };
}
interface GridSpanArgs {
desktop: string;
narrow?: string;
}
/** Accepts `"1"`–`"12"` and the aliases `"full"` (12), `"half"` (6), `"third"` (4). */
function parseColSpan(raw: string | undefined): number | undefined;
/** Accepts numeric strings `"1"`+; rejects `"0"`, negatives, decimals, aliases. */
function parseRowSpan(raw: string | undefined): number | undefined;
/** Compose a resolved spec from already-extracted `colSpan` / `rowSpan` annotation values. */
function resolveGridSpec(
colSpan: GridSpanArgs | undefined,
rowSpan: GridSpanArgs | undefined,
): GridSpec;
/** Emit `col-span-X` / `row-span-X` + `as-narrow:` variants for the spec. */
function buildGridClasses(spec: GridSpec): string;See Grid Layout.
Decimal helpers
Framework-agnostic decimal-string formatting and parsing — shared by @atscript/vue-form AsDecimal and @atscript/vue-table cells. Storage is string-only so DB-precision decimals never bounce through floats.
interface CurrencyDisplay {
decimals: number;
symbol: string;
}
interface DecimalParts {
integer: string;
fraction: string;
sign: "+" | "-";
}
interface FormatDecimalOptions {
currency?: string;
locale?: string;
/** Number of fractional digits to display. */
scale?: number;
}
function enforceScale(value: string, scale: number): string;
function formatDecimalForDisplay(value: string, opts?: FormatDecimalOptions): string;
function parseDecimalInput(input: string, opts?: FormatDecimalOptions): string | undefined;
function getCurrencyDecimals(currency: string, locale?: string): number;
function getCurrencyDisplayParts(currency: string, locale?: string): CurrencyDisplay;
function getDecimalSeparator(locale?: string): string;
function getThousandsSeparator(locale?: string): string;
function groupInteger(integer: string, locale?: string): string;
function joinDecimalString(parts: DecimalParts): string;
function splitDecimalString(value: string): DecimalParts;Column helpers
function getVisibleColumns(def: TableDef, hidden?: Set<string>): ColumnDef[];
function getSortableColumns(def: TableDef): ColumnDef[];
function getFilterableColumns(def: TableDef): ColumnDef[];
function getColumn(def: TableDef, path: string): ColumnDef | undefined;Error map utilities
/** Merge any number of partial error maps; falsy values are dropped, later maps win when both have a string. */
function mergeErrorMaps(
...maps: Array<Record<string, string | undefined> | undefined>
): Record<string, string>;
/** Yields every ancestor prefix longest-first, including the path itself. `"a.b.c"` → `"a.b.c", "a.b", "a"`. */
function* iteratePathAncestors(path: string): Generator<string>;
/** Build `Map<absolutePath, descendantErrorCount>` so each struct in the tree renders an error-count badge in O(1). */
function buildDescendantErrorCounts(
errors: Record<string, string | undefined>,
): Map<string, number>;buildDescendantErrorCounts powers the count badges on AsObject headers.
Type guards
function isArrayField(field: FormFieldDef): field is FormArrayFieldDef;
function isObjectField(field: FormFieldDef): field is FormObjectFieldDef;
function isUnionField(field: FormFieldDef): field is FormUnionFieldDef;
function isTupleField(field: FormFieldDef): field is FormTupleFieldDef;Misc utilities
function asArray<T>(x: T | T[]): T[];
/** `@ui.form.label.singular` for array fields; falls back to `"item"`. */
function resolveSingularLabel(prop: TAtscriptAnnotatedType | undefined): string;
/** Always returns a `MeasurementInfo` — individual fields are `undefined` when their annotation is absent. */
function extractMeasurement(prop: TAtscriptAnnotatedType): MeasurementInfo;
function str(value: unknown): string;extractMeasurement reads @db.amount.currency* / @db.unit* / @db.column.precision and returns a structured info record consumed by AsField for currency/unit adornments.
Client factory
ClientFactory is the contract Vue tables and value-help use to build HTTP clients. Override globally to inject auth headers / retries / interceptors.
type ClientFactory = (url: string) => Client; // `Client` from `@atscript/db-client`
function setDefaultClientFactory(factory: ClientFactory): void;
function getDefaultClientFactory(): ClientFactory; // never `undefined` — falls back to `(url) => new Client(url)`
function resetDefaultClientFactory(): void;Meta cache
A single /meta fetch per URL is cached across useTable instances and resolveValueHelp calls. getMetaEntry is synchronous — the promises on the entry resolve once the underlying fetch settles.
function getMetaEntry(url: string, factory?: ClientFactory): MetaCacheEntry;
function resetMetaCache(): void;
interface MetaCacheEntry {
client: Client; // from `@atscript/db-client`
meta: Promise<MetaResponse>;
type: Promise<TAtscriptAnnotatedType>; // pre-deserialized
resolved?: Promise<ResolvedValueHelp>; // populated lazily by `resolveValueHelp`
tableDef?: Promise<TableDef>; // populated lazily by Vue `useTable`
}Cross-links
- Forms — Annotations Reference
- Forms — Validation
- Forms — References (FK)
- Tables — Annotations Reference
- @atscript/ui-fns — dynamic resolver
- atscript.dev —
.assyntax andValidator