Build Better Forms with Vue.js 3 Composition API: A Practical Guide

Learn how to create custom form components with Vue.js 3 Composition API, including validation, submission handling and more 🔥

Introduction

Managing large and complex forms can be a challenge for any web developer. Fortunately, Vue.js 3 provides a powerful solution in the form of the Composition API. With the Composition API, you can create custom form components that make it easy to manage form data, perform validation, and handle form submissions. In this article, I'll explore how to use the Vue.js 3 Composition API to design and manage complex forms, providing valuable insights and practical examples that will be of interest to Vue.js developers of all levels.

Steps to follow

Introduction to the Vue.js 3 Composition API

Summary
Explains what the Composition API is, and how it differs from the Options API. Discusses the benefits of using the Composition API, such as better code organization and reusability.

If you're a Vue.js developer, you're likely familiar with the Options API, which has been the standard way of building Vue.js components for many years. However, with the release of Vue.js 3, a new way of building components has been introduced - the Composition API.

The Composition API is a powerful and flexible way to build Vue.js components that provides many benefits over the Options API. With the Composition API, you can encapsulate component logic into reusable functions, making your code more modular and easier to read. You can also share state and logic between components, making it easier to manage complex applications.

One of the key benefits of the Composition API is that it allows you to organize your code based on logic, rather than based on lifecycle hooks. This makes it easier to reason about your code and to reuse logic across multiple components. Additionally, the Composition API provides a more intuitive way to work with reactive data, making it easier to manage complex state.

In the next sections, I'll explore how to use the Vue.js 3 Composition API to build custom form components that make it easy to manage form data, perform validation, and handle form submissions.

Designing a Custom Form Component

Summary
Demonstrates how to create a custom form component using the Composition API. Discusses how to use reactive state to manage form data and track changes to form fields.

One of the most powerful features of the Vue.js 3 Composition API is the ability to create reusable functions that encapsulate component logic. This makes it easy to build custom form components that can be used throughout your application.

To create a custom form component using the Composition API, you can use the defineComponent() function along with the defineProps() function. Here's an example:

CustomForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <label>
      First Name:
      <input type="text" v-model="formData.firstName" />
    </label>
    <label>
      Last Name:
      <input type="text" v-model="formData.lastName" />
    </label>
    <button type="submit">Submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, defineProps, reactive } from 'vue';

interface FormData {
  firstName: string;
  lastName: string;
}

export default defineComponent({
  name: 'CustomForm',
  props: defineProps({
    formData: {
      type: Object as () => FormData,
      required: true
    }
  }),
  setup(props) {
    // Define reactive state for form data
    const formData = reactive(props.formData);

    // Define a function to handle form submissions
    const handleSubmit = () => {
      // Perform form submission logic here
    };

    // Return the component's properties and methods
    return {
      formData,
      handleSubmit
    };
  }
});
</script>

In this example, we're defining a custom form component called CustomForm that takes a prop called formData. We're using the defineProps() function to define the prop's type and requirements, and we're using the reactive() function to define reactive state for the form data. We're also defining a function called handleSubmit() to handle form submissions.

To use this custom form component in your application, you can import it and include it in your template like this:

App.vue

<template>
  <CustomForm :formData="formData" />
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';
import CustomForm from './CustomForm.vue';

interface FormData {
  firstName: string;
  lastName: string;
}

export default defineComponent({
  name: 'App',
  components: {
    CustomForm
  },
  setup() {
    // Define initial form data
    const formData = reactive<FormData>({
      firstName: '',
      lastName: ''
    });

    // Return the component's properties and methods
    return {
      formData
    };
  }
});
</script>

By using the Composition API to define custom form components, you can encapsulate form logic into reusable functions, making it easier to build and maintain complex forms in your application. By using Vite and TypeScript, you can also benefit from faster development times and improved code reliability.

Performing Form Validation

Summary
Shows how to use computed properties to perform form validation and display error messages. Discusses how to use watchers to perform real-time validation and prevent invalid form submissions.

Form validation is an essential part of any web application that accepts user input. With the Vue.js 3 Composition API, it's easy to perform form validation and provide users with meaningful feedback when their input is invalid.

To perform form validation, you can define a function that checks the validity of the form data, and use it to conditionally enable or disable the form submission button. Here's an example:

CustomForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <label>
      First Name:
      <input type="text" v-model="formData.firstName" />
      <div v-if="!isValidFirstName" class="error">Please enter a valid first name</div>
    </label>
    <label>
      Last Name:
      <input type="text" v-model="formData.lastName" />
      <div v-if="!isValidLastName" class="error">Please enter a valid last name</div>
    </label>
    <button type="submit" :disabled="!isFormValid">Submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, defineProps, reactive, computed } from 'vue';

interface FormData {
  firstName: string;
  lastName: string;
}

export default defineComponent({
  name: 'CustomForm',
  props: defineProps({
    formData: {
      type: Object as () => FormData,
      required: true
    }
  }),
  setup(props) {
    // Define reactive state for form data
    const formData = reactive(props.formData);

    // Define computed properties for form validation
    const isValidFirstName = computed(() => formData.firstName.length > 0);
    const isValidLastName = computed(() => formData.lastName.length > 0);
    const isFormValid = computed(() => isValidFirstName.value && isValidLastName.value);

    // Define a function to handle form submissions
    const handleSubmit = () => {
      // Perform form submission logic here
    };

    // Return the component's properties and methods
    return {
      formData,
      isValidFirstName,
      isValidLastName,
      isFormValid,
      handleSubmit
    };
  }
});
</script>

In this example, we've added two computed properties to the setup() function that check the validity of the firstName and lastName fields. We've also added a third computed property that checks if the entire form is valid. We're using these computed properties to conditionally enable or disable the form submission button and show error messages when the input is invalid.

You can use the Composition API to perform more advanced form field validation, such as checking for valid email addresses or phone numbers. Vue.js 3 provides several built-in validation rules that can be applied to form fields, namely required, minLength, maxLength, email, numeric, and integer.

By using the Composition API to define custom form components with form validation, you can provide users with a better user experience and improve the overall reliability of your application.

Handling Form Submissions

Summary
Discusses how to handle form submissions using the Composition API. Shows how to use the emit() function to emit events from child components and trigger actions in the parent component.

Once the user has filled out a form, the next step is to handle the form submission. With the Composition API, you can define a function to handle form submissions and perform any necessary validation or data manipulation.

To handle form submissions, you can define a function that gets called when the form is submitted. This function can perform any necessary validation or data manipulation and then send the data to the server or perform any other necessary action. Here's an example:

CustomForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <label>
      First Name:
      <input type="text" v-model="formData.firstName" />
    </label>
    <label>
      Last Name:
      <input type="text" v-model="formData.lastName" />
    </label>
    <button type="submit">Submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, defineProps, reactive } from 'vue';

interface FormData {
  firstName: string;
  lastName: string;
}

export default defineComponent({
  name: 'CustomForm',
  props: defineProps({
    formData: {
      type: Object as () => FormData,
      required: true
    }
  }),
  setup(props) {
    // Define reactive state for form data
    const formData = reactive(props.formData);

    // Define a function to handle form submissions
    const handleSubmit = () => {
      // Perform form validation logic here
      if (formData.firstName.length === 0 || formData.lastName.length === 0) {
        alert('Please enter both first and last name');
        return;
      }

      // Perform form submission logic here
      alert(`Form submitted with data: ${JSON.stringify(formData)}`);
    };

    // Return the component's properties and methods
    return {
      formData,
      handleSubmit
    };
  }
});
</script>

In this example, we've defined a handleSubmit function in the setup() function that gets called when the form is submitted. We're performing some basic form validation by checking that both the firstName and lastName fields are not empty. If the form is valid, we're sending the form data to the server (in this case, just displaying an alert with the form data).

Advanced Topics

Summary
Discusses more advanced topics related to managing complex forms with the Composition API, such as conditional form fields, dynamic form fields, and form field arrays. Provides practical examples of how these advanced features can be used to enhance the functionality of a web application.

While the above examples cover the basics of using the Vue.js 3 Composition API for forms, there are several advanced topics you can explore to take your forms to the next level. Here are a few examples:

  1. Dynamic Form Fields - You can use the Composition API to dynamically add or remove form fields based on user input or other factors. This can be useful for creating more flexible and customized forms. Here's an example:

CustomForm.vue

