NOTFORM
Getting Started

Usage

Learn the fundamentals of NotForm and create your first validated form.

notform makes form validation in Vue simple and intuitive. This guide will walk you through the basics of creating validated forms with notform .

Your First Form

Let's start with a simple login form to understand the core concepts:

form.vue
<script setup lang="ts">
import { useNotForm, NotForm, NotField, NotMessage } from 'notform'
import { z } from 'zod'

const { state, id, submit } = useNotForm({
  schema: z.object({
    email: z.string().email('Please enter a valid email'),
    password: z.string().min(8, 'Password must be at least 8 characters')
  }),
  onSubmit: async (data) => {
    console.log('Form submitted:', data)
    // Handle your form submission here
  }
})
</script>

<template>
  <NotForm :id="id" @submit="submit">
    <NotField name="email" v-slot="{ methods, name }">
      <label :for="name">
        Email
        <input
          v-model="state.email"
          type="email"
          :id="name"
          :name="name"
          v-bind="methods"
        />
        <NotMessage :name="name" />
      </label>
    </NotField>

    <NotField name="password" v-slot="{ methods, name }">
      <label :for="name">
        Password
        <input
          v-model="state.password"
          type="password"
          :id="name"
          :name="name"
          v-bind="methods"
        />
        <NotMessage :name="name" />
      </label>
    </NotField>

    <button type="submit">Login</button>
  </NotForm>
</template>

That's it! You now have a fully validated form with error handling. Let's break down what's happening.

Understanding the Components

useNotForm Composable

The useNotForm composable is the core of NotForm. It sets up your form state, validation, and submission handling:

form.vue
<script lang="ts" setup>
const { state, id, submit } = useNotForm({
  schema: z.object({
    email: z.string().email(),
    password: z.string().min(8)
  }),
  onSubmit: async (data) => {
    // This only runs if validation passes
    console.log('Validated data:', data)
  }
})
<script>

What you get back:

  • state - Reactive form state (typed based on your schema)
  • id - Unique form ID for context binding and accessibility
  • submit - Submit handler that validates before calling your onSubmit
  • ...and many more

NotForm Component

The NotForm component wraps your form fields and handles submission. It also provides the form context to nested components.

<template>
<NotForm :id="id" @submit="submit">
  <!-- Your fields go here -->
</NotForm>
</template>

NotField Component

NotField is a renderless component that provides field-specific methods and state:

form.vue
<template>
<NotField name="email" v-slot="{ methods, name }">
  <input
    v-model="state.email"
    :name="name"
    v-bind="methods"
  />
  <NotMessage :name="name" />
</NotField>
</template>

Slot props:

  • methods - Event handlers for validation (onBlur, onChange, etc.)
  • name - The field name (useful for labels and accessibility)
  • ...see NotField API

NotMessage Component

NotMessage automatically displays validation errors for a field:

form.vue
<template>
<NotMessage :name="name" />
</template>

It shows error messages when validation fails and hides them when the field is valid.

Validation Timing

By default, NotForm validates fields:

  • onBlur - When a field loses focus
  • onSubmit - When the form is submitted
  • onInput - When the user types
  • onChange - When the user changes the value of a field
  • onMount - When the form is mounted

You can customize this behavior via the validateOn option in useNotForm.

Working with Different Input Types

notform works with any HTML input element:

Checkboxes

checkbox.vue
<template>
<NotField name="agree" v-slot="{ methods, name }">
  <label>
    <input
      v-model="state.agree"
      type="checkbox"
      :name="name"
      v-bind="methods"
    />
    I agree to the terms
  </label>
  <NotMessage :name="name" />
</NotField>
</template>

Select Dropdowns

dropdown.vue
</template>
 <NotField name="country" v-slot="{ methods, name }">
  <select
    v-model="state.country"
    :name="name"
    v-bind="methods"
  >
    <option value="">Select a country</option>
    <option value="us">United States</option>
    <option value="uk">United Kingdom</option>
    <option value="ca">Canada</option>
  </select>
  <NotMessage :name="name" />
 </NotField>
</template>

Complete Example

Here's a more comprehensive registration form:

RegistrationForm.vue
<script setup lang="ts">
import { useNotForm, NotForm, NotField, NotMessage } from 'notform'
import { z } from 'zod'

const { state, id, submit } = useNotForm({
  schema: z.object({
    username: z.string().min(3, 'Username must be at least 3 characters'),
    email: z.string().email('Invalid email address'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
    confirmPassword: z.string(),
    country: z.string().min(1, 'Please select a country'),
    agree: z.literal(true, 'You must agree to continue')
  }).refine(data => data.password === data.confirmPassword, {
    message: "Passwords don't match",
    path: ['confirmPassword']
  }),
  onSubmit: async (data) => {
    console.log('Registration data:', data)
    // Handle registration logic
  }
})
</script>

<template>
  <NotForm :id="id" @submit="submit">
    <NotField name="username" v-slot="{ methods, name }">
      <label :for="name">
        Username
        <input
          v-model="state.username"
          type="text"
          :id="name"
          :name="name"
          v-bind="methods"
        />
        <NotMessage :name="name" />
      </label>
    </NotField>

    <NotField name="email" v-slot="{ methods, name }">
      <label :for="name">
        Email
        <input
          v-model="state.email"
          type="email"
          :id="name"
          :name="name"
          v-bind="methods"
        />
        <NotMessage :name="name" />
      </label>
    </NotField>

    <NotField name="password" v-slot="{ methods, name }">
      <label :for="name">
        Password
        <input
          v-model="state.password"
          type="password"
          :id="name"
          :name="name"
          v-bind="methods"
        />
        <NotMessage :name="name" />
      </label>
    </NotField>

    <NotField name="confirmPassword" v-slot="{ methods, name }">
      <label :for="name">
        Confirm Password
        <input
          v-model="state.confirmPassword"
          type="password"
          :id="name"
          :name="name"
          v-bind="methods"
        />
        <NotMessage :name="name" />
      </label>
    </NotField>

    <NotField name="country" v-slot="{ methods, name }">
      <label :for="name">
        Country
        <select
          v-model="state.country"
          :id="name"
          :name="name"
          v-bind="methods"
        >
          <option value="">Select a country</option>
          <option value="us">United States</option>
          <option value="uk">United Kingdom</option>
          <option value="ca">Canada</option>
        </select>
        <NotMessage :name="name" />
      </label>
    </NotField>

    <NotField name="agree" v-slot="{ methods, name }">
      <label>
        <input
          v-model="state.agree"
          type="checkbox"
          :name="name"
          v-bind="methods"
        />
        I agree to the terms and conditions
      </label>
      <NotMessage :name="name" />
    </NotField>

    <button type="submit">Register</button>
  </NotForm>
</template>

Next Steps

Now that you understand the basics, explore more advanced features: