Vue 2 and Vue 3 OTP/PIN Form Tutorial: Implementing a Headless Technique for Seamless Results
In the ever-expanding world of fintech and banking solutions, the demand for seamless authentication and verification processes has never been higher. One common challenge faced by Vue developers is crafting user-friendly one-time password (OTP) and PIN input forms.
Traditional input fields are straightforward — they take an input and display the value. However, we aspire to offer our users an exceptional experience. We envision input fields that gracefully split and progress as users interact with the form. While various packages exist for this purpose, many lack the flexibility and customization we seek.
In this tutorial, we embark on a journey to create a distinctive OTP form field using a Headless approach, all without relying on external packages.
Prerequisite
This guide is designed for developers who already have experience with Vue.js. Whether you’re working on a project that requires a flexible OTP form or you’re creating a component library for your team, this guide is tailored to meet your needs. Prior knowledge of Vue.js is essential to make the most of this resource.
Tutorial Overview
In this tutorial, we will focus on implementing the OTP feature using Vue 3 with the Options API. For those interested in the Composition API, a separate resource will be provided in a future course or article. This tutorial aims to provide a comprehensive understanding of OTP implementation in Vue.js.
Get a cup of coffee, and let’s dive right into it!
Tutorial sections
- Environmental Setup: We’ll start by setting up the necessary environment and tools to begin working on our OTP/PIN input field project. This step will ensure you have everything in place to proceed smoothly.
- Creating the Required Files: Next, we’ll create the essential files and project structure needed for our OTP/PIN input field. Organizing your project effectively is a crucial step in building a maintainable application.
- Writing the Logic: Here, we’ll dive deep into the code and logic required to build our Headless UI OTP/PIN input field. We’ll explore different aspects of Vue 2 and Vue 3, leveraging the Options API and Composition API to create a robust and user-friendly component.
- Usage: Finally, we’ll demonstrate how to utilize the OTP/PIN input field component within your Vue applications. You’ll learn how to integrate this feature seamlessly into your projects, enhancing user authentication and verification processes.
By the end of this tutorial, you’ll have a solid grasp of creating a versatile OTP/PIN input field in Vue 3, allowing you to take your Vue development skills to the next level. Let’s get started!
Environmental Setup
To set up the OTP component, follow these steps in your terminal:
- Navigate to the desired directory using the terminal:
cd desired-directory
2. Run the following command to create a Vue project named “otp-component”:
vue create otp-component
Note: If `Vue` is not installed on your system, execute the following command:
# Install Vue globally (if not already installed)
npm install -g @vue/cli
Then proceed with the ‘vue create otp-component’ command.
You will encounter a menu resembling the image below. From there, simply choose ‘Vue 3’.
We opt for ‘Vue 3’ due to its flexibility, allowing us to harness both the ‘Options API’ and the ‘Composition API’ for our development needs.
Creating the Required Files
After you’ve finished creating the Vue 3 app, proceed to create the following project files as follows:
otp-component/
├── src/
│ ├── components/
│ │ ├── otp/
│ │ │ ├── Otp.Component.vue
│ │ │ ├── OtpGroup.Component.vue
│ │ │ ├── OtpGroupInput.Component.vue
│ │ │ ├── OtpErrorMessage.Component.vue
│ │ │ ├── index.js
│ ├── views/
│ │ ├── App.vue
Writing the Logic
Alright, let’s dive into the exciting world of component logic! Grab your virtual coding gear as we open up the Otp.Component.vue
file and embark on this Vue.js adventure! 🚀💻
Otp.Component.vue
<template>
<div name="otp">
<!-- Here, we define a slot to pass data to our component -->
<slot
:digits="digits"
:is-valid="isValid"
:on-blur="onBlur"
:on-input="onInput"
:on-paste="onPaste"
/>
<!-- Another slot for handling errors -->
<slot name="error" :message="errorMessage" :hasError="error" />
</div>
</template>
In the template
section:
We set up the basic structure of our component. It’s like building a house with rooms that can be customized later. We create slots where we can insert data from the outside.
Now, get ready for the exciting part — the script section where all the magic happens!
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "otp-component", // Giving our component a name
props: {
modelValue: String, // This is where we receive input from the user
count: { // This is the place where users will specify the number of input fields.
type: Number,
required: true,
},
},
data() {
return {
error: false, // We start without errors
errorMessage: "", // No error message initially
digits: Array(this.count).fill(""), // Create an array to store our OTP digits
};
},
computed: {
isValid() {
return this.digits.length === this.count; // We check if all OTP digits are filled
},
},
mounted() {
if (this.modelValue) this.digits = this.modelValue?.split("");
// When our component is ready, we check if there's already an OTP provided and fill it in
},
methods: {
onInput(index, digit) {
// This function handles user input for each OTP digit
switch (typeof digit) {
case "string":
this.digits[index] = digit;
break;
case "number":
this.digits[index] = digit?.toString();
break;
default:
this.digits[index] = digit?.data ? digit?.data : "";
break;
}
this.$emit("update:modelValue", this.digits?.join(""));
// We update the OTP digits and notify the parent component of changes
},
onPaste(index, event) {
event.preventDefault();
const clipboardData = event?.clipboardData || window?.clipboardData;
if (clipboardData) {
const pastedText = clipboardData.getData("text").trim();
// Remove non-digit characters from the pasted text
const parsedPastedText = pastedText.replace(/\D/g, "");
// Initialize an array for the new digits
let newDigits = Array(this.count).fill("");
// Check if the pasted text length exceeds the available input fields
if (this.digits.length <= parsedPastedText?.length) {
this.errro = true;
this.errorMessage = "Invalid characters in the pasted OTP.";
}
// Copy parsed digits into the newDigits array, starting from the current index
for (let i = 0; i < parsedPastedText.length; i++) {
if (index + i < newDigits.length) {
newDigits[index + i] = parsedPastedText[i];
}
}
// Update the digits with the newly pasted values
this.digits = newDigits;
} else {
// Handle cases where clipboard data is unavailable
this.error = true;
this.errorMessage = "Invalid characters in the pasted OTP.";
}
},
onBlur() {
// This function is called when the input field loses focus
this.checkDigits();
},
clearDigits() {
// This function clears all OTP digits and errors
this.error = false;
this.digits.fill("");
this.errorMessage = "";
},
checkDigits() {
// You can add custom validation logic here
if (!this.isValid) {
this.errorMessage = "Please enter a valid OTP";
this.error = true;
} else {
this.errorMessage = "";
this.error = false;
}
}
}
});
</script>
In the script
section:
- It receives user input via the
modelValue
prop and allows users to specify the number of input fields with thecount
prop. - The component maintains a state with
error
,errorMessage
, and an array ofdigits
. - It computes whether the OTP is valid by checking if all digits are filled.
- When mounted, it initializes the
digits
array with values frommodelValue
. - The
onInput
method handles user input for each OTP digit and emits changes to the parent component. onPaste
handles pasted content, sanitizes it, and updates the OTP digits.onBlur
is triggered when the input field loses focus and can perform custom validation.clearDigits
clears all OTP digits and errors.- The component emits updates to the parent component using the
update:modelValue
event. - It provides flexibility for OTP input and error handling.
Next, let’s work on the OtpGroup.Component
OtpGroup.Component.vue
<template>
<div name="otp-group" ref="otpGroup">
<slot :focus-next="focusNext" :focus-prev="focusPrev" />
</div>
</template>
In the template
section:
- We use the
ref
attribute to assign a reference to this<div>
element, which we name "otpGroup." - We bind the
focus-next
andfocus-prev
slot props to thefocusNext
andfocusPrev
methods defined in the component'sscript
section.
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "otp-group",
methods: {
focusNext(index) {
if (this.$refs.otpGroup?.children[index + 1]) {
this.$refs.otpGroup.children[index + 1]?.focus();
}
},
focusPrev(index) {
if (index > 0) {
this.$refs.otpGroup.children[index - 1]?.focus();
}
}
}
});
</script>
In the script
section:
- Within this component, we define two methods:
focusNext
andfocusPrev
. focusNext(index)
is a method that receives anindex
parameter. It is used to focus on the next child element within the<div>
with the reference "otpGroup" when called. It checks if there is a next child element, and if so, it sets the focus on it.focusPrev(index)
is a method that receives anindex
parameter. It is used to focus on the previous child element within the<div>
with the reference "otpGroup" when called. It checks if there is a previous child element (by ensuring the index is greater than 0), and if so, it sets the focus on it.
Overall, this component acts as a container for a slot, allowing the parent component to pass in content with focus-next
and focus-prev
slot props. The focusNext
and focusPrev
methods enable navigation between child elements within the container.
Let’s implement the OtpGroupInput.Component
`OtpGroupInput.Component.vue
<template>
<input
v-model="digit"
type="text"
:maxlength="1"
/>
</template>
In the template
section:
- We define an
<input>
element, which is a text input field. - We use
v-model
to bind thedigit
data property to the input's value. This means that the input's value will be controlled by thedigit
property in the component's data. - The input has
type="text"
, indicating that it's a text input. - We use
:maxlength="1"
to set the maximum length of the input to 1 character.
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "otp-group-input",
props: {
value: String,
},
data() {
return {
digit: "",
};
},
watch: {
value() {
this.setIntialValue();
},
digit(newValue, oldValue) {
if (newValue !== oldValue) {
if (newValue === "") {
this.emitPrev();
} else {
this.emitNext();
}
}
},
},
methods: {
emitPrev() {
this.$emit("prev");
},
emitNext() {
this.$emit("next");
},
setIntialValue() {
switch (typeof this.value) {
case "string":
this.digit = this.value;
break;
case "number":
this.digit = this.value?.toString();
break;
default:
this.digit = this.value?.data ? this.value?.data : "";
break;
}
}
}
});
</script>
In the script
section:
- The component accepts a
value
prop of type String, which will be used to set the initial value of the input. - In the
data
section, we define adigit
property, initially set to an empty string. This property represents the value of the input field. - We use the
watch
section to watch for changes in thevalue
anddigit
properties: - When the
value
prop changes, thevalue
watcher calls thesetInitialValue
method to set thedigit
property based on thevalue
prop's type. - When the
digit
property changes, thedigit
watcher checks if it's empty and emits either a "prev" or "next" event using the$emit
method, depending on whether the input is cleared or a new digit is entered. - The
emitPrev
andemitNext
methods emit "prev" and "next" events, respectively, which can be used by the parent component to navigate between input fields. - The
setInitialValue
method sets thedigit
property based on the type of thevalue
prop, handling cases wherevalue
is a string, number, or an object with adata
property.
This component represents an individual input field for an OTP (One-Time Password) and provides functionality for handling input, emitting events, and setting initial values based on the provided value
.
Finally, let’s implement the OtpErrorMessage.Component
OtpErrorMessage.Component.vue
<template>
<div v-show="hasError" name="otp-error-message">
{{ displayMessage }}
<slot />
</div>
</template>
In the template
section:
- We have a
<div>
element that is conditionally displayed usingv-show="hasError"
. This means the<div>
will only be visible when thehasError
prop istrue
. - Inside the
<div>
, we display thedisplayMessage
computed property using{{ displayMessage }}
. This displays an error message ifdisplayMessage
is not empty. - Additionally, there is a
<slot />
element. This slot allows the parent component to insert content within this<div>
. Any content provided by the parent will be rendered in place of the<slot />
.
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "otp-error-message",
props: {
message: String,
hasError: Boolean,
},
computed: {
displayMessage() {
return this.message ? this.message : "";
},
},
});
</script>
In the script
section:
- The component accepts two props:
message
: A String prop that represents an error message.hasError
: A Boolean prop that indicates whether an error is present or not.- Inside the
computed
section, we define a computed property nameddisplayMessage
. This computed property checks if themessage
prop is provided and returns it. Ifmessage
is empty or not provided, it returns an empty string.
This component is designed to display an error message conditionally based on the hasError
prop and allows the parent component to insert custom content within the error message <div>
using slots. The displayMessage
computed property ensures that the error message is only displayed when the message
prop has content.
Now, let’s export all the component files.
index.js
import Otp from './Otp.Component.vue';
import OtpGroup from './OtpGroup.Component.vue';
import OtpGroupInput from './OtpGroupInput.Component.vue';
import OtpErrorMessage from './OtpErrorMessage.Component.vue';
export {
Otp,
OtpGroup,
OtpGroupInput,
OtpErrorMessage
};
The index.js
file in this context serves as an entry point for exporting multiple Vue components from the "otp" folder. It acts as a module, making all the components easily accessible and importable for use in different parts of the application.
Usage
You can seamlessly integrate the Headless OTP Component into your App.vue
by following these steps:
App.vue
<template>
<otp v-model="pin" :count="5" ref="otpComponent">
<template v-slot="{ digits, onInput, onPaste, onBlur }">
<div class="flex items-center space-x-4">
<otp-group class="flex space-x-2 p-2">
<template v-slot="{ focusNext, focusPrev }">
<otp-group-input
v-for="(digit, index) in digits"
:key="index"
:value="digit"
autofocus
placeholder="*"
@blur="onBlur"
@next="focusNext(index)"
@prev="focusPrev(index)"
@paste="onPaste(index, $event)"
@input="onInput(index, $event)"
class="h-12 w-12 rounded-xl text-3xl text-center text-slate-700 caret-teal-400 border-2 border-slate-300 placeholder:text-slate-400 focus:outline-none focus:border-teal-400"
/>
</template>
</otp-group>
<button
class="w-8 h-8 rounded-full transition-color text-slate-700 hover:text-teal-500"
@click="clearPin"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-8 h-8"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
</div>
</template>
<template v-slot:error="{ message, hasError }">
<otp-error-message :message="message" :hasError="hasError" />
</template>
</otp>
</template>
<script>
import { defineComponent } from "vue";
// COMPONENTS
import { Otp, OtpGroup, OtpGroupInput, OtpErrorMessage } from "@/components/otp";
export default defineComponent({
name: "App",
components: {
Otp,
OtpGroup,
OtpGroupInput,
OtpErrorMessage,
},
data() {
return {
pin: "",
};
},
watch: {
pin(pin) {
console.log({ pin });
},
},
methods: {
clearPin() {
this.pin = "";
this.$refs.otpComponent.clearDigits();
},
},
});
</script>
Summary:
The OTP component provides a powerful and flexible solution for implementing One-Time Password (OTP) input fields in your Vue.js applications. With its user-friendly features and easy integration, you can enhance your app’s authentication and verification processes. Here’s a summary of its usage:
- Component Structure:
- The
otp
component is at the core of OTP input handling. - It allows you to specify the OTP length with the
:count
prop. - You can easily access its methods and properties using a
ref
(in this example,ref="otpComponent"
).
2. Customization and Styling:
- You have complete control over the styling and structure of the OTP input fields.
- The
otp
component utilizes slots to give you full control over how individual digits are rendered. - The
otp-group
component is used to group the OTP input fields together, and you can apply custom classes and styles. otp-group-input
represents each individual digit input field, and you can customize its appearance and behavior.
3. User Interaction:
- The component handles various user interactions seamlessly.
- It supports keyboard navigation for moving between OTP input fields using the
@next
and@prev
events. - Users can paste OTP codes from their clipboard, and the component validates and populates the fields accordingly.
4. Validation and Error Handling:
- The component provides built-in validation to ensure that users enter a valid OTP.
- It can display error messages for invalid inputs, enhancing user feedback.
5. Clearing OTP Input:
- You can easily clear the OTP input by calling the
clearDigits
method, making it user-friendly.
Advantages and Flexibility:
- The OTP component leverages Vue.js, allowing for seamless integration into your Vue applications.
- It offers a structured and modular approach to OTP input handling, making it easy to understand and maintain.
- Full customization and styling control enable you to match the component with your application’s design.
- Keyboard navigation and paste functionality enhance user experience and convenience.
- Built-in validation and error handling ensure data accuracy and user-friendly error messages.
- The component’s clear method simplifies input clearing, reducing user frustration.
With the OTP component, you can effortlessly enhance your Vue.js application’s authentication and verification processes while maintaining full control over its appearance and behavior.
Below is an illustrative example showcasing the component’s usage, coupled with styling crafted using Tailwind CSS for a visually appealing and responsive design.
Feel free to utilize the enclosed CodeSandbox
to conveniently explore the codebase for your reference.
Note: Stay tuned for my upcoming tutorial series, where we’ll delve into the development of the Headless OTP component. I’ve got you covered whether you’re a Vue 3 enthusiast looking to harness the power of the Composition API or a React aficionado eager to explore OTP component integration with React and Next.js.
If you found this tutorial helpful, please consider giving it a clap. Your support means the world to me. Follow for more insightful content!
Cheers to your coding success! 🥂
Stackademic
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us on Twitter(X), LinkedIn, and YouTube.
- Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.