nuxt / nuxt

The Intuitive Vue Framework.
https://nuxt.com
MIT License
54.94k stars 5.02k forks source link

Form actions for progressive enhancement #20649

Open Hebilicious opened 1 year ago

Hebilicious commented 1 year ago

Describe the feature

Edit : Form actions have been released as a Nuxt module 🎉 Please try it and leave feedback.


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 : image

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="john@doe.com" />
    </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="john@doe.com" />
    </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 :

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 :

Additional information

Final checks

Hebilicious commented 1 year ago

Currently this is implemented as a module : https://github.com/Hebilicious/form-actions-nuxt

sanba-anass commented 1 year ago

is this a stable module ?

Currently this is implemented as a module : https://github.com/Hebilicious/form-actions-nuxt

Hebilicious commented 1 year ago

is this a stable module ?

Currently this is implemented as a module : Hebilicious/form-actions-nuxt

I would say it is still experimental. I'm looking for feedback from users to see if some things will need to be changed so if you're interested give it a try. We're working with @pi0 on getting direct nitro support https://github.com/unjs/nitro/pull/1286, which will make this module much easier to use

Edit: Starting from Nuxt 3.7.0 the module works without modification to Nitro/H3.

binajmen commented 1 year ago

From a DX perspective, I think it's logical to incorporate this directly into Nuxt. Currently, being new to Nuxt, I'm finding it challenging to understand the correct approach to handle form submissions, especially since I've previously worked with Remix and have lost touch with the methods used before.

Hebilicious commented 1 year ago

From a DX perspective, I think it's logical to incorporate this directly into Nuxt. Currently, being new to Nuxt, I'm finding it challenging to understand the correct approach to handle form submissions, especially since I've previously worked with Remix and have lost touch with the methods used before.

While I do agree with you, the module itself has very low popularity and usage at this point, so I'm not sure this is on the table at this point :/

mubaidr commented 1 year ago

Maybe a bit off topic but server part in nuxt is not loved on nuxt documentation. (Though a recent pull request added server section to homepage)

Nuxt has everything for building a complete server,infact great things. I would love to see Nuxt actually be recommended/ marketed as full stack framework where form actions or Astro like forms are available (btw forms in astro looks great: https://docs.astro.build/en/recipes/build-forms/).

binajmen commented 1 year ago

From a DX perspective, I think it's logical to incorporate this directly into Nuxt. Currently, being new to Nuxt, I'm finding it challenging to understand the correct approach to handle form submissions, especially since I've previously worked with Remix and have lost touch with the methods used before.

While I do agree with you, the module itself has very low popularity and usage at this point, so I'm not sure this is on the table at this point :/

In that case, I'm curious about how people handle progressive enhancement or even form submissions in a seamless way. If one still needs to bind the input fields and use an e.preventDefault() function handler, then I'm not convinced..

I couldn't find comprehensive documentation on the topic, nor any convincing examples.

Hebilicious commented 1 year ago

In the meantime, please use and recommend the module and let me know if you have any issues with it 🙏

AaronBeaudoin commented 1 year ago

I think that moving this into Nuxt core would be very smart. Nuxt maintainers—If you have not already read this article, please check it out for why this feature is arguably such a significant steps forward:

The Web’s Next Transition — Kent C. Dodds Progressively Enhanced Single Page Apps (PESPAs)

TLDR: Form actions not only provide the benefit of a built-in way to allow a website to work even in the absence of JavaScript (see this commonly linked flowchart), but more importantly, they allow for a dramatically simpler mental model by simplifying state management and colocating related client and server code.

Also, interest in this approach is gaining traction with both Next.js 14 getting Server Actions as of Oct 26 which can be used with forms for progressive enhancement and Remix getting Vite support as of Oct 31 which should help it's adoption. Notably, SvelteKit mentions in its docs that although API routes can be used to send data to the server, form actions "are a better way to submit data".

Sematically, Nuxt already does the equivalent of this for <a> elements. Before JS loads, the browser default behavior simply works as a "fallback", but after JavaScript loads (or potentially if it loads) then client-side navigation is used and the links are "progressively enhanced". This feature would just extends that philosophy to <form> elements, which is extremely powerful for DX and accessibility.