Created
May 6, 2024 13:31
-
-
Save bsthomsen/168edad257a979f8ddaebbab7018ca53 to your computer and use it in GitHub Desktop.
Simple Split input for OTP in Vue3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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