Input components

To make your code more readable, we recommend that you develop your own input components if you are not using a prebuilt UI library. There you can encapsulate logic to display error messages, for example.

If you're already a bit more experienced, you can use the input components we developed for our playground as a starting point. You can find the code in our GitHub repository here.

Why input components?

Currently, your fields might look something like this:

<template>
  <Field :of="loginForm" :path="['email']" v-slot="field">
    <div>
      <label :for="field.props.name">Email</label>
      <input
        v-bind="field.props"
        :id="field.props.name"
        v-model="field.input"
        type="email"
        required
      />
      <div v-if="field.errors">{{ field.errors[0] }}</div>
    </div>
  </Field>
</template>

If CSS and a few more functionalities are added here, the code quickly becomes confusing. In addition, you have to rewrite the same code for almost every form field.

Our goal is to develop a TextInput component so that the code ends up looking like this:

<template>
  <Field :of="loginForm" :path="['email']" v-slot="field">
    <TextInput
      v-model="field.input"
      :props="field.props"
      type="email"
      label="Email"
      :errors="field.errors"
      required
    />
  </Field>
</template>

Create an input component

In the first step, you create a new file for the TextInput component and, if you use TypeScript, define its properties. The props object from your field contains all the necessary event handlers and attributes.

<script setup lang="ts">
import type { FieldElementProps } from '@formisch/vue';

interface TextInputProps {
  type: 'text' | 'email' | 'tel' | 'password' | 'url' | 'number' | 'date';
  label?: string;
  placeholder?: string;
  required?: boolean;
  errors: [string, ...string[]] | null;
  props: FieldElementProps;
}

const props = defineProps<TextInputProps>();
const model = defineModel<string | number | undefined>({ required: true });
</script>

Template code

After that, you can add the template code.

<template>
  <div>
    <label v-if="label" :for="props.props.name">
      {{ label }} <span v-if="required">*</span>
    </label>
    <input
      v-model="model"
      v-bind="props.props"
      :id="props.props.name"
      :type="type"
      :placeholder="placeholder"
      :required="required"
      :aria-invalid="!!errors"
      :aria-errormessage="`${props.props.name}-error`"
    />
    <div v-if="errors" :id="`${props.props.name}-error`">{{ errors[0] }}</div>
  </div>
</template>

Next steps

You can now build on this code and add CSS, for example. You can also follow the procedure to create other components such as Checkbox, Slider, Select and FileInput.

Final code

Below is an overview of the entire code of the TextInput component.

<script setup lang="ts">
import type { FieldElementProps } from '@formisch/vue';

interface TextInputProps {
  type: 'text' | 'email' | 'tel' | 'password' | 'url' | 'number' | 'date';
  label?: string;
  placeholder?: string;
  required?: boolean;
  errors: [string, ...string[]] | null;
  props: FieldElementProps;
}

const props = defineProps<TextInputProps>();
const model = defineModel<string | number | undefined>({ required: true });
</script>

<template>
  <div>
    <label v-if="label" :for="props.props.name">
      {{ label }} <span v-if="required">*</span>
    </label>
    <input
      v-model="model"
      v-bind="props.props"
      :id="props.props.name"
      :type="type"
      :placeholder="placeholder"
      :required="required"
      :aria-invalid="!!errors"
      :aria-errormessage="`${props.props.name}-error`"
    />
    <div v-if="errors" :id="`${props.props.name}-error`">{{ errors[0] }}</div>
  </div>
</template>

Using component libraries

Component libraries render their own components instead of plain elements, so you cannot use v-bind="field.props" on them. Vue treats the ref entry inside that object as a real template ref, which would register the component instance rather than a DOM element. Bind the entries individually and set the value through v-model:

<template>
  <Field :of="form" :path="['date']" v-slot="field">
    <DatePicker
      :ref="(instance) => field.props.ref(instance?.$el ?? instance)"
      :name="field.props.name"
      :autofocus="field.props.autofocus"
      v-model="field.input"
      @focus="field.props.onFocus"
      @blur="field.props.onBlur"
    />
  </Field>
</template>

Assigning field.input updates the field value and triggers validation, while the focus handler is what marks the field as touched. Unwrapping $el only helps when the component's root is the focusable element; if it wraps its control in a container, reach for the library's own input ref instead, or accept that focus cannot target that field. For more details, see the controlled fields guide.

For ready-made wiring recipes, check out our integration guides:

Next steps

Now that you know how to create reusable input components, continue to the handle submission guide to learn how to process form data when the user submits the form.

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