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.

Contributors

Thanks to all the contributors who helped make this page better!

  • GitHub profile picture of @fabian-hiller

Partners

Thanks to our partners who support the project ideally and financially.

Sponsors

Thanks to our GitHub sponsors who support the project financially.

  • GitHub profile picture of @vasilii-kovalev
  • GitHub profile picture of @UpwayShop
  • GitHub profile picture of @ruiaraujo012
  • GitHub profile picture of @hyunbinseo
  • GitHub profile picture of @nickytonline
  • GitHub profile picture of @kibertoad
  • GitHub profile picture of @caegdeveloper
  • GitHub profile picture of @Thanaen
  • GitHub profile picture of @bmoyroud
  • GitHub profile picture of @ysknsid25
  • GitHub profile picture of @dslatkin