-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Describe the feature
Edit : Form actions have been released as a Nuxt module 🎉 Please try it and leave feedback.
- Docs : https://form-actions-nuxt.pages.dev/
- Module : https://nuxt.com/modules/form-actions
- GitHub : https://github.com/Hebilicious/form-actions-nuxt
Progressively enhanced patterns relies on using native forms to make it easier to have apps that works without JavaScript. This can work well with Nuxt Server components too, as it could allow client-server communication with 0client side javascript. While this is not desirable in all scenarios, and having this as the default to do everything is very opinionated, it would be nice to support these patterns at the framework level :
Multiple frameworks already support this pattern (Remix, SvelteKit, SolidStart ...), and Next is adding it :
For Nuxt, it's (almost) possible already to implement this patterns by doing something like this (in /server/api/pespa.ts) :
import { EventHandler, H3Event } from "h3"
type Actions = {
[key: string]: EventHandler
}
const defineFormActions = (actions: Actions) => async (event: H3Event) => {
const action = Object.keys(getQuery(event))[0] ?? "default"
const method = getMethod(event)
if (method === "POST") return defineEventHandler(actions[action])(event)
}
export default defineFormActions({
default: async (event) => {
const form = await readMultipartFormData(event)
const body = await readBody(event)
const contentType = getHeader(event, "content-type")
console.log({ form, body, contentType })
return sendRedirect(event, "/pespa")
},
logout: async (event) => {
const form = await readMultipartFormData(event)
const body = await readBody(event)
const contentType = getHeader(event, "content-type")
console.log({ form, body, contentType })
return sendRedirect(event, "/pespa")
}
})
Then creating a page at /pespa ...
<template>
<form method="POST" action="api/pespa">
<label>
Email
<input name="email" type="email" value="[email protected]" />
</label>
<label>
Password
<input name="password" type="password" value="password" />
</label>
<button>Log in</button>
</form>
<form method="POST" action="api/pespa?logout">
<button>Log Out</button>
</form>
</template>
However here we have to use "/api" for the action attribute, and the DX isn't the best for multiple reasons.
It would be nice to have a /actions
directory (in /server
for nuxt) so that we can define form actions more easily.
(This probably needs a discussion/RFC somewhere so that the Nuxt/Nitro/H3 side can be updated together, let me know if I should repost this somewhere else)
For the nitro syntax, the defineFormAction
I proposed above could be integrated in nitro, as a replacement for defineEventHandler.
I'm not sure if manually redirecting in the action handler is the best way to do things, perhaps what would need to happen for Nuxt specifically is to generate POST only handlers for these form actions, while these GET methods to the same URL are handled by the corresponding route or nuxt page.
If I'm not mistaken, for Nuxt we also need a way to easily pass data back and forth, perhaps the existing ways can be used, but a new composable feels appropriate.
There's also a need for something similar to sveltekit use:enhance / applyAction to make it easier to progressively enhance forms (altough, e.preventDefault() could be enough, the DX is a little barebones)
Having something like getNativeRequest
from H3 would be really useful too:
eventHandler(event => {
const request = getNativeRequest(event)
const data = request.formData()
//do stuff
})
This might need some change in @vue/core, see React here.
Proposed API :
I'm suggesting an API here to illustrate what we could do.
pages/signIn.vue
<script setup>
const { enhance } = useEnhance(({form, data, action, cancel, submitter }) => {
// Using SvelteKit API to illustrate, but the Nuxt one could/should be different ...
// `form` is the `<form>` element;`data` is its `FormData` object
// `action` is the URL to which the form is posted;v`cancel()` will prevent the submission
// `submitter` is the `HTMLElement` that caused the form to be submitted
return async ({ result, update }) => {
// `result` is returned by the matching defineFormAction ....
// `update` is a function which triggers the logic that would be triggered if this callback wasn't set
};
})
// Alternatively something like this, which I personally prefer...
const { result, loading, error, enhance } = useAction("signIn", (actionSettings) => {
// actions settings could contain form, data, action, cancel ... like in useEnhance
})
//Can use result/loading/error etc in the template for conditional rendering
</script>
<template>
<form method="post" action="signIn" v-enhance="enhance">
<label>
Email
<input name="email" type="email" value="[email protected]" />
</label>
<label>
Password
<input name="password" type="password" value="password" />
</label>
<button>Log in</button>
</form>
</template>
server/signIn.vue
export defineFormAction((event) => {
const request = getNativeRequest(event)
const data = request.formData()
// signin the User
// Not that we should have a way to return something that works
// well with progressive enhancement to the client,
//Like a JSON payload that can contain data and "metadata" (errors, redirects )
// so they can be applied smoothly in CSR.
// A `respondWithFormAction` helper could be added too.
const result = `This was sent ${JSON.stringify(data)}`
return actionResponse(event, { result } )
})
To recap here :
- New vue directive for nuxt,
v-enhance
to use on forms to enhance them - New event handler for Nitro or H3,
defineFormAction
. Could accept a function or an object of functions - New server directory for Nitro
/server/actions
to define server actions. - New helpers for H3
getNativeRequest()
andactionResponse()
to simplify the action workflow - New feature for Nuxt,
useAction
oruseEnhance
, to work with actionResponse and v-enhance.
Overall I feel like this API is respectful of the standard web semantics, and feels vue-ish. Feel free to suggest any improvements for it.
Reference from other frameworks :
- Svelte Form actions, Discussion for semantic form actions
- Solid Start actions
- Remix Actions
- Kent PESPA article
- Next.js Server Actions
Additional information
- Would you be willing to help implement this feature?
- Could this feature be implemented as a module?
Final checks
- Read the contribution guide.
- Check existing discussions and issues.