Nuxt Precognition

npm version npm downloads License Nuxt

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.

Table of Contents

Features

  • Convenient form submission and validation with useForm and usePrecognitionForm composables
  • Server-side validation of form data before the actual event handler runs with definePrecognitionEventHandler Nuxt server utility/middleware

Quick Setup

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 ✨

Examples

🏀 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.

Usage - Client-Side

The usePrecognitionForm Vue Composable

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

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, after onSuccess or onError
  • Form State
    • isDirty - Boolean - Indicates if the form values have been changed since it was instanciated.
    • hasErrors - Boolean - Indicates if there are validation errors
    • processing - Boolean - Indicates if the form has been submitted, but a response has not yet been received
    • wasSuccessful - Boolean - Indicates if the last form submission was successful
    • recentlySuccessful - Boolean - Temporarily indicates true 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 created
    • setError(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 for submit() with GET
    • post(url: string, options?) - Convenience function for submit() with POST
    • put(url: string, options?) - Convenience function for submit() with PUT
    • patch(url: string, options?) - Convenience function for submit() with PATCH
    • delete(url: string, options?) - Convenience function for submit() with DELETE

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>

Usage - Server-Side

The handlePrecognitionRequest Server Utility / Middleware

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);

Parameters:

  • 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

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)

Parameters:

  • 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

Returns:

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.

Throws:

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.

Example:

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
  // ...
  // ...
});

Contribution

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