Form Actions
Use form actions to submit data to the server.
Form Actions are an alternative to api routes, and useFetch
/useAsyncData
composables.
They allow you to submit data to your server using native HTML forms that can be progressively enhanced.
Basic Usage
Let's start by creating a Nuxt page in pages/login.vue
:
<template>
<form method="POST">
<label>
Email
<input name="email" type="email" autocomplete="username">
</label>
<label>
Password
<input name="password" type="password" autocomplete="current-password">
</label>
<button>Log in</button>
</form>
</template>
If you navigate to /login
and you click the button on a form like this, the browser will send a POST
request to the current path, which is /login
.
Note: The path matched by your page is very important, as it respects the default behaviour of the browser.
In order to handle such a request with Nuxt, you need to create a route handler. With this module, you can create a form action to handle this using the /server/actions
directory. Let's create an action in /server/actions/login.ts
:
export default defineFormActions({
default: () => {
console.log("Login called !")
}
})
Note: It's important that the file name matches the path of the page, and that you use a default export with the defineFormActions
composable.
defineFormActions
accepts an object of h3 event handlers. The key that you use only matters if you want to handle more than 1 action on the same route. By convention we use default
for the main action.You have now a working form action, but it doesn't do much. Let's add some logic to it.
Handling form data
Continuing with the previous example, let's add some dummy logic to our form action :
// Replace with real logic
const createSession = (user: unknown) => "session-id"
// Replace with real logic
const getUser = (email: string, password: string) => ({ name: "Luke" })
// Replace with real validation
const validValue = (v: unknown): v is string => typeof v === "string" && v.length > 0
export default defineFormActions({
signIn: async (event) => {
// h3 exports a readFormData to obtain a FormData object
const formData = await readFormData(event)
const email = formData.get("email")
const password = formData.get("password")
// Handle your errors
if (!validValue(email)) {
return actionResponse(event, { email, invalid: true },
{ error: { message: "Invalid email" } })
}
if (!validValue(password)) {
return actionResponse(event, { email, invalid: true },
{ error: { message: "Invalid password" } })
}
// Load the user
const user = getUser(email, password)
if (!user) {
return actionResponse(event, { email, incorrect: true },
{ error: { message: "Invalid login" } })
}
// Attach a session cookie to the response
setCookie(event, "session", createSession(user))
// Respond with the user
return actionResponse(event, { user })
}
})
Now on succesful submissions, our server route will respond with a JSON payload containing the user data, and a session cookie.
defineFormActions
and actionResponse
are auto-imported, but you can explicitly import them from #form-actions
.Progressively enhancing the form
Now that we have a working form action, we can progressively enhance the form to use it.
The useFormAction
composable expose multiple helpers to help you with this.
enhance
must be bound to the form element with the customv-enhance
directive.data
is a reactive object that will contain the response from the form action.
We can now use vue to display the response from the form action and to handle the error states. We can also bind the value
of the inputs to the response from the form action, so that the form is pre-filled with the values that were submitted in case of error.
<script setup lang="ts">
const { enhance, data } = await useFormAction()
</script>
<template>
<form v-enhance="enhance" method="POST" action="login">
<p v-if="data.formResponse?.invalid" class="error">
Invalid credentials.
</p>
<p v-if="data.formResponse?.incorrect" class="error">
Invalid login.
</p>
<p v-if="data.formResponse?.user" class="success">
{{ data.formResponse.user.name }} Found !
</p>
<label>
Email
<input name="email" type="email" :value="data.formResponse?.email ?? ''">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>
</template>
Redirecting
It's common to redirect the user after a successful form submission. Let's first create a profile page to redirect our users to :
<template>
<h1>Profile</h1>
</template>
Now let's update our form action to redirect to this page. You can do this by using the 3rd argument of actionResponse
:
export default defineFormActions({
signIn: (event) => {
// ...
return actionResponse(event, { user }, { redirect: "/profile" })
}
})
By default Nuxt will use server side navigation and hard navigate to /profile
. However, if your form is progressively enhanced, Nuxt will use client side navigation instead.
Multiple actions
It's possible that you want to handle several actions in the same form actions.
defineFormActions
let you define multiple actions, and you can use the a query parameter to specify which action to call.
Let's add some actions to our profile page :
<template>
<h1>Profile</h1>
<form method="POST">
<button>Log out</button>
<button formaction="profile?delete">
Delete account
</button>
</form>
</template>
default
action will be matched first. If no action is found, the first action will be called.We need 2 event handlers to handle these actions. Let's create a route handler for these 2 actions :
export default defineFormActions({
logout: () => {
console.log("logout ...")
},
delete: () => {
console.log("delete ...")
}
})
Now when we click on the buttons a POST request will be sent to /profile
or /profile?delete
. The route handler will execute the correct matching handler.
Example
Refer to the simple template to see a full setup.