Actions
Forms ship with a single Submit button by default. To add extra buttons — "Save draft", "Discard", "Send for review", workflow step transitions — declare them as phantom fields annotated with @ui.form.action. The form renders a button per action and emits an action event when the user clicks it. No data is collected for these fields.
The annotation
@ui.form.action 'actionId', 'Button label'
fieldName: ui.action'actionId'is the id emitted back to the host.'Button label'is the rendered text.- The
ui.actionprimitive marks the field as a phantom — no value is stored onformData, no validator runs.
Listen with @action
<AsForm> emits action with the id and the current form data:
<script setup lang="ts">
import { AsForm, createDefaultTypes, createAsFormDef } from "@atscript/vue-form";
import { OrderForm } from "./order-form.as";
const { def, formData } = createAsFormDef(OrderForm);
const types = createDefaultTypes();
function onAction(name: string, data: unknown) {
if (name === "save-draft") saveDraft(data);
if (name === "discard") resetForm();
}
function onSubmit(data: unknown) {
submitOrder(data);
}
</script>
<template>
<AsForm :def="def" :form-data="formData" :types="types" @submit="onSubmit" @action="onAction" />
</template>The action event signature is (name: string, data: TFormData) => void — see useAsForm's emits contract in packages/vue-form/src/composables/use-as-form.ts.
Multi-action form
@meta.label 'Order'
export interface OrderForm {
@meta.label 'Item'
item: string
@meta.label 'Quantity'
quantity: number
@ui.form.action 'save-draft', 'Save draft'
saveDraft: ui.action
@ui.form.action 'discard', 'Discard'
discard: ui.action
}Submit still works as usual. The phantom action fields render as additional buttons inline with the form. Use grid annotations to control their placement:
@ui.form.action 'save-draft', 'Save draft'
@ui.form.grid.colSpan '6'
saveDraft: ui.action
@ui.form.action 'discard', 'Discard'
@ui.form.grid.colSpan '6'
discard: ui.actionCustomising the Submit button
The Submit button is rendered by <AsForm> itself (not a phantom field). Two annotations on the root interface customise it:
@meta.label 'Registration'
@ui.form.submit.text 'Register'
@ui.form.fn.submit.disabled '(data) => !data.email'
export interface Registration { ... }@ui.form.submit.text— static label@ui.form.fn.submit.text— dynamic label (requires@atscript/ui-fns)@ui.form.fn.submit.disabled— dynamic disabled state
To suppress the built-in Submit button entirely (host chrome owns it):
<AsForm :def="def" :form-data="formData" :types="types" hide-submit />Or override the slot:
<AsForm :def="def" :form-data="formData" :types="types">
<template #form.submit="{ disabled, text }">
<MyPrimaryButton type="submit" :disabled>{{ text }}</MyPrimaryButton>
</template>
</AsForm>Unsupported actions
When the emitted action id doesn't match any @ui.form.action (or @wf.action.withData) in the def, <AsForm> fires unsupported-action instead of action. Useful as a no-op guard rail when controllers refactor action ids.
<AsForm @action="onAction" @unsupported-action="(name) => console.warn('Unknown action:', name)" />Workflow actions
In a workflow form (<AsWfForm>, see Workflows), @wf.action.withData is a workflow-aware variant of @ui.form.action that includes the form data with the action invocation. It's recognised by the same action-routing pipeline — see Workflow actions for the full picture.
The AsAction component
The default AsAction component (Tier 2 swap target) is intentionally minimal: a <button type="button"> that emits action with its formAction.id. Swap it through the :types map to use your design system's button:
<script setup lang="ts">
import { createDefaultTypes } from "@atscript/vue-form";
import MyActionButton from "./MyActionButton.vue";
const types = { ...createDefaultTypes(), action: MyActionButton };
</script>The custom component receives the standard TAsComponentProps contract — inspect formAction for the id and label, and emit 'action' with the id:
<script setup lang="ts">
import type { TAsComponentProps, TAsComponentEmits } from "@atscript/vue-form";
defineProps<TAsComponentProps>();
const emit = defineEmits<TAsComponentEmits>();
</script>
<template>
<button type="button" class="c8-light" @click="formAction && emit('action', formAction.id)">
{{ formAction?.label }}
</button>
</template>See Custom field components for the full TAsComponentProps contract.
Next steps
- Customization — override
AsActionglobally - Dynamic fields —
@ui.form.fn.submit.disabledand friends - Workflow actions —
@wf.action.withDatafor partial-data flows