Skip to content

Instantly share code, notes, and snippets.

@bsthomsen
Created May 6, 2024 13:31
Show Gist options
  • Save bsthomsen/168edad257a979f8ddaebbab7018ca53 to your computer and use it in GitHub Desktop.
Save bsthomsen/168edad257a979f8ddaebbab7018ca53 to your computer and use it in GitHub Desktop.
Simple Split input for OTP in Vue3
<template>
<form class="mb-10 flex w-full max-w-[400px] flex-col gap-6">
<SplitInput
:fields="6"
ref="splitInput"
@complete="verifyCode"
@change="(value) => (code = value)"
/>
</form>
<div
class=""
v-if="error || isLoading"
>
<p v-if="error">{{ error }} <strong @click="resendOtp">Resend code</strong></p>
<p v-else>
<span v-if="isLoading">Verifying...</span>
</p>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const email = ref<string>(route.query.email ? (route.query.email as string) : '')
const splitInput = ref<any>(null)
const code = ref<string>('')
const error = ref<string>('')
const isLoading = ref<boolean>(false)
const resendOtp = async () => {
await auth.signInWithOtp({
email: email.value,
options: {
shouldCreateUser: true,
},
})
}
const verifyCode = async () => {
isLoading.value = true
const res = await auth.verifyOtp({
email: email.value,
type: 'email',
token: code.value,
})
if (res.error) {
error.value = res.error.message
splitInput.value?.reset()
isLoading.value = false
return
}
if (res.data) {
// Success
// navigateTo()
}
}
</script>
<template>
<div class="verification-code">
<div class="verification-code-inputs flex gap-2">
<template
v-for="(v, index) in values"
:key="index"
>
<input
class="focus-within:border-primary focus-within:outline-primary w-full rounded border text-center text-xl font-semibold leading-[80px] focus-within:outline-1"
:autoFocus="autoFocus && index === autoFocusIndex"
:data-id="index"
:value="v"
:ref="
(el: any) => {
if (el) inputs[index] = el
}
"
@paste="onPaste(index, $event)"
@input="onValueChange"
@keydown="onKeyDown"
maxlength="1"
/>
</template>
</div>
<input
v-model="value"
:maxlength="fields"
v-show="false"
/>
</div>
</template>
<script setup lang="ts">
import { ref, toRef, onBeforeUpdate } from 'vue'
const props = defineProps({
className: String,
fields: {
type: Number,
default: 3,
},
})
const emit = defineEmits(['change', 'complete'])
const KEY = {
BACKSPACE: 'Backspace',
DELETE: 'Delete',
ARROW_LEFT: 'ArrowLeft',
ARROW_UP: 'ArrowUp',
ARROW_RIGHT: 'ArrowRight',
ARROW_DOWN: 'ArrowDown',
}
const value = ref('')
const values = ref<any[]>(new Array(props.fields).fill(''))
const inputs = ref<any[]>([])
const fields = toRef(props, 'fields')
const autoFocusIndex = ref(0)
const autoFocus = true
watch(
values,
(newValue, oldValue) => {
value.value = newValue.join('')
triggerChange()
},
{ deep: true }
)
const onValueChange = (e: Event) => {
const el = e.target as HTMLInputElement
const index = parseInt(el.dataset.id!)
values.value[index] = el.value
focusInput(index + 1)
}
const focusInput = (index: number) => {
if (index < 0) {
return
}
if (index >= fields.value) {
inputs.value[fields.value - 1].focus()
return
}
inputs.value[index].focus()
}
const onKeyDown = (e: KeyboardEvent) => {
const el = e.target as HTMLInputElement
const index = parseInt(el.dataset.id!)
switch (e.key) {
case KEY.DELETE: {
e.preventDefault()
values.value[index] = ''
break
}
case KEY.BACKSPACE: {
e.preventDefault()
if (values.value[index] == '') {
focusInput(index - 1)
} else {
values.value[index] = ''
}
break
}
case KEY.ARROW_LEFT:
e.preventDefault()
focusInput(index - 1)
break
case KEY.ARROW_RIGHT:
e.preventDefault()
focusInput(index + 1)
break
}
}
const onPaste = (index: number, e: ClipboardEvent) => {
e.preventDefault()
const clipboardData = e.clipboardData || window?.clipboardData
const pastedData = clipboardData!.getData('Text')
const parsedPastedData = pastedData.trim().split('')
parsedPastedData.forEach((char: any, i: number) => {
if (index + i < fields.value) {
values.value[index + i] = char
}
})
focusInput(parsedPastedData.length)
}
const triggerChange = () => {
const parsedValue = value.value
emit('change', parsedValue)
if (parsedValue.length >= fields.value) {
inputs.value[fields.value - 1].blur()
emit('complete')
}
}
const reset = () => {
values.value = new Array(fields.value).fill('')
autoFocusIndex.value = 0
focusInput(0)
}
defineExpose({
reset,
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment