GitHub

Quickstart

Build a validated form from scratch.

This page walks through building a complete form from scratch. It covers the four components you will use in almost every form.

How it works

Create a form instance

useNotForm takes a schema and initial values. It returns the instance you work with throughout the form.

<script setup lang="ts">
import { useNotForm } from 'notform'
import { z } from 'zod'

const form = useNotForm({
  schema: z.object({
    email: z.string().email('Enter a valid email'),
    password: z.string('Invalid input').min(8, 'At least 8 characters'),
  }),
  initialValues: { email: '', password: '' },
  onSubmit: async (values) => {
    // Called only when schema passes. values is typed.
    await login(values)
  },
})

onSubmit only runs when validation passes. If validation fails on submit, it is never called.

Wrap with <NotForm>

<NotForm> renders a <form> element and makes the instance available to all descendants through Vue's provide/inject. Pass the instance once here — you do not need to pass it to every field.

<template>
  <NotForm :form="form" @submit="form.submit">
    <NotField
      v-slot="{ events,path }"
      path="email"
    >
      <!-- Fields -->
    </NotField>
  </NotForm>
</template>

Bind @submit to form.submit. It calls event.preventDefault() when validation fails or when onSubmit is defined, so you never need to wire that up yourself.

Add fields with <NotField>

<NotField> is renderless. It tracks state for one field and hands it to the slot. Spread events onto any native input.

<template>
  <NotField path="email" v-slot="{ events }">
    <input v-model="form.values.email" type="email" v-bind="events" />
  </NotField>
</template>
  • path — dot-separated path to the value in form.values
  • v-model binds directly to form.values — no unwrapping
  • v-bind="events" attaches onBlur, onInput, onChange, onFocus

Show errors with <NotMessage>

<NotMessage> renders the first error for a path. It renders nothing when there is no error.

<template>
  <NotField path="email" v-slot="{ events }">
    <input v-model="form.values.email" v-bind="events" />
    <NotMessage path="email" />
  </NotField>
</template>

It renders a <span> by default. Use as to change the tag, and pass any class or attribute as normal.

<NotMessage path="email" as="p" />

Next steps