Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PinInput] Implement component #1123

Closed
aloky opened this issue Dec 18, 2023 · 7 comments
Closed

[PinInput] Implement component #1123

aloky opened this issue Dec 18, 2023 · 7 comments
Labels

Comments

@aloky
Copy link

aloky commented Dec 18, 2023

https://www.radix-vue.com/components/pin-input.html

@aloky aloky added the enhancement New feature or request label Dec 18, 2023
@ddahan
Copy link

ddahan commented Dec 30, 2023

I built this kind of component for my own project which uses Nuxt UI:
image

It has theses props:

length: number; // choose the number of digits
autoFocus: boolean;
autoValidation: boolean; // trigger a method automatically when all fields are filled
hide: boolean; // hide digits (like a password)

And it handles:

  • auto focusing to next input when typing
  • auto selecting an input if focused
  • pasting the code directly

@ddahan
Copy link

ddahan commented Jan 2, 2024

@benjamincanac are you interesting in merging this component if I make a PR?
Any special recommendations?
Thanks.

Copy link
Member

Not sure it makes sense to work on this now, after the radix-vue migration we'll be able to use this: https://www.radix-vue.com/components/pin-input.html

@benjamincanac benjamincanac added the v3 #1289 label Jan 5, 2024
@moshetanzer moshetanzer added feature and removed enhancement New feature or request labels Mar 28, 2024
@JoelHutchinson
Copy link

@ddahan Do you have the source code for this component? I would love to try it out in my own Nuxt project!

@sebastiandotdev
Copy link

You may want to test with https://vue-input-otp.vercel.app/.

@emwadde
Copy link

emwadde commented Jun 21, 2024

@ddahan is it possible to share this component.

@benjamincanac benjamincanac changed the title OTP Input [OTP Input] Implement component Sep 26, 2024
@benjamincanac benjamincanac changed the title [OTP Input] Implement component [PINInput] Implement component Oct 17, 2024
@benjamincanac benjamincanac changed the title [PINInput] Implement component [PinInput] Implement component Nov 4, 2024
@ddahan
Copy link

ddahan commented Nov 12, 2024

@ddahan is it possible to share this component.

@emwadde @marcbejar @pierresigwalt @JoelHutchinson
Sorry I did not receive notifications for this one. Here is the code if it can still help:

<template>
  <UFormGroup>
    <div class="flex gap-2.5">
      <div v-for="i in Array.from({ length: length }, (_, i) => i)" :key="i" class="w-12">
        <div class="flex gap-1">
          <UInput
            :ref="(el) => (uInputComponents[i] = el)"
            size="xl"
            maxlength="1"
            :type="hide ? 'password' : 'text'"
            :ui="{ base: 'text-center', size: { xl: 'text-lg' } }"
            :autofocus="i === 0 && autoFocus"
            v-model="otp[i]"
            @input="autoTab(i)"
            @focus="selectTextOnFocus(i)"
            @paste="handlePaste($event)"
            @keydown.delete="handleBackspace(i, $event)"
          />
        </div>
      </div>
    </div>
  </UFormGroup>
</template>
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    length?: number;
    autoFocus?: boolean;
    autoValidation?: boolean; // only applied on pasting for better UX
    hide?: boolean;
  }>(),
  { length: 6, autoFocus: true, autoValidation: true, hide: false }
);

const emit = defineEmits(["update:modelValue", "otpAutoSubmit"]);

// State
const otp: Ref<string[]> = ref(Array(props.length).fill(""));
const uInputComponents: Ref<any[]> = ref(Array(props.length).fill(null));

// Send code string to the parent component
const code: Ref<string> = computed(() => Object.values(otp.value).join(""));
watch(code, (newCode) => {
  emit("update:modelValue", newCode);
});

const autoTab = async (i: number) => {
  /* auto-focus next input after typing */

  await nextTick(); // wait until Vue completes the DOM updates.
  if (i < props.length - 1 && otp.value[i]) {
    const nextInput = uInputComponents.value[i + 1].input;
    if (nextInput) {
      nextInput.focus();
    }
  }
};

const selectTextOnFocus = (i: number) => {
  /* auto-select input content when focusing it */

  const input = uInputComponents.value[i].input;
  input.select();
};

const handlePaste = async (event: ClipboardEvent) => {
  /* handle the case where the user paste the code (+ auto validation) */

  event.preventDefault();
  const pastedText = event.clipboardData?.getData("text").trim();
  // Paste is disabled if the text is not the right size
  if (pastedText?.length === props.length) {
    otp.value = [...pastedText].map((char, i) => char || "");
    uInputComponents.value[props.length - 1].input.focus();

    const allFieldsFilled = otp.value.every((x) => x.length === 1);
    if (allFieldsFilled && props.autoValidation) {
      await nextTick(); // prevent otpAutoSubmit to be called before code update
      emit("otpAutoSubmit");
    }
  }
};

const handleBackspace = async (i: number, event: KeyboardEvent) => {
  /* Inteligent erasing that goes to previous field after deletion */

  event.preventDefault(); // avoid a race condition with the focus below
  otp.value[i] = ""; // deletion
  if (i > 0) {
    uInputComponents.value[i - 1].input.focus();
  }
};
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants