DEV Community

Cover image for How to Create OTP Input with Resend CountDown Timer in Livewire using Alphinejs and TailwindCss
Andy
Andy

Posted on

How to Create OTP Input with Resend CountDown Timer in Livewire using Alphinejs and TailwindCss

If you're working with Laravel Livewire and need to create an OTP Input. Here is how to create a OTP input with a count down timer that has a resend button using Alphine.js and TailwindCss.

Design the Input Field

The code below creates a single input field for entering a single number. The number of fields depends on the length of the OTP code. The next part will show you how to display the number of input as required.

<form class="fi-form grid gap-y-6">
<input
type="tel"
maxlength="1"
class="border border-gray-500 w-10 h-10 text-center"
/>
@error('code')
  <p data-validation-error="" class="fi-fo-field-wrp-error-message text-sm text-danger-600 dark:text-danger-400">
   {{ $message }}
  </p>
@enderror
<button 
    style="--c-400:var(--primary-400);--c-500:var(--primary-500);--c-600:var(--primary-600);"
    class="fi-btn relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus-visible:ring-2 rounded-lg fi-color-custom fi-btn-color-primary fi-size-md fi-btn-size-md gap-1.5 px-3 py-2 text-sm inline-grid shadow-sm bg-custom-600 text-white hover:bg-custom-500 focus-visible:ring-custom-500/50 dark:bg-custom-500 dark:hover:bg-custom-400 dark:focus-visible:ring-custom-400/50 fi-ac-action fi-ac-btn-action"
   type="submit"
>Verify</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Display Specific Number of Input fields

Here we use javascript (alphinejs) to specify number of boxes to display and use for loop to display them.

<form 
  class="fi-form grid gap-y-6"
  wire:submit="verify"
  x-data="otpForm()"
>
    <div style="--cols-default: repeat(1, minmax(0, 1fr));" class="grid grid-cols-[--cols-default] fi-fo-component-ctn gap-6">
        <div style="--col-span-default: span 1 / span 1;" class="col-[--col-span-default]">
            <div data-field-wrapper="" class="fi-fo-field-wrp">

            <div class="grid gap-y-2">
                <input 
                    type="hidden" 
                    id="otp" 
                    wire:model="code"
                    required="required"
                    x-model="otp"
                >
            </div>
            <div class="grid gap-y-2">
                <div class="">
                    <div class="py-6 px-0 w-80 mx-auto text-center my-6">
                        <div class="flex justify-between">
                            <template x-for="(input, index) in length" :key="index">
                                <input
                                    type="tel"
                                    maxlength="1"
                                    class="border border-gray-500 w-10 h-10 text-center"
                                    :x-ref="index"
                                    x-on:input="handleInput($event)"
                                    x-on:paste="handlePaste($event)"
                                    x-on:keyup="handleDelete($event)"

                                />
                            </template>
                        </div>
                    </div>
                </div>

                @error('code')
                <p data-validation-error="" class="fi-fo-field-wrp-error-message text-sm text-danger-600 dark:text-danger-400">
                {{ $message }}
                </p>
                @enderror

            </div>
            <div class="grid gap-y-2">
                <div class="fi-form-actions">
                    <div class="fi-ac gap-3 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))]">
                        <button 
                            style="--c-400:var(--primary-400);--c-500:var(--primary-500);--c-600:var(--primary-600);"
                            class="fi-btn relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus-visible:ring-2 rounded-lg fi-color-custom fi-btn-color-primary fi-size-md fi-btn-size-md gap-1.5 px-3 py-2 text-sm inline-grid shadow-sm bg-custom-600 text-white hover:bg-custom-500 focus-visible:ring-custom-500/50 dark:bg-custom-500 dark:hover:bg-custom-400 dark:focus-visible:ring-custom-400/50 fi-ac-action fi-ac-btn-action"
                            type="submit"
                            x-on:click="$wire.code=document.getElementById('otp').value"
                        >
                            Verify
                        </button>
                    </div>
                </div>
            </div>

            </div>
        </div>

    </div> 
    </div>  
    </form>

    @push('scripts')
    <script>
        function otpForm() {
            return {
                    length: 7,
                    otp: '',

                    handleInput(e) {

                        const input = e.target;

                        this.otp = Array.from(Array(this.length), (element, i) => {
                            let ref = document.querySelector('[x-ref="' + i + '"]');
                            return ref.value || '';
                        }).join('');

                        if (input.nextElementSibling && input.value) {
                            input.nextElementSibling.focus();
                            input.nextElementSibling.select();
                        }
                    },

                    handlePaste(e) {
                        const paste = e.clipboardData.getData('text');
                        this.otp = paste;

                        const inputs = Array.from(Array(this.length));

                        inputs.forEach((element, i) => {
                            let ref = document.querySelector('[x-ref="' + i + '"]');
                            ref.value = paste[i] || '';
                        });
                    },

                    handleDelete(e) {
                        let key = e.keyCode || e.charCode;
                        if(key == 8 || key == 46) {
                            currentRef = e.target.getAttribute('x-ref');
                            const previous = parseInt(currentRef) - 1;

                            let ref = document.querySelector('[x-ref="' + previous + '"]');
                            ref && ref.focus();
                        }
                    },
            }
        }
</script>
@endpush
Enter fullscreen mode Exit fullscreen mode

We passed the javascript function to the form as x-data. This will allow us access all the variables and functions inside the otpForm() function inside the form element.
The OtpForm function contains
the length variable, that will show the number of boxes to display.
handleInput function concatenates otp codes entered in all the boxes and stores it to the otp variable.
handlePaste function helps to transfer copied otp content from clipboard to the boxes.
handleDelete function helps handle deleting of contents of the otp boxes and refocuses the cursor.

We used :x-ref to uniquely identify each input box and then used document.querySelector to retrieve the value of each of the boxes based on their position.

We also added a hidden field to store the otp code before submitting to our model.

<input 
  type="hidden" 
  id="otp" 
  wire:model="code"
  required="required"
  x-model="otp"
>
Enter fullscreen mode Exit fullscreen mode

Add Resend Button with CountDown Timer

Let's add a resend button with countdown timer.

            <div class="grid gap-y-2 text-center" x-data="otpSend(80)" x-init="init()">
            <template  x-if="getTime() <= 0">
            <form wire:submit="resendOtp">
                <button
                    type="submit"
                >
                    Resend OTP
                    <div wire:loading>
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
                                <circle fill="#FF156D" stroke="#FF156D" stroke-width="15" r="15" cx="40" cy="100">
                                    <animate attributeName="opacity" calcMode="spline" dur="2" values="1;0;1;" keySplines=".5 0 .5 1;.5 0 .5 1" repeatCount="indefinite" begin="-.4">
                                    </animate>
                                </circle>
                                <circle fill="#FF156D" stroke="#FF156D" stroke-width="15" r="15" cx="100" cy="100">
                                    <animate attributeName="opacity" calcMode="spline" dur="2" values="1;0;1;" keySplines=".5 0 .5 1;.5 0 .5 1" repeatCount="indefinite" begin="-.2">
                                    </animate>
                                </circle>
                                    <circle fill="#FF156D" stroke="#FF156D" stroke-width="15" r="15" cx="160" cy="100">
                                        <animate attributeName="opacity" calcMode="spline" dur="2" values="1;0;1;" keySplines=".5 0 .5 1;.5 0 .5 1" repeatCount="indefinite" begin="0">
                                        </animate>
                                </circle>
                            </svg>

                            </div>
                </button>
                <input type="hidden" wire:model="logid">
            </form>
            </template>
            <template x-if="getTime() > 0">
            <small>
                Resend OTP in 
                <span x-text="formatTime(getTime())"></span>
            </small>
            </template>
            </div>

<script>
function otpSend(num) {
            const milliseconds = num * 1000 //60 seconds
            const currentDate = Date.now() + milliseconds
            var countDownTime = new Date(currentDate).getTime()
            let interval;

            return {
                countDown: milliseconds,
                countDownTimer: new Date(currentDate).getTime(),
                intervalID: null,
                init(){
                    if (!this.intervalID ) {
                        this.intervalID = setInterval(() => {
                            this.countDown = this.countDownTimer - new Date().getTime();
                        }, 1000);
                    }

                },
                getTime(){
                    if(this.countDown < 0){
                        this.clearTimer()
                    }
                    return this.countDown;
                },
                formatTime(num){

                    var date = new Date(num);
                    return new Date(this.countDown).toLocaleTimeString(navigator.language, {
                        minute: '2-digit',
                        second:'2-digit'
                    });

                },
                clearTimer() {
                    clearInterval(this.intervalID);
                }

            }
        }

        </script>
Enter fullscreen mode Exit fullscreen mode

The otpSend(num) function uses a countdown timer to only display the resend button after specified time num in seconds.

You can see the full source code on github here

Top comments (0)