<template>
  <form @submit.prevent="handleSubmit">
    <label v-for="(field, index) in formData.fields" :key="index">
      {{ field.label }}:
      <input :type="field.type" v-model="field.value" />
    </label>
    <button type="button" @click="addFormField">Add Field</button>
    <button type="submit">Submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, defineProps, reactive } from 'vue';

interface FormField {
  label: string;
  type: string;
  value: string;
}

interface FormData {
  fields: FormField[];
}

export default defineComponent({
  name: 'DynamicForm',
  props: defineProps({
    formData: {
      type: Object as () => FormData,
      required: true
    }
  }),
  setup(props) {
    // Define reactive state for form data
    const formData = reactive(props.formData);

    // Define function to add new form field
    const addFormField = () => {
      formData.fields.push({
        label: 'New Field',
        type: 'text',
        value: ''
      });
    };

    // Define function to handle form submissions
    const handleSubmit = () => {
      // Perform form submission logic here
      alert(`Form submitted with data: ${JSON.stringify(formData)}`);
    };

    // Return the component's properties and methods
    return {
      formData,
      addFormField,
      handleSubmit
    };
  }
});
</script>

In this example, I'm using the Composition API to dynamically add or remove form fields based on user input. I'm defining a FormField interface and using it to define the fields array in the FormData interface. I'm also defining a addFormField function that adds a new form field to the fields array when the "Add Field" button is clicked.

  1. Error Handling - In addition to displaying form validation errors to the user, you may also need to handle more general errors that occur during form submissions, such as network errors or server errors. To handle these types of errors in Vue.js 3, you can use the try/catch block around your form submission logic and catch any errors that are thrown. You can then display a generic error message to the user or provide them with more specific instructions for resolving the error. Additionally, you can use global error handling libraries like Vue.js Error Handler or custom event handling to manage errors across your entire application.

CustomForm.vue

<template>
  <form @submit.prevent="submit">
    <!-- form fields -->
    <input type="text" v-model="name" />
    <input type="email" v-model="email" />
    <!-- error message -->
    <div v-if="errorMessage">{{ errorMessage }}</div>
    <button type="submit">Submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

interface FormFields {
  name: string;
  email: string;
}

export default defineComponent({
  props: {
    onSubmit: {
      type: Function as () => (data: FormFields) => Promise<void>,
      required: true,
    },
  },
  setup(props) {
    // form fields
    const name = ref('');
    const email = ref('');

    // error message
    const errorMessage = ref('');

    const submit = async () => {
      // validate form fields
      if (!name.value || !email.value) {
        errorMessage.value = 'Please fill in all fields';
        return;
      }

      // submit form
      try {
        await props.onSubmit({
          name: name.value,
          email: email.value,
        });
      } catch (error) {
        errorMessage.value = 'An error occurred while submitting the form';
        console.error(error);
      }
    };

    return {
      name,
      email,
      errorMessage,
      submit,
    };
  },
});
</script>

In this example, I added an error message div that is only displayed if the errorMessage variable is truthy. The submit function now includes a try-catch block, where any errors that occur during form submission will be caught and the errorMessage variable will be set accordingly. Finally, we log the error to the console for debugging purposes. I've also used the as syntax to cast the onSubmit prop to a function that takes in the FormValues type as an argument and returns a Promise<void>.

  1. Multi-step - One even more advanced topic when it comes to complex forms in Vue.js 3 is building multi-step or wizard-style forms. These types of forms typically involve multiple pages or steps that guide the user through a complex process or workflow and often require saving and retrieving data between steps. To implement this type of form in Vue.js 3, you would need to use advanced routing techniques, such as nested routes or named views, to manage the different steps of the form. You would also need to carefully manage the state of the form data between steps, either by storing it in a Vuex store or using other advanced state management techniques. Additionally, you would need to consider the user experience and design of the form, such as how to display progress indicators and how to handle user navigation between steps. Overall, building multi-step forms in Vue.js 3 requires a high level of expertise and experience with the framework.

Let me know in the comment if you want me to build a multi-step form example in the next article :D

Thank you for reading <3

Useful links

https://vuejs.org/guide/introduction.html

https://vuejs.org/guide/typescript/composition-api.html#typing-component-props

https://www.freecodecamp.org/news/the-vue-handbook-a-thorough-introduction-to-vue-js-1e86835d8446/