Nuxt Precognition is a "precognition" utility for sharing server-side validation with your front-end. This is inspired by Laravel Precognition and helps you provide a good user validation experience, while also helping to make the Nuxt form submission and validation process as smooth as possible.
- Convenient form submission and validation with
useForm
andusePrecognitionForm
composables - Server-side validation of form data before the actual event handler runs with
definePrecognitionEventHandler
Nuxt server utility/middleware
Install the module to your Nuxt application with one command:
npx nuxi module add @gearbox-solutions/nuxt-precognition
That's it! You can now use Nuxt Precognition in your Nuxt app β¨
π Try it on Stackblitz! π
An example implementation can be found in the /playground
directory of this project. You can run it locally by checking out this repo, installing dependencies with pnpm install
, and then running the playground with pnpm run dev
. This should launch a local server for you to see the form submissions both with validation-only using the useForm
composable and precognition validation with usePrecognitionForm
.
Alternatively, use the Stackblitz link above to run it in your browser.
The usePrecogitionForm
composable is the core feature for setting up the precognition workflow. This composable extends the useForm
composable (described below)and provides a more robust form submission and validation workflow when used in conjunction with the handlePrecognitionRequest
server middleware.
The useProecognitionForm
composable is automatically imported into your Nuxt app when you install the module, and can be used without needing to manually import it.
This composable is used to create your form object and will manage form submission and validation. It is stateful, and will provide you with the form states and errors for you to nicely display validation and submission state in your components.
Here is an example of using the usePrecognitionForm
composable to create a form object, validate the data on change, and handle form submission and responses:
<script setup lang="ts">
const { data: entries, refresh } = await useFetch("/api/entries");
const form = usePrecognitionForm("post", "/api/entries", {
name: "",
});
const submitForm = async () => {
await form.submit({
onSuccess: () => {
refresh();
},
});
};
</script>
<template>
<div>
<div class="flex gap-x-8">
<form class="space-y-4" @submit.prevent="submitForm">
<div class="">
<label for="description" class="block text-sm uppercase"> Name </label>
<input id="description" v-model="form.name" type="text" name="description" @change="form.validate('name')" />
<div v-for="error in form.errors.name" :key="error" class="text-red-500">
{{ error }}
</div>
</div>
<div>
<button :disabled="form.processing">Submit</button>
</div>
</form>
<pre>{{ form }}</pre>
</div>
<div class="pt-12">
<div class="text-lg font-bold uppercase">Entries</div>
<div v-for="(entry, index) in entries" :key="index">
{{ entry.name }}
</div>
</div>
</div>
</template>
The useForm
Vue composable provides a convenient way to handle form submission and validation errors in your Nuxt app for cases when you may not want to use the full precognition features, but still keep the same convenient form-submission process. A form created through the useForm
composable provide your form submissions with a number of useful features.
This form is based on the useForm
composable from Inertia.js and generally implements the same API and can be used in the same way. The documentation for the useForm
composable should be consulted for details on how to use the form.
Here's a quick summary for people who don't want to leave this page:
- Lifecycle Hooks
onBefore()
onStart({ request, options })
onSuccess({ request, options, response })
- Runs after the request. Called only if the request is successful.onError({ request, options, response, errors })
- Runs after the request. Called only if the request fails.onFinish({ request, options, response })
- Always runs after the request, afteronSuccess
oronError
- Form State
isDirty
- Boolean - Indicates if the form values have been changed since it was instanciated.hasErrors
- Boolean - Indicates if there are validation errorsprocessing
- Boolean - Indicates if the form has been submitted, but a response has not yet been receivedwasSuccessful
- Boolean - Indicates if the last form submission was successfulrecentlySuccessful
- Boolean - Temporarily indicatestrue
if the last form submission was successful, then changes to false. This is useful for temporarily showing a "Success!" type of message to users.
- Functions
reset()
- Reset the form to the state it was in when it was createdsetError(field, value)
- Manually set an error state.clearErrors()
- Clear the error state.submit(method, url, options?)
- Submit the form using the specified method. You can also use the helper functions instead of this.get(url: string, options?)
- Convenience function forsubmit()
withGET
post(url: string, options?)
- Convenience function forsubmit()
withPOST
put(url: string, options?)
- Convenience function forsubmit()
withPUT
patch(url: string, options?)
- Convenience function forsubmit()
withPATCH
delete(url: string, options?)
- Convenience function forsubmit()
withDELETE
This composable is automatically imported into your Nuxt app when you install the module, and can be used without needing to manually import it.
The useForm composable can be used without the precognition middleware, and is just a convenient way to handle form submission and validation errors in your Nuxt app in general. You can use this with the getValidatedInput()
utility to validate and return validation errors if you don't want to do the precognition validation.
Here is an example of using the form to submit a Todo item to an API endpoint:
<script setup lang="ts">
const { data: entries, refresh } = await useFetch("/api/entries");
const form = useForm({
description: "",
});
const submitForm = async () => {
await form.post("/api/entries", {
onSuccess: (response) => {
refresh();
},
});
};
</script>
<template>
<div>
<div class="flex gap-x-8">
<form class="space-y-4" @submit.prevent="submitForm">
<div class="space-x-4">
<label for="description">Name</label>
<input id="description" v-model="form.name" type="text" name="description" :errors="form.errors.name" />
</div>
<div>
<button :disabled="form.processing">Submit</button>
</div>
</form>
<pre>{{ form }}</pre>
</div>
<div class="pt-12">
<div class="text-lg font-bold uppercase">Entries</div>
<div v-for="(entry, index) in entries" :key="index">
{{ entry.description }}
</div>
</div>
</div>
</template>
The handlePrecognitionRequest
server middleware is a server-side middleware that can be used to handle form submissions and validation errors in your Nuxt app. It is designed to work with the usePrecognitionForm
composable, and will validate the data submitted by that form, and will return validation results before it reaches the main part of your handler.
Note
Your main handler code will not run on a precognition validation request, even though it will be posting to the same endpoint. How convenient!
Validation is configured using a Zod Object which should be designed to handle the fields in your form subimssion.
Your Nuxt server routes should return a default definePrecognitionEventHandler
instead of the usual defineEventHandler
. This new handler takes a Zod object as its first parameter, and then the second parameter is your regular event handler function definition.
This new handler type is automatically imported by Nuxt when the module is installed, and does not need to be manually imported.
definePrecognitionEventHandler(zodSchemaObject, handler);
- zodSchemaObject - The Zod validation Schema Object which will be used for validation of form data on Precognition requests, without executing the handler.
- handler - A regular Nuxt event handler callback function to run, which would be your regular event handler for this server endpoint.
Example API endpoint handler:
import { z } from "zod";
// define the Zod object for validation
const todoRequestSchema = z.object({
description: z.string().trim().min(1, "Description is required"),
});
// use the Precognition handler with the Zod schema
// definePrecognitionEventHandler is used instead of defineEventHandler
export default definePrecognitionEventHandler(todoRequestSchema, async (event) => {
// This handler callback doesn't execute on precognition validation requests!
// perform validation on the input for when the form is directly submitted
const validated = await getValidatedInput(event, todoRequestSchema);
// continue and do something with the body
// ...
// ...
});
The getValidatedInput
utility function provides a simple, one-liner function call for validating your input. This function is a wrapper around H3's readValidatedBody
to make it easier to reuse your same Zod validation object as in your precognition request handling.
This utility function is automatically imported by Nuxt.
getValidatedInput(event, validationSchema)
- event - the H3 event being processed by your event handler
- validationSchema - The Zod validation schema, which you would probably want to be the same schema as used by your
definePrecognitionEventHandler
a promise which will resolve to an object of your validated form data. You can await
this function call to get the form data directly.
A Nuxt Error Object with validation error information. You can allow this error to be thrown, which will then be received by the usePrecogntionForm
or useForm
composables to update your validation error state. Alternatively, you may decide to catch the error and handle it.
export default defineEventHandler(async (event) => {
// get the validated form data or throw an exception back to our form
const validated = await getValidatedInput(event, todoRequestSchema);
// continue and do something with the form data
// ...
// ...
});
Local development
# Install dependencies
pnpm install
# Generate type stubs
pnpm run dev:prepare
# Develop with the playground
pnpm run dev
# Build the playground
pnpm run dev:build
# Run ESLint
pnpm run lint
# Run Prettier
pnpm run prettier
# Run Vitest
pnpm run test
pnpm run test:watch
# Release new version
pnpm run release