useRegister
The bridge between
v-registeron a wrapper component andv-registeron its inner native input.
- Category
- Composable
- Signature
useRegister<V>(options?): UseRegisterReturn<V> | undefined- Returns
- hybrid Ref + RegisterValue Proxy
- Auto-installed
- attaform plugin
Type into either of the two fields rendered by <FieldRow> and watch the JSON readout update. The parent applies v-register="form.register('email')" to the <FieldRow> root; inside FieldRow, useRegister() captures that binding and v-register="rv" reattaches it to the inner <input>. The sections below trace every step.
Each FieldRow only takes a label prop. The schema path arrives ambiently: the parent below applies v-register to the row, and useRegister() inside the row picks it up.
When to reach for it
useRegister exists for one specific shape: a Vue component that wraps a single form field, whose root element is not the input itself:
<!-- A labeled-row wrapper. The root is <label>, not the input. -->
<FieldRow v-register="form.register('email')" label="Email" />
Inside <FieldRow>, the wrapper's root receives the directive, but you need the binding on the inner <input> to make the form work. useRegister captures the parent's v-register binding so you can re-apply it inside.
When the wrapper's root is the input itself, Vue's attribute fallthrough already handles the binding, so useRegister is unnecessary.
Capture, re-apply
<script setup lang="ts">
import { useRegister } from 'attaform'
const rv = useRegister()
</script>
<template>
<label class="field-row">
<span>{{ label }}</span>
<input v-register="rv" />
</label>
</template>
That's the whole pattern: call once, hand the return value to v-register inside. useRegister reads the parent's binding, surfaces it as a hybrid Ref + RegisterValue Proxy, and the directive picks it up exactly as if the parent had applied it directly to the inner input.
Modifiers come along
Modifiers the parent writes on the wrapper reach the inner control:
<FieldRow v-register.trim="form.register('email')" label="Email" />
The <input v-register="rv"> inside FieldRow now trims exactly as if .trim were written on it. Modifiers on the inner binding still apply, and the two sets combine: .trim on the parent with v-register.lazy="rv" inside gives the input .lazy.trim. When the inner control is itself a component, the parent's modifiers reach it the same way, timed to focus leaving it.
Two surfaces on the return value
useRegister() returns a hybrid Proxy:
- Ref-shaped:
v-register="rv"works because the Proxy answers__v_isRef/.valuelike aRef<RegisterValue | undefined>. Vue's template auto-unwrap surfaces the underlying RegisterValue to the directive. - Pierced reads:
rv.path,rv.segments,rv.formKey,rv.formInstanceIdall work directly in script setup without.value. Reads inside reactive scopes (computed,watchEffect) track changes.
const rv = useRegister()
// In script setup: direct pierce
const ariaLabel = computed(() => `Field for ${rv?.path}`)
// In template: Vue auto-unwraps
// v-register="rv"
Unbound state
When the parent didn't apply v-register, every pierced read returns undefined and the composable's return type surfaces this honestly as UseRegisterReturn<V> | undefined:
const rv = useRegister()
// rv?.formKey: optional-chain to defend
In dev mode, a single console.warn fires per instance at mount time when the parent never bound. The directive accepts undefined peacefully (its binding type is RegisterValue<V> | undefined), so v-register="rv" works whether or not a parent bound. No runtime crash, just the warn.
A wrapper meant to work both ways says so, and the warn stands down:
const rv = useRegister({ optional: true })
Leave it off for a wrapper that always expects a binding. There the warn earns its keep: a parent that forgot v-register renders a field that looks fine and stores nothing, and only the author knows which of the two a given component is.
rv?.isBound is the test for "did a parent bind me", and it is the one to reach for. Script code holds the hybrid Proxy, which is an object and so always truthy; a template holds the unwrapped ref, which is undefined when unbound. Those two disagree, and isBound reads the same on both:
const rv = useRegister()
if (rv?.isBound) {
// a parent bound this wrapper to a field
}
It is reactive in both directions, because the answer can change after mount. A parent is free to bind on a later render, which is also why useRegister cannot simply return undefined when nothing has bound yet: at setup time it does not know, and giving up the Proxy would sever the binding that arrives later.
<AddressField v-register="ready ? form.register('street') : undefined" />
One component, bound and unbound
A wrapper that calls useRegister({ optional: true }) still works when nobody binds it, and it does not need a second copy of its markup to manage that. Write the plain binding you would write without Attaform, and v-register takes over whenever a field resolves:
<select v-register="rv" :value="modelValue" @change="onChange">
<option value="">None</option>
<option v-for="o in options" :key="o.value" :value="o.value">{{ o.label }}</option>
</select>
Bound, the field drives the control and the :value is inert. Unbound, the :value is the whole binding and behaves exactly as it would on a bare <select>. The same holds for :value on a text input or textarea and for :checked on a checkbox or radio.
An <option>'s own :selected works the same way, so a hand-marked option list keeps its marks when nobody binds the <select>.
One edge to know: v-model is not a substitute for :value here. It installs Vue's own model directive beside v-register, so two writers drive one element, and that still warns.
Multi-field wrappers
useRegister is the right call for single-field wrappers. For compound components binding multiple paths (a date-range picker exposing start + end fields, an address subform exposing street + city + zip), save injectForm<Form>() into a form handle and call form?.register(path) for each one. That sidesteps the single-binding assumption useRegister makes.
Where to next
- The
v-registerdirective: the directiveuseRegisterre-binds. - Custom assigners: the escape hatch for non-standard element value surfaces.
injectForm: for compound components binding multiple paths.