Usage

Advanced Usage

Advanced usage of form actions and server loaders.

Form Actions alongside Server Loaders

You can define form actions and server loaders in the same file. Let's look at a todo list example :

actions/todos.ts
import { createTodo, deleteTodo, getTodos } from "../db"

export default defineFormActions({
  add: async (event) => {
    const description = (await readFormData(event)).get("description") as string
    try {
      const todo = await createTodo(description)
      return actionResponse(event, { todo })
    }
    catch (e) {
      if (e instanceof Error) {
        return actionResponse(event, { todoId },
          { error: { code: 422, message: e?.message } })
      }
      throw e
    }
  },
  delete: async (event) => {
    const todoId = (await readFormData(event)).get("id") as string
    try {
      const todo = await deleteTodo(todoId)
      return actionResponse(event, { todo })
    }
    catch (e) {
      if (e instanceof Error) {
        return actionResponse(event, { todoId },
          { error: { code: 422, message: e?.message } })
      }
      throw e
    }
  }
})

export const loader = defineServerLoader(async () => {
  const todos = await getTodos()
  return { todos }
})

Use them together in your pages :

pages/todos.vue
<script setup lang="ts">
// Rename the enhance props to handle multiple forms on the same page.
const { data, enhance: createTodo } = await useFormAction({
  loader: "todos" // This is needed for Typescript to infer the loader return type.
})
const { enhance: deleteTodo } = await useFormAction()
</script>

<template>
  <div>
    <h1>Todos</h1>

    <form v-enhance="createTodo" method="POST" action="todos">
      <label>
        add a todo:
        <input name="description" autocomplete="off">
      </label>
    </form>

    <ul v-if="data.loader?.todos" class="todos">
      <li v-for="todo in data.loader.todos" :key="todo.id">
        <form v-enhance="deleteTodo" method="POST" action="todos?delete">
          <input type="hidden" name="id" :value="todo.id">
          <span>{{ todo.description }} - {{ todo.id }}</span>
          <button aria-label="Mark as complete" />
        </form>
      </li>
    </ul>
  </div>
</template>

Optimistic updates

Optimistic updates is a pattern that updates an UI before the server responds. Let's take the previous example further and introduce optimistic updates.

The optimistic function is exposed by the run function. You can access it from the useFormAction argument. optimistic accepts a callback that exposes a result ref that can be mutated to update the state stored in useFormAction.

useFormAction uses Nuxt's useState under the hood so all composables that reference the same loader will properly update.

pages/todos.vue
<script setup lang="ts">
const { data, enhance: createTodo } = await useFormAction({
  loader: "todos"
})
const { enhance: deleteTodo } = await useFormAction({
  loader: "todos",
  run: ({ optimistic, formData }) => {
    optimistic(({ result }) => {
      // This will update the UI before any data-fetching.
      result.value.todos = result.value.todos.filter(todo => todo.id !== formData.id)
    })
  }
})
</script>

<template>
  <div>
    <h1>Todos</h1>

    <form v-enhance="createTodo" method="POST" action="todos">
      <label>
        add a todo:
        <input name="description" autocomplete="off">
      </label>
    </form>

    <ul v-if="data.loader?.todos" class="todos">
      <li v-for="todo in data.loader.todos" :key="todo.id">
        <form v-enhance="deleteTodo" method="POST" action="todos?delete">
          <input type="hidden" name="id" :value="todo.id">
          <span>{{ todo.description }} - {{ todo.id }}</span>
          <button aria-label="Mark as complete" />
        </form>
      </li>
    </ul>
  </div>
</template>

Run function

The run function can be used to customize the behavior of the useFormAction composable. Here's the full Typescript interface for the run function :

interface ActionFunctionArgs<R extends LoaderName> {
  /**
   * Cancel the default submission.
   */
  cancel: () => void
  /**
   * Handle optimistic updates
   *
   * @param update Update callback to update the result.
   */
  optimistic: (update: UpdateFunction<R>) => void
  /**
   * An object version of the `FormData` from this form
   */
  formData: Record<string, any>
  /**
   * The original submit event.
   */
  event: SubmitEvent
  /**
   * The name of the action.
   */
  action: string
  /**
   * The form element.
   */
  form: HTMLFormElement
  /**
   * The Element that submitted.
   */
  submitter: HTMLElement
  /**
   * The loader URL.
   */
  loader: string | undefined
  /**
   * The default submit function.
   */
  submitForm: () => Promise<void>
}

Advanced Example

Refer to the advanced and vorms-zod templates to illustrate all the different way this module can be used.