Forms
Adapted from the package's src/form/README.md. The web app wraps this in apps/web/src/adapter/form.ts (localized required / selectRequired rules and a default calendarDateTimeCodec).
Mental model
useTamanForm(options)
-> Form Vue component rendered in the template
-> formApi controller for values, validation, submission, and schema changes
- Raw values — what field components use (
CalendarDate, etc.). - Submit values — what
getValues()andhandleSubmitsee (ISO strings after a codec).
Without a codec, both shapes are the same.
Setup
Register named rules once at startup (setupTamanForm). A rule returns true or an error string:
import { isEmptyFormValue, setupTamanForm } from '@vinicunca/taman-ui/form';
setupTamanForm({
rules: {
required(value, _params, context) {
return isEmptyFormValue(value)
? `${context.label ?? context.name} is required`
: true;
},
},
});
Quick start
<script setup lang="ts">
import { useTamanForm, z } from '@vinicunca/taman-ui/form';
interface ProfileFormValues {
email: string;
name: string;
}
const [ProfileForm, formApi] = useTamanForm<ProfileFormValues>({
handleSubmit(values) {
console.log(values);
},
schema: [
{
component: 'Input',
fieldName: 'name',
label: 'Name',
rules: z.string().min(1, 'Name is required'),
},
{
component: 'Input',
fieldName: 'email',
label: 'Email',
rules: z.email('Enter a valid email'),
},
],
});
async function save() {
const values = await formApi.validateAndSubmit();
if (values) {
console.log('Submitted', values);
}
}
</script>
<template>
<ProfileForm />
<button
type="button"
@click="save"
>
Save
</button>
</template>
Render <Form /> before awaiting API methods that need the runtime form. Those methods wait for the first mount; awaiting them without rendering waits forever.
z is a namespace re-export of Zod 4.
Generics
useTamanForm<TFormValues, TComponentType, TComponentPropsMap, TSubmitValues>(options);
The app adapter fixes component types from adapter/components.ts and only asks you for TFormValues / TSubmitValues.
Options
| Option | Default | Purpose |
|---|---|---|
schema | [] | Fields and groups |
commonConfig | {} | Shared field fallbacks |
wrapperClass | grid-cols-1 | Form grid |
compact | false | No reserved message space |
disabled | false | Disables fields and default actions |
showDefaultActions | true | Submit/reset |
actionLayout | rowEnd | inline | newLine | rowEnd |
actionPosition | right | left | center | right |
submitOnEnter | false | |
submitOnChange | false | Debounced by changeDebouncedTime (300 ms) |
scrollToFirstError | false | |
codec | none | Raw ↔ submit |
handleSubmit | (values, rawValues) | |
handleReset | Replaces default reset; call formApi.reset() yourself if you still want a clear | |
handleValuesChange | (rawValues, changedFields, getFormattedValues) — format lazily |
commonConfig.disabled disables fields only.
Schema
Field names support nested paths (profile.email) and arrays (contacts[0].name). Wrap [profile.email] when the dots are literal.
| Property | Purpose |
|---|---|
component | Built-in name, registered name, or Vue component |
fieldName | Value path |
defaultValue | Initial raw value |
label / description / help / suffix | Copy |
componentProps | Object or (ctx) => props |
formFieldProps | validateOn, validators, asyncDebounceMs |
rules | Named rule, Zod schema, null, or another rule name |
dependencies | Recompute when triggerFields change |
hide / disabled / collapsed / collapsible | Visibility |
formItemClass | Grid item classes (cols-span-2, …) |
Dependencies
{
fieldName: 'companyName',
component: 'Input',
dependencies: {
triggerFields: ['accountType'],
resolve: ({ values }) => ({
show: values.accountType === 'business',
required: values.accountType === 'business',
rules: values.accountType === 'business'
? z.string().min(1, 'Company name is required')
: null,
}),
},
}
Groups (component: 'Group' or a schema array of fields) share wrapperClass. Array fields use component: 'Array' with fieldArray.
Validation
Default policy is progressive: no error while typing → blur or submit reveals → further changes revalidate.
formFieldProps: {
validateOn: ['blur'], // or ['change'], or both
asyncDebounceMs: 400,
validators: {
onDynamicAsync: async ({ value }: { value: string }) => {
const available = await checkUsername(value);
return available ? undefined : 'Username is already used';
},
},
}
Async work keeps the field (and default submit) busy. Stale results are dropped on reset / clearValidation.
formApi
Methods that need the runtime wait until <Form /> is mounted.
| Goal | Method |
|---|---|
| One field | setFieldValue(name, value, shouldValidate?) |
| Patch, keep dirty/errors | setValues(patch, filterFields?, shouldValidate?) |
| New record | reset({ values }) or reset() |
| Load API-shaped values | setSubmitValues(values) (requires a codec) |
| Raw / encoded | getRawValues() / getValues() / getValueSnapshot() |
| Validate | validate(), validateField(name), isFieldValid(name) |
| Server error | setFieldError(name, message?) |
| Clear | clearValidation(names?) |
| Submit | validateAndSubmit() (preferred) or submit(event?) |
setValues deep-merges objects, replaces arrays, and by default drops paths not in the schema. It is for patches in the same session, not for opening a different record in a reused drawer — use reset({ values }) for that.
validateAndSubmit() validates once, returns undefined when invalid, otherwise calls handleSubmit(values, rawValues) and returns formatted values.
updateSchema / setState({ schema }) change fields at runtime. formApi.form is the underlying TanStack Form instance for selectors (useStore) and low-level actions.
Slots
| Slot | Use |
|---|---|
default | Full custom layout (built-in actions are omitted) |
#<fieldName> | Replace one field (componentField, error, formApi, values, …) |
submit-before / reset-before | Extra action-area content |
expand-before / expand-after | Collapse control |
Codecs
const codec: FormCodec<FilterFormValues, FilterSubmitValues> = {
decode(values) { /* API → UI */ },
encode(values) { /* UI → API */ },
};
Exports: calendarDateCodec, calendarDateTimeCodec, timeCodec, createCalendarDateCodec, createCalendarDateTimeCodec, createTimeCodec. Failures throw FormCodecError with phase: 'encode' | 'decode'.
The web adapter default-assigns calendarDateTimeCodec when you omit codec.