Controlled fields
By default, all form fields are uncontrolled because that's the default behavior of the browser. For a simple login or contact form this is quite sufficient.
Why controlled?
As soon as your forms become more complex, for example you set initial values or change the values of a form field via setInput, it becomes necessary that you control your fields yourself. For example, depending on which HTML form field you use, you may need to set the value, checked or selected attributes.
Simple text inputs
For a text input you simply add the v-model directive:
<template>
<Field :of="loginForm" :path="['firstName']" v-slot="field">
<input v-model="field.input" v-bind="field.props" type="text" />
</Field>
</template>Numbers and dates
In Vue you don't wire up an input handler — v-model reads and writes the value through the field.input setter. That makes numbers and dates mostly automatic.
Number inputs
For <input type="number" />, the .number modifier converts the value to a real number that matches a v.number() schema. Vue applies it automatically for number inputs, but you can also add it explicitly:
<template>
<Field :of="form" :path="['age']" v-slot="field">
<input v-model.number="field.input" v-bind="field.props" type="number" />
</Field>
</template>Date inputs
An <input type="date" /> exposes its value as a yyyy-mm-dd string, which v-model stores as-is. This pairs directly with a v.string() schema:
<template>
<Field :of="form" :path="['birthday']" v-slot="field">
<input v-model="field.input" v-bind="field.props" type="date" />
</Field>
</template>If your schema expects a real Date (v.date()), Vue has no modifier for this, so convert in both directions yourself:
<script setup lang="ts">
function parseDate(event: Event) {
return (event.target as HTMLInputElement).valueAsDate ?? undefined;
}
</script>
<template>
<Field :of="form" :path="['birthday']" v-slot="field">
<input
v-bind="field.props"
type="date"
:value="field.input?.toISOString().split('T', 1)[0] ?? ''"
@input="field.input = parseDate($event)"
/>
</Field>
</template>Checkboxes
For checkboxes, you need to bind to the checked attribute and handle both boolean and array values:
Single checkbox (boolean):
<template>
<Field :of="form" :path="['acceptTerms']" v-slot="field">
<input type="checkbox" v-bind="field.props" v-model="field.input" />
</Field>
</template>Multiple checkboxes (array of strings):
<template>
<Field :of="form" :path="['interests']" v-slot="field">
<label v-for="option in options" :key="option.value">
<input
type="checkbox"
:value="option.value"
v-bind="field.props"
v-model="field.input"
/>
{{ option.label }}
</label>
</Field>
</template>Select elements
For select elements, you need to bind to the selected attribute:
Single select:
<template>
<Field :of="form" :path="['country']" v-slot="field">
<select v-bind="field.props" v-model="field.input">
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</Field>
</template>Multiple select:
<template>
<Field :of="form" :path="['languages']" v-slot="field">
<select multiple v-bind="field.props" v-model="field.input">
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</Field>
</template>File inputs
The HTML <input type="file" /> element is an exception because it cannot be controlled in the traditional way. However, you can control the UI around it. For inspiration, check out our FileInput component from the playground.
Next steps
Now that you understand controlled fields, you can explore more advanced topics like nested fields and field arrays to handle complex form structures.