Usage
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:
<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:
<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 accessibilitysubmit- Submit handler that validates before calling youronSubmit- ...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:
<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:
<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 focusonSubmit- When the form is submittedonInput- When the user typesonChange- When the user changes the value of a fieldonMount- 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
<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
</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:
<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:
- Array Fields - Handle dynamic lists and nested forms
- Nuxt Module - Setup NotForm in your Nuxt project
- API Reference - Complete API documentation