# @macrulez/vue-form-schema — AI Reference Reactive Vue 3 forms generated from a schema — plain JSON, or converted from Zod/Yup/Valibot/OpenAPI — with validation (sync + async), input masking, conditional field visibility via a sandboxed expression language, dynamic array fields, multi-step wizards, server-error mapping, SSR-safe persistence, and an optional Vue DevTools panel. Version 0.2.7. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs), and prose is kept to the minimum needed to use the API correctly. For human-readable narrative docs (why you'd reach for each piece, worked examples), see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/vue-form-schema/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/vue-form-schema/guide/overview - GitHub: https://github.com/macrulezru/vue-form-schema - npm: https://www.npmjs.com/package/@macrulez/vue-form-schema Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map — what to import from where No app-level plugin/provider is required to use `useForm()` itself — unlike some form/state libraries, there's no `app.use(...)` step needed for the core composable to work. `app.use()` is only relevant for two OPTIONAL pieces: a global custom-component registry (`createFormRegistry`, section 8) and, in a separate subpath, the DevTools panel (`installFormDevtools`, section 10). | Import path | Install | Contains | |---|---|---| | `@macrulez/vue-form-schema` | `npm install @macrulez/vue-form-schema` | Core: `useForm` + all composables, types, validators, mask engine, schema utilities, `parseJSON` (sections 3–9). | | `@macrulez/vue-form-schema/zod` | (same install, peer `zod`) | `parseZod` (section 6.2). | | `@macrulez/vue-form-schema/yup` | (same install, peer `yup`) | `parseYup`. | | `@macrulez/vue-form-schema/valibot` | (same install, peer `valibot`) | `parseValibot`. | | `@macrulez/vue-form-schema/openapi` | (same install, no peer) | `parseJSONSchema` / `parseOpenAPI` — real JSON Schema / OpenAPI documents (section 6.3), NOT the same input shape as core's own `parseJSON`. | | `@macrulez/vue-form-schema/ui` | (same install) | `FormRenderer`, `MultiStepFormRenderer` — unstyled/headless (section 9). | | `@macrulez/vue-form-schema/ui/tailwind` | (same install) | Tailwind-styled `TailwindFormRenderer` + prefixed field components (`Tw*`). | | `@macrulez/vue-form-schema/ui/shadcn` | (same install) | shadcn-styled `ShadcnFormRenderer` + `Sh*` field components. | | `@macrulez/vue-form-schema/ui/primevue` | (same install, peer `primevue`) | PrimeVue-styled `PrimeVueFormRenderer` + `Pv*` field components. | | `@macrulez/vue-form-schema/ui/naive` | (same install, peer `naive-ui`) | Naive UI-styled `NaiveFormRenderer` + `Nu*` field components. | | `@macrulez/vue-form-schema/devtools` | (same install, optional peer `@vue/devtools-api`) | `installFormDevtools(app)` — must be called manually, dev-only (section 10). | | `@macrulez/vue-form-schema/style.css` | (same install) | Base styles for the headless `FormRenderer` (`vfs-*` class names). | **`@macrulez/nuxt-vue-form-schema` (the Nuxt module, section 12) is NOT currently published to npm** (`npm view @macrulez/nuxt-vue-form-schema` → 404 at the time of writing) — `npm install` for it will fail. The core package still works perfectly inside a Nuxt app without the module (it's plain Vue composables/components, no Nuxt-specific runtime dependency); you only lose the module's auto-imports and prefixed global components. Verify current publish status before recommending an install command for it. --- ## 2. Core types (verbatim from `core/types.ts`) ```ts type ValidatorFn = (value: unknown, values: Record) => string | null type AsyncValidatorFn = (value: unknown, values: Record) => Promise type MaskPreset = 'phone-ru' | 'phone-eu' | 'date' | 'inn' | 'iban' interface MaskConfig { preset?: MaskPreset; pattern?: string } // '#' = digit, 'A' = letter, other chars are literals interface FieldOption { label: string; value: unknown } type FieldType = 'text' | 'number' | 'email' | 'select' | 'checkbox' | 'radio' | 'textarea' | 'date' | 'array' | 'group' | 'file' interface FieldDefinition { type: FieldType name: string // dot-path for nested fields, e.g. "address.city" label?: string placeholder?: string defaultValue?: unknown | ((values: Record) => unknown) required?: boolean disabled?: boolean | ((values: Record) => boolean) visible?: boolean | string | ((values: Record) => boolean) // string = safe expression, see section 5 validators?: ValidatorFn[] asyncValidators?: AsyncValidatorFn[] mask?: string | MaskConfig options?: FieldOption[] | ((values: Record) => FieldOption[] | Promise) optionsDeps?: string[] // field names that trigger re-fetching async `options` optionsLoading?: boolean // internal — set by useForm, read-only for consumers fields?: FieldDefinition[] // for 'group' and 'array' transform?: (value: unknown, values: Record) => unknown // applied on every setField, before storing parse?: (raw: unknown) => unknown // applied at submit time, after validation passes component?: Component | string // custom renderer for this field; must emit 'update:modelValue' and 'blur' accept?: string; multiple?: boolean; maxSize?: number; maxFiles?: number // 'file' type only } interface FormFieldProps { field: FieldDefinition; modelValue: unknown; error: string[]; touched: boolean } type ValidateOn = 'input' | 'blur' | 'submit' | 'eager' type ValidateMode = 'first' | 'all' interface UseFormConfig> { schema: FieldDefinition[] | JSONSchema initialValues?: Partial validateOn?: ValidateOn // default 'blur' validateMode?: ValidateMode // default 'first' clearOnHide?: boolean // default false onSubmit?: (values: T) => void | Promise persist?: false | 'session' | 'local' // default false persistKey?: string // default: 'vfs:' + field names joined with ',' — see section 4's persist note debug?: boolean // default false — console.group logs on every value change } interface UseFormReturn> { fields: ComputedRef // resolved (visible/disabled/options evaluated) — NOT the raw schema values: Ref errors: Ref> touched: Ref> optionsLoading: Ref> isDirty: ComputedRef isValid: ComputedRef isSubmitting: Ref submit(): Promise reset(values?: Partial): void setField(path: string, value: unknown): void getField(path: string): unknown } type BuiltinRule = 'required' | 'minLength' | 'maxLength' | 'min' | 'max' | 'pattern' | 'email' | 'url' ``` --- ## 3. Schema authoring & auto-detection `useForm({ schema })` accepts EITHER a `FieldDefinition[]` (this library's native, function-capable shape) OR a `JSONSchema` (`JSONSchemaField[]` — a plain-data subset using `default` instead of `defaultValue` and `{ rule, value?, message? }` validator objects instead of functions, since it has to survive `JSON.parse`/serialize round-trips). `useForm` auto-detects which one it got via `isJSONSchema()`: - If ANY field has a function in its `validators` array → treated as `FieldDefinition[]` (skips JSON detection entirely). - Otherwise, treated as `JSONSchema` (and run through `parseJSON`, section 6.1) ONLY if at least one field has a `default` key, or a `validators` array whose first entry is an object with a `rule` key. - **A `FieldDefinition[]` with no `validators` and no `defaultValue` at all is ambiguous by this heuristic and falls through to being treated as `FieldDefinition[]` as-is** (the JSON-schema markers just never matched) — which is the correct outcome, but worth knowing the detection is heuristic-based, not derived from a type tag in the data. `options`/`fields` semantics: `type: 'group'` renders (and validates) its `fields` as a nested namespace under `name` (dot-path); `type: 'array'` pairs with `useFieldArray` (section 7) — its `fields` describe ONE row's shape, using bare field names (row-index prefixing is applied at runtime, not in the schema itself). --- ## 4. `useForm(config)` ```ts // Overload 1 — schema from parseZod/parseYup/parseValibot/parseJSONSchema/parseOpenAPI: // T is inferred automatically from the adapter's branded return type. function useForm>( config: Omit, 'schema'> & { schema: TypedFieldDefinitions } ): UseFormReturn // Overload 2 — plain FieldDefinition[] / JSONSchema: T defaults to Record // unless given explicitly via useForm({ schema }). function useForm = Record>( config: UseFormConfig ): UseFormReturn ``` Must be called inside a component `setup()` — it registers `onMounted`/`onUnmounted` hooks on the current instance (restoring persisted values / fetching initial async options happens `onMounted`; the DevTools registry entry is cleaned up `onUnmounted`). ### 4.1 Initial values Built by `buildInitialValues`: for each field, `initialValues` (config-level override) wins over the field's own `defaultValue`; a function `defaultValue` is evaluated AFTER all static defaults are resolved (so it can read sibling fields' resolved values, but NOT other function-defaulted fields — those resolve in the same pass, order not guaranteed against each other). `'array'` fields default to `[]` if nothing else applies (a function `defaultValue` on an array field is ignored — only static defaults apply there). Keys in `initialValues` that don't match any schema field are still merged into the initial `values` object verbatim. ### 4.2 `validateOn` — exact trigger rules | Mode | `setField()` (on every value change) | `touchField()` (on blur, via FormRenderer) | |---|---|---| | `'blur'` (default) | no validation | validates that field | | `'input'` | validates that field | validates that field | | `'submit'` | no validation | no validation — only `submit()` validates | | `'eager'` | validates ONLY if the field was already blurred once before | validates that field, and marks it "blurred" for future `'eager'` input-time checks | `submit()` (4.5) always runs full validation across every field regardless of `validateOn`. ### 4.3 Persistence (`persist`) ```ts persist?: false | 'session' | 'local' // default false — 'session'/'local' → sessionStorage/localStorage persistKey?: string // default: `vfs:${fieldNames.join(',')}` ``` **The default key is a literal comma-joined list of field names, NOT a hash** despite the type's own doc comment describing it as "a hash of the schema field names" — two forms with the same field names (in the same order) share the same default storage key and will read/write each other's persisted state if `persistKey` isn't set explicitly. Persisted values are restored `onMounted` (client-only — nothing reads storage during SSR) and merged shallowly on top of the built initial values (`{ ...initial, ...parsed }` — a top-level merge, not deep, so a persisted partial nested-group value replaces the whole group, not just its own keys). Saved on every `values` change (`watch(values, ..., { deep: true })`) while `persist` is truthy. `reset()` clears the stored key and briefly suppresses the persist watcher for one microtask so the removal isn't immediately undone by the watcher's own reaction to `reset()`'s value change. ### 4.4 `isDirty` / `isValid` — what they actually check, and timing ```ts isDirty: ComputedRef // deep-compares `values` against a ONE-TIME snapshot of the initial values taken at useForm() call time isValid: ComputedRef ``` - `isDirty`'s baseline is captured once, synchronously, when `useForm` runs — it does NOT track a live `initialValues` ref if you pass one that changes later; only `reset(newValues)` establishes a new baseline going forward implicitly (by rebuilding `values` from `newValues`, though `isDirty`'s own snapshot is NOT recomputed by `reset()` — it stays pinned to the very first call's initial values). The deep-equal check special-cases `File` objects (compared by reference, since they can't be structurally compared). - `isValid` combines a **synchronous, always-fresh** full re-validation (`engine.validateAll`, recomputed every time `isValid` is read, from the CURRENT `values`) with whatever's already sitting in `errors.value` for async validators. **This means `isValid` can read `true` while an async validator is still pending** — a not-yet-resolved async check hasn't written its error yet, so it doesn't count against `isValid` until it actually resolves. `submit()` (4.5) is the only path that reliably waits for async validators before deciding pass/fail. ### 4.5 `submit()` ```ts submit(): Promise ``` 1. Marks every field (recursively, including nested group/array fields) as `touched`. 2. Runs full sync validation for every field. 3. Runs every field's async validators **immediately, bypassing the normal 300ms debounce** (`validateAllAsync`), and awaits all of them — this is what makes `submit()` a reliable pass/fail check unlike `isValid` (4.4). 4. If ANY field (sync or async) has an error, `errors` is updated and `submit()` returns without calling `onSubmit` — no exception thrown, just a silent early return (check `errors`/`isValid` after `await`ing `submit()` if you need to know whether it actually submitted). 5. Otherwise runs `field.parse` transforms (post-validation, pre-call) over the values, sets `isSubmitting`, calls `onSubmit(parsed)`, and resets `isSubmitting` in a `finally`. An exception thrown by `onSubmit` propagates out of `submit()` (after `isSubmitting` is reset) — callers should catch it themselves if needed. ### 4.6 `setField` / `getField` / `reset` ```ts setField(path: string, value: unknown): void // path is a dot-path; runs field.transform first if set getField(path: string): unknown reset(values?: Partial): void // rebuilds from `values` (or the ORIGINAL config.initialValues if omitted), clears errors/touched, clears persisted storage ``` `reset()` always falls back to the config's ORIGINAL `initialValues` object when called with no argument — not whatever the form's values happened to be at some earlier point, and not a live-updated version of a reactive `initialValues` you might have passed in (it was destructured once at `useForm()` call time). ### 4.7 Async `options` — a real double-invocation gotcha `FieldDefinition.options` can be a function returning either a plain array or a `Promise`. **Two independent mechanisms call this function, for different reasons:** 1. **`ConditionEvaluator`** (section 5) re-invokes `field.options(values)` inside its `watchEffect` on **every** change to `values` (the whole object, not just this field) as part of resolving `visible`/ `disabled`/`options` reactively — for an async `options` function, it discards the returned Promise immediately (`options instanceof Promise ? undefined : result`) and just lets the earlier value stand. 2. **`useForm`'s own `fetchAsyncOptions`** is what actually awaits the promise and caches the result — triggered once `onMounted`, and again only when a field in `optionsDeps` changes. **Net effect: an async `options` function runs (and any side effects or network calls inside it fire) on every keystroke across the WHOLE form — not just when `optionsDeps` change — even though only the `optionsDeps`-triggered calls' results are ever used.** If the function does something non-idempotent (analytics, a non-cached fetch), expect it to fire far more often than `optionsDeps` alone would suggest. --- ## 5. Conditional fields — `visible` / `disabled` Each can be a plain `boolean`, a function `(values) => boolean`, or a **string expression** evaluated by a small hand-rolled, sandboxed interpreter (`ConditionEvaluator`) — deliberately NOT `eval`/`new Function`, so it can only ever read `values` and cannot execute arbitrary code: - Supported: number/string/`true`/`false`/`null`/`undefined` literals, `values.path.to.field` / `values['dynamic']` member access (`__proto__`/`prototype`/`constructor` keys always read as `undefined`), parentheses, unary `!`/`-`, `* / %`, `+ -`, `< > <= >=`, `== != === !==`, `&& ||`, ternary `?:`. - `values` is the ONLY identifier the expression may reference — any other bare identifier throws. - **On any parse error or unknown identifier, the expression fails OPEN**: `visible`/`disabled`/whatever resolved to `true` (a console.warn is logged, the field stays visible/becomes... actually `resolveBoolean`'s `defaultValue` argument governs which "true" though — `visible` defaults to visible, `disabled` defaults to NOT-disabled — either way, a broken expression never hides or disables a field by accident, it silently does nothing instead. ```ts { type: 'text', name: 'promoCode', visible: 'values.hasPromo === true' } { type: 'number', name: 'age', disabled: (values) => values.locked === true } ``` `clearOnHide` (a `useForm` config flag, default `false`): when true, a field transitioning from visible → hidden (compared against the PREVIOUS reactive evaluation, not a one-time snapshot) has its value reset to `defaultValue ?? null` — recursively, into nested group/array fields too. Does nothing on the initial render (there's no "previous" state yet) and does nothing for a field that starts hidden and stays hidden. `discriminatedFields(discriminatorName, variants)` (schema-composition helper, section 6.4) is the standard way to build a whole show-one-variant-of-many-based-on-a-select-field pattern on top of `visible` without hand-writing the expression per field. --- ## 6. Schema construction ### 6.1 `parseJSON(schema: JSONSchema): FieldDefinition[]` Converts this library's OWN plain-JSON schema shape (see section 3) — NOT a general JSON Schema/OpenAPI document (that's `parseJSONSchema`/ `parseOpenAPI`, 6.3). Rule objects (`{ rule: 'minLength', value: 5, message?: '...' }`) map to the matching built-in validator (section 7.1); an unrecognized `rule` name is dropped with a console.warn (the field just has no validator for that rule, parsing doesn't throw). ### 6.2 `parseZod` / `parseYup` / `parseValibot` ```ts function parseZod>(schema: S): TypedFieldDefinitions> function parseZod[]>( schema: z.ZodDiscriminatedUnion ): TypedFieldDefinitions> function parseYup(schema: S): TypedFieldDefinitions> function parseValibot(schema: S): TypedFieldDefinitions<...> ``` All three follow the same mapping shape: object/group schema → `type: 'group'` with nested `fields`; string → `'text'`; number → `'number'`; boolean → `'checkbox'`; array → `'array'`; enum → `'select'` with `options` built from the enum values. Each library's own constraint checks (min/max length, email, url, regex, etc.) are mapped to the equivalent built-in validator (7.1) where recognized, PLUS a final catch-all validator that calls the underlying schema's own `safeParse`/equivalent — so a constraint this adapter doesn't special-case still gets enforced, just surfaced as that library's own error message instead of this package's default wording. `z.discriminatedUnion(key, [...])` → a `select` field for the discriminator (options = each variant's literal discriminator value) followed by every variant's OWN fields, each wired through `discriminatedFields` (6.4) so only the currently-selected variant's fields are visible. Combine with `clearOnHide: true` so switching variants actually clears the previous variant's now-hidden values. The return type of all `parseX` functions carries `T` (a `TypedFieldDefinitions`, section 2) via a type-only brand — `useForm({ schema: parseZod(mySchema) })` gets a fully typed `values`/`onSubmit` with NO explicit `useForm(...)` needed. ### 6.3 `parseJSONSchema` / `parseOpenAPI` (`/openapi` subpath) ```ts function parseJSONSchema( schema: S, rootDocument?: JSONSchemaDocument ): TypedFieldDefinitions> function parseOpenAPI>( document: JSONSchemaDocument, selector: string /* local JSON pointer, e.g. '#/components/schemas/User' */ | { path: string; method?: string /* default 'post' */; contentType?: string /* default 'application/json' */ } ): TypedFieldDefinitions ``` Accepts a REAL JSON Schema object (the kind a backend's OpenAPI/Swagger spec already emits) — a materially different input shape from `parseJSON` (6.1), which is this library's own simplified format. `$ref` resolves ONLY local JSON pointers (`#/...`) against `rootDocument` (or `schema` itself if omitted); a remote/external `$ref` throws. Explicitly NOT supported (silently downgraded to a plain `text` field with no extra constraint, never throws): `oneOf`/`anyOf`/`allOf`, `additionalProperties`, `patternProperties`, tuple-form `items`, `exclusiveMinimum`/`exclusiveMaximum` as separate keywords. `enum`/ `const` → `'select'`. `format: 'email'` → `type: 'email'`; `'date'`/ `'date-time'` → `type: 'date'`; `'uri'`/`'url'` → adds the `url` validator on top of whatever base type. `parseOpenAPI`'s inferred value type defaults to `Record` (the extracted schema's shape isn't known statically from a runtime document) — pass an explicit type argument if you already have one generated separately. ### 6.4 Schema composition utilities ```ts mergeSchemas(...schemas: FieldDefinition[][]): FieldDefinition[] // later schemas override earlier ones by field name omitFields(schema, keys: string[]): FieldDefinition[] // top-level names only pickFields(schema, keys: string[]): FieldDefinition[] // preserves original order, not `keys`' order extendField(schema, fieldName: string, overrides: Partial): FieldDefinition[] // non-mutating discriminatedFields(discriminatorName: string, variants: Record): FieldDefinition[] ``` `discriminatedFields` does NOT create the discriminator field itself (a `select`/`radio` you define separately, options matching the variant keys) — it takes every variant's fields and ANDs a "does `values[discriminatorName]` match this variant" check into each field's existing `visible` (if any), via the same expression evaluation semantics as the real `ConditionEvaluator`. --- ## 7. Validation ### 7.1 Built-in validators (all exported from the root package) ```ts required: ValidatorFn // null/undefined/'' /empty array → error; `false`/`0` are NOT treated as empty minLength(min: number, message?: string): ValidatorFn // strings and arrays; no-ops for other types maxLength(max: number, message?: string): ValidatorFn min(minVal: number, message?: string): ValidatorFn // Number(value); no-ops if NaN max(maxVal: number, message?: string): ValidatorFn pattern(regex: RegExp, message?: string): ValidatorFn // strings only email: ValidatorFn // no-ops on falsy value; simple regex, not RFC-complete url: ValidatorFn // no-ops on falsy value; tries `new URL(String(value))` sameAs(otherFieldName: string, message?: string): ValidatorFn // compares by === against another field's CURRENT value fileType(types: string[], message?: string): ValidatorFn // MIME prefix OR filename-extension match, any accepted file must match one fileSize(maxBytes: number, message?: string): ValidatorFn fileCount(maxCount: number, message?: string): ValidatorFn ``` `required` treating `false` and `0` as valid, non-empty values matters for a required checkbox meant to force "must be checked" — `required` alone does NOT enforce that (an unchecked `false` checkbox passes `required`); use a custom validator (`(v) => v === true ? null : '...'`) for a must-be-true checkbox instead. ### 7.2 `validateMode` `'first'` (default): stops at the first failing validator per field — checks `required` first, then `validators` in array order, returning as soon as one fails. `'all'`: collects every failing validator's message for that field instead of stopping at the first. ### 7.3 Async validators ```ts asyncValidators?: AsyncValidatorFn[] // on FieldDefinition ``` During normal typing/blurring, async validators run **debounced 300ms** (fixed, not configurable) after the field's value/touch last changed — a rapid burst of edits only triggers one actual run, 300ms after the LAST edit. At `submit()` time (4.5), every field's async validators run immediately with no debounce, and `submit()` awaits all of them before deciding pass/fail — this is the only path where a pending async validator is guaranteed to have resolved before a decision is made. --- ## 8. Custom field components ### 8.1 The component contract A custom field component receives `FormFieldProps` (section 2) as props and must emit `'update:modelValue'` (with the new value) and `'blur'` (with no payload) — `FormRenderer` (section 9) wires both automatically for any component it resolves. ```ts function useFormField(props: FormFieldProps): { hasError: ComputedRef // props.touched && props.error.length > 0 errorMessage: ComputedRef // props.error[0] if hasError, else null allErrors: ComputedRef // props.error if touched, else [] isRequired: ComputedRef isDisabled: ComputedRef // only true for a STATIC `field.disabled === true` — a function/string disabled is NOT resolved here (FormRenderer already receives the pre-resolved field from `form.fields`, where disabled IS a plain boolean by the time it reaches a component — see note below) } ``` Note on `isDisabled`: by the time a field reaches a rendered component via `form.fields.value` (not the raw schema), `ConditionEvaluator` has already resolved `disabled` down to a plain `boolean` — so in practice `props.field.disabled` IS always a boolean at that point despite the type allowing a function; `useFormField`'s narrow `typeof === 'boolean'` check is a defensive fallback, not something that normally triggers a `false` incorrectly for a function-typed `disabled`. ### 8.2 Component registry (`createFormRegistry` / `useRegistry` / `provideRegistry`) ```ts type ComponentMap = Partial> function createFormRegistry(components: ComponentMap): Plugin // app.use(createFormRegistry({ text: MyTextInput })) function useRegistry(): ComponentMap // called inside FormRenderer itself function provideRegistry(components: ComponentMap): void // provide()-based LOCAL override, deeper in the tree ``` Component resolution order for a given field, highest priority first (from `FormRenderer`'s `resolveComponent`): 1. `field.component` (per-field, in the schema itself) 2. `FormRenderer`'s own `components` prop (per-renderer-instance) 3. the app/local registry (`createFormRegistry` / `provideRegistry`) 4. the built-in default for that `FieldType` (only `text`, `email`, `number`, `textarea`, `select`, `checkbox`, `radio`, `date`, `file` have a built-in default — `group` and `array` are handled by dedicated template branches, not this resolution chain at all, so registering a component for `'group'`/`'array'` in any of the above has no effect) --- ## 9. `FormRenderer` / `MultiStepFormRenderer` (`/ui` and theme subpaths) ```ts interface FormRendererProps { form: UseFormReturn components?: Partial> submitLabel?: string // default 'Submit' fields?: FieldDefinition[] // INTERNAL — used for group recursion; don't pass this yourself } // Emits: submit (after form.submit() resolves) ``` Renders only fields where `visible !== false` (from `form.fields`, the RESOLVED list — see 4). Per-field slot overrides, by field name: `#field-{name}` (replaces the entire field, gets `{ field, value, error, touched, setValue, touch }`), `#label-{name}`, `#error-{name}`. A `#submit` slot (gets `{ isSubmitting, isValid }`) replaces the default submit `