Form generation and validation with FormKit
Tags: nuxt3, formkit, validation
Published: 2022-02-23 14:31:57

Since our project is already on the bleeding edge, we might just as well use a very exciting form library that just came out of private beta: [FormKit](https://formkit.com/).

## Setting up FormKit in Nuxt3

At the time of writing this, the official `@formkit/nuxt` module offers little more than passing the options from an external file to the library and configuring transpilation. Also, since the `FormKitSchema` component isn't imported by default, we'll setup the library ourselves in a plugin.
```shell
yarn add -D @formkit/vue
```

The library needs to be transpiled to work with ES modules, so in `nuxt.config.ts` we add `@formkit/vue` to the `build.transpile` array.

We can now create our plugin in `plugins/formkit.ts`:
```typescript
import { plugin, defaultConfig, FormKitSchema } from "@formkit/vue";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component("FormKitSchema", FormKitSchema);
  nuxtApp.vueApp.use(plugin, defaultConfig);
});
```

### Configuring form classes with Tailwind CSS

Let's add some styles to our form [sections](https://formkit.com/essentials/inputs#sections) by customizing the default plugin options:
```typescript
// ...
import type { DefaultConfigOptions } from "@formkit/vue";

const options: DefaultConfigOptions = {
  config: {
    classes: {
      outer: "mb-3",
      label: "font-bold",
      help: "text-sm text-slate-400",
      messages: "mb-3 text-sm text-red-800",
    },
  },
};

// ...
  nuxtApp.vueApp.use(plugin, defaultConfig(options));
// ...
```

Adding the `btn` class to the submit input is a little more tricky, as it requires hooking into [`rootClasses`](https://formkit.com/essentials/styling#root-classes-function):
```typescript
// ...
    rootClasses(sectionKey, node) {
      return {
        [`formkit-${sectionKey}`]: true,
        ...(sectionKey === "input" && node.props.type === "submit" && { btn: true }),
      };
    },
// ...
```

## Refactoring the login form with FormKit

Our login form's structure (fields types, names, labels and validation) can be abstracted away inside `useAuth` by adding a [FormKit schema](https://formkit.com/advanced/schema) property which we'll name `loginFormSchema`:
```typescript
// FormKit schema for the login form
const loginFormSchema = [
  {
    $formkit: "text",
    name: "email",
    label: "Email",
    validation: "required|email",
  },
  {
    $formkit: "password",
    name: "password",
    label: "Password",
    validation: "required",
  },
];
```

Our `FormLogin` component can now be rewritten like so:
```vuejs
<script setup lang="ts">
const { login, loginFormSchema } = useAuth();
const formErrors = ref<string[]>([]);

async function handleSubmit(credentials: any) {
  const redirect = (useRoute().query as { redirect: string }).redirect || "/";
  try {
    await login(credentials as LoginFormData);
    useRouter().push(redirect);
  } catch (error) {
    // Remove status code and path from error message
    const message = (error as Error).message.replace(/\d+ (.*) .*/, "$1");
    formErrors.value = [message];
  }
}
</script>

<template>
  <FormKit type="form" :errors="formErrors" submit-label="Login" @submit="handleSubmit">
    <FormKitSchema :schema="loginFormSchema" />
  </FormKit>
</template>
```

In the code above, we bind `formErrors` in order to handle server errors in addition to validation warnings. To make the error human-readable, we extract the meaningful part with a regex before adding it to our form.

Also, we have to cast `credentials as LoginFormData` because the typings are not available at build time (i.e. FormKit being very dynamic, the structure could be altered at runtime).

## Conclusion

This concludes the first public version of this series, which I hope you have found both enjoyable and helpful. I plan on keeping it up to date as the various libraries used evolve.