In this guide, we’ll walk through the process of creating, registering, and using a custom input. Specifically, we’re going create a "one-time password" input ("OTP" for short). OTPs are commonly used for two-factor authentication when a user is required to type in a code sent via SMS or authenticator app. Let’s get started!
This guide assumes you are using a standard Vue 3 build tool like Vite, Nuxt 3, or Vue CLI that will allow you to import .vue single file components.
To get started, let's create our input’s component file. We'll call it OneTimePassword.vue:
<script setup>
const props = defineProps({
context: Object,
})
</script>
<template>
<div>More to come here...</div>
</template>
FormKit provides a lot of input features out-of-the-box that we're going to want to preserve — like labels, help text, and showing error messages. All we really want to modify is the input section of our input. We can preserve these standard FormKit features by using the createInput utility function from the @formkit/vue package.
As we build out our input, we’ll want to visualize its progress, so let’s create a sample form to:
OneTimePassword.vuecreateInput()type prop of a <FormKit> component.We’ll call this sample form Register.vue:
Excellent! Now we can iterate on our OneTimePassword.vue file and see the results. One of the first things to notice is how our input already supports labels, help text, validation, and other universal FormKit props. Those features come courtesy of createInput().
Also, notice that <pre> tag in the above example? It is outputting the current state of the form’s data. We'll use this visualize the value of our custom input. Since our input currently has no value, it does not appear in the form’s data. Time to change that!
Let’s open up OneTimePassword.vue again and change our <div> to an <input> tag. We’ll start with a single text input, and work our way up from there. But how do we actually set and display the value of our custom input?
All custom inputs are passed the almighty context object as the context prop. In order for our input to set its value, it needs to call context.node.input(value). To properly display the value of our input, we should set the input’s :value attribute to context._value.
Our little baby input is all grown up! It might not look pretty, but it now reads and writes values. As proof, try setting the initial value of the form’s values object to { two_factor_code: '12345' } and you'll see the input gets auto-populated with the value.
Ok, now that we understand how to create an input, how to use it, and how to read and write values — let’s tackle the actual "business logic" of our one-time password input. Here are our requirements:
<input> tag.For our first requirement, we need n <input> tags. Perhaps it would be best to expose the number of digits as a prop. To do that, we need to inform our createInput function that we want to accept a new prop:
createInput(OneTimePassword, {
props: ['digits'],
})
We now have access to context.digits. Back in OneTimePassword.vue, let's use that to output the correct number of <input> tags.
OK — we have multiple inputs! Our first requirement is complete:
<input> tag.We’ve added a touch of CSS in the above example, but in general we’re not going to dive into styling in this guide. It is recommended to use context.classes.yourKey as the class name of elements in your input.
Notice in the above example that when you type into one input all the other inputs are synced to the same value? Kinda neat, but not what we want. This is because we are still using the same input handler and :value. Here's a plan to improve our input:
focus() on the next input.digits, we update the value of the input by calling context.node.input().Great! This is starting to work like we expect. Let’s check our requirements again:
<input> tag.Looks like we only have one thing left to do — copy & paste support. Fortunately, browsers have a paste event. To ensure our user experience is top notch, we’ll make an assumption: if a user is copy/pasting they are trying to copy and paste the entire code. Not a single digit of the code. Seems reasonable.
All we need to do is capture the copy/paste event on any of our input tags, get the text being pasted, and set the tmp value to that string of digits. Let’s whip up another event handler:
handlePaste(e) {
const paste = e.clipboardData.getData('text')
if (typeof paste === 'string') {
// If it is the right length, paste it.
this.tmp = paste.substr(0, this.max)
const inputs = e.target.parentElement.querySelectorAll('input')
// Focus on the last character
inputs.item(this.tmp.length - 1).focus()
}
}
Our requirements are all complete!
Now that we've worked up an excellent input, let’s register it with our application so we can use it anywhere by just using the string otp. Open up your Vue application’s main file (where app.use(formKit) is). We’ll just add to it:
import { createApp } from 'Vue'
import App from 'App.vue'
import OneTimePassword from './OneTimePassword.vue'
import { plugin, defaultConfig, createInput } from '@formkit/vue'
const app = createApp(App)
app.use(
plugin,
defaultConfig({
inputs: {
otp: createInput(OneTimePassword, {
props: ['digits'],
}),
},
})
)
app.mount('#app')
Done! Now you can use your input anywhere in your application:
<FormKit type="otp" digits="4" />
Our one-time password input is working great! Here are some ideas for additional features we could flesh out even further:
Hopefully this guide helped you understand how custom inputs are declared, written, and registered. If you want to dive in deeper, try reading about the core internals of FormKit and creating custom inputs!