How to Handle Edge Cases When OTP Value Is Updated Externally in vue3-otp-input
The vue3-otp-input component uses a Vue watcher on the value prop that synchronizes internal state only when the incoming string matches numInputs length or when the internal OTP array is empty, preventing partial overwrites during user input.
The vue3-otp-input library provides a robust one-time password input component for Vue 3 applications. When building forms that require programmatic OTP updates—such as auto-fill from SMS, API-generated codes, or user-initiated resets—understanding how the component handles external value changes is critical to prevent UI desynchronization.
How External Value Synchronization Works in vue3-otp-input
The synchronization logic resides in src/components/vue3-otp-input.vue. The component watches the value prop (bound via v-model) and conditionally updates its internal reactive otp array:
watch(
() => props.value,
(val) => {
// fix issue: https://github.com/ejirocodes/vue3-otp-input/issues/34
if (val.length === props.numInputs || otp.value.length === 0) {
const fill = val.split("");
otp.value = fill;
}
},
{ immediate: true }
);
This watcher executes with { immediate: true }, ensuring the component initializes correctly when mounted with a pre-filled value. The conditional check prevents the external value from overwriting partially entered user input unless the new value is complete (matches numInputs) or the component is in its initial empty state.
Edge Cases and How the Component Handles Them
The watcher implementation specifically guards against four common edge cases encountered when updating OTP values externally:
External Update with a Complete OTP
When the parent component sets the v-model value to a complete OTP string (e.g., "1234" for a 4-input field), the watcher detects that val.length === props.numInputs and immediately repopulates the internal otp array. The UI updates instantly to display the new values across all input fields.
External Update with an Empty String (Form Reset)
When resetting a form programmatically by setting the bound value to an empty string, the condition otp.value.length === 0 evaluates to true (assuming the component was previously filled). This allows the watcher to clear the internal array, effectively resetting all input fields to empty state even though the new value length (0) does not match numInputs.
Partial External Update While User Is Typing
If the parent component attempts to update the value while the user is actively entering digits (e.g., setting the value to "12" when numInputs is 4), the watcher blocks the update. Since val.length !== props.numInputs and otp.value.length !== 0, the conditional fails, preserving the user's current input state and preventing disruptive overwrites.
Programmatic Fill via Exposed Methods
The component exposes fillInput(value: string) and clearInput() methods via template refs. These methods use the same internal logic as the watcher to update otp.value and emit "update:value" and "on-complete" events. When calling fillInput("5678"), the component behaves identically to an external value update, ensuring consistent validation and event emission.
Practical Implementation Examples
Basic Two-Way Binding with v-model
Implement standard two-way binding to allow external updates to flow into the component:
<template>
<Vue3OtpInput v-model="otpCode" :numInputs="6" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import Vue3OtpInput from "vue3-otp-input";
const otpCode = ref("");
// External update example:
// otpCode.value = "987654" // Automatically updates the input fields
</script>
Updating OTP After an Async API Call
Handle OTP generation from backend services by updating the bound value after the promise resolves:
<template>
<Vue3OtpInput v-model="otpCode" :numInputs="4" />
<button @click="fetchOtp">Refresh OTP</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Vue3OtpInput from "vue3-otp-input";
const otpCode = ref("");
async function fetchOtp() {
const response = await fetch("/api/generate-otp");
const { otp } = await response.json();
otpCode.value = otp; // Triggers watcher → UI updates automatically
}
</script>
Using Exposed clearInput and fillInput Methods
For direct programmatic control without modifying the bound value externally, use template refs to access component methods:
<template>
<Vue3OtpInput
ref="otpRef"
v-model="otpCode"
:numInputs="4"
@on-complete="handleComplete"
/>
<button @click="reset">Clear</button>
<button @click="prefill">Prefill 2468</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Vue3OtpInput from "vue3-otp-input";
const otpRef = ref<InstanceType<typeof Vue3OtpInput> | null>(null);
const otpCode = ref("");
function reset() {
otpRef.value?.clearInput(); // Clears internal state and emits empty value
}
function prefill() {
otpRef.value?.fillInput("2468"); // Fills inputs and triggers on-complete
}
function handleComplete(value: string) {
console.log("OTP entered:", value);
}
</script>
Key Source Files
Understanding the architecture requires familiarity with these specific files in the ejirocodes/vue3-otp-input repository:
| File | Role | Location |
|---|---|---|
src/components/vue3-otp-input.vue |
Main component containing the value prop watcher and synchronization logic |
View Source |
src/components/single-otp-input.vue |
Individual input field component handling keyboard navigation, paste events, and focus management | View Source |
src/index.ts |
Public API entry point exporting the component for consumer applications | View Source |
README.md |
Official documentation with basic usage examples and configuration options | View Source |
Summary
- Conditional Synchronization: The watcher in
src/components/vue3-otp-input.vueonly updates internal state when the external value length matchesnumInputsor when the component is empty, preventing disruption of active user input. - Complete OTP Replacement: External updates containing full OTP strings automatically populate all input fields and trigger the
on-completeevent. - Safe Form Resets: Setting the bound value to an empty string clears the component without requiring the empty string to match
numInputslength. - Programmatic Control: The exposed
fillInput()andclearInput()methods provide imperative APIs that use the same synchronization logic as the reactive watcher. - Event Consistency: All update paths emit
"update:value"to maintain v-model contract and"on-complete"when the input reaches the required length.
Frequently Asked Questions
What happens if the external value is shorter than numInputs?
The component preserves the existing user input. The watcher checks if (val.length === props.numInputs || otp.value.length === 0) before updating. Since the partial value length does not equal numInputs and the internal otp array is not empty, the condition fails and the UI remains unchanged, preventing disruptive overwrites during user typing.
Can I clear the OTP input programmatically?
Yes. You can either set the bound v-model value to an empty string (otpCode.value = "") or call the exposed clearInput() method via a template ref. Both approaches trigger the watcher or internal clearing logic, which sets the internal otp array to an empty array and emits "update:value" with an empty string, effectively resetting all input fields.
How do I prevent the watcher from overwriting user input?
The component automatically guards against this. The watcher only synchronizes the internal state when the incoming value length exactly matches numInputs or when the component is in its initial empty state. This design means that partial external updates (such as those triggered by debounced API calls while the user is still typing) will not overwrite the current OTP input, ensuring a smooth user experience.
Is it safe to update the v-model value during the on-complete event?
Yes, but with caution. Updating the v-model value during @on-complete will trigger the watcher again. If you set it to a new valid OTP string of the same length, the component will re-process it (though the UI will remain visually identical). If you clear it immediately, the user might see a flash of completion before the reset. For most use cases, it is better to handle the completed OTP in the parent component and only modify the v-model value when you need to reset or replace the OTP entirely.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →