useRegister
The bridge between
v-registeron a wrapper component andv-registeron its inner native input.
- Category
- Composable
- Signature
- { "useRegister<V>()": "UseRegisterReturn<V> | undefined", "kind": "code" }
- 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.
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.
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), reach for injectForm<Form>() and call ctx.register(path) directly. 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.