Input OTP #554
Replies: 6 comments 1 reply
-
I agree this would be cool as built-in thing. In the meantime, if anybody else is in the need, I created this which autofocuses the next input when something is inserted: <?php
use Livewire\Volt\Component;
use Livewire\Attributes\Modelable;
new class extends Component {
#[Modelable]
public string $code = '';
public string $dig_1 = '';
public string $dig_2 = '';
public string $dig_3 = '';
public string $dig_4 = '';
public string $dig_5 = '';
public string $dig_6 = '';
public function updated()
{
$this->code = $this->dig_1 . $this->dig_2 . $this->dig_3 . $this->dig_4 . $this->dig_5 . $this->dig_6;
}
} ?>
<div x-data="{ currentIndex: 0, shiftFocus(index, event) {
if (['1','2','3','4','5','6','7','8','9','0'].includes(event.key)) {
$el.querySelector(`input[name='dig_${index}']`).value = '';
$nextTick(() => {
$wire.$refresh();
$el.querySelector(`input[name='dig_${index + 1}']`)?.focus()
});
return;
}
if (event.key === 'Backspace' && index > 1) {
$nextTick(() => {
$wire.$refresh();
$el.querySelector(`input[name='dig_${index - 1}']`).value = '';
$el.querySelector(`input[name='dig_${index - 1}']`).focus();
});
return;
}
event.preventDefault();
} }" class="flex gap-2">
<flux:input wire:model="dig_1" class:input="text-center" @keydown="shiftFocus(1, $event)" type="text" maxlength="1" name="dig_1" required autofocus />
<flux:input wire:model="dig_2" class:input="text-center" @keydown="shiftFocus(2, $event)" type="text" maxlength="1" name="dig_2" required />
<flux:input wire:model="dig_3" class:input="text-center" @keydown="shiftFocus(3, $event)" type="text" maxlength="1" name="dig_3" required />
<flux:input wire:model="dig_4" class:input="text-center" @keydown="shiftFocus(4, $event)" type="text" maxlength="1" name="dig_4" required />
<flux:input wire:model="dig_5" class:input="text-center" @keydown="shiftFocus(5, $event)" type="text" maxlength="1" name="dig_5" required />
<flux:input wire:model="dig_6" class:input="text-center" @keydown="shiftFocus(6, $event)" type="text" maxlength="1" name="dig_6" required />
</div> |
Beta Was this translation helpful? Give feedback.
-
Here is another one. I added a complete modifer for Here 's a screencapture: Pin.component.mp4@props([
'name' => $attributes->whereStartsWith('wire:model')->first(),
'length' => 6,
'clearable' => null,
'obfuscate' => null,
'viewable' => null,
'iconVariant' => 'mini',
'variant' => 'outline',
'iconTrailing' => null,
'iconLeading' => null,
'loading' => null,
'invalid' => $attributes->whereStartsWith('invalid')->first(),
'valid' => $attributes->whereStartsWith('valid')->first(),
'size' => null,
'icon' => null,
'pattern' => null // aaa-999
])
@php
// There are a few loading scenarios that this covers:
// If `:loading="false"` then never show loading.
// If `:loading="true"` then always show loading.
// If `:loading="foo"` then show loading when `foo` request is happening.
// If `wire:model` then never show loading.
// If `wire:model.live` then show loading when the `wire:model` value request is happening.
$wireModel = $attributes->wire('model');
$wireModelComplete = $wireModel?->hasModifier('complete') ? true : false;
$validModifiers = collect(array_keys($attributes->thatStartWith('valid')->toArray()));
if($validModifiers->isNotEmpty()){
$validBase = Str::of($validModifiers->first());
$validModifiers = $validBase->contains('.') ? $validBase->after('valid.')->split('/\./') : collect([]);
}
$wireTarget = null;
if ($loading !== false) {
if ($loading === true) {
$loading = true;
} elseif ($wireModel?->directive) {
$loading = $wireModel->hasModifier('live') || $wireModel->hasModifier('complete');
$wireTarget = $loading ? $wireModel->value() : null;
} else {
$wireTarget = $loading;
$loading = (bool) $loading;
}
}
$invalid ??= ($name && $errors->has($name));
$iconLeading ??= $icon;
$hasLeadingIcon = (bool) ($iconLeading);
$iconClasses = Flux::classes()
// When using the outline icon variant, we need to size it down to match the default icon sizes...
->add($iconVariant === 'outline' ? 'size-5' : '');
$fieldClasses = Flux::classes()
->add('flex gap-2 items-center rounded-lg disabled:shadow-none dark:shadow-none has-focus:bg-zinc-50 dark:bg-zinc-100/10 p-2');
$inputClasses = Flux::classes()
->add('[&_input]:text-center')
->add(match($size){
'xs' => 'max-w-6 [&_input]:pe-1! [&_input]:ps-1!',
'sm' => 'max-w-8 [&_input]:pe-1! [&_input]:ps-1!',
'md' => 'max-w-10 [&_input]:pe-1! [&_input]:ps-1! [&_input]:text-lg',
'lg' => 'max-w-12 [&_input]:h-12! [&_input]:pe-1! [&_input]:ps-1! [&_input]:text-xl',
})
->add(match($size){
'xs' => $obfuscate ? '[&_input]:text-md' : '',
'sm' => $obfuscate ? '[&_input]:text-lg' : '',
'md' => $obfuscate ? '[&_input]:text-xl' : '',
'lg' => $obfuscate ? '[&_input]:text-3xl' : '',
});
$pattern = $pattern ? str_split($pattern) : null;
@endphp
<flux:with-field :$name :attributes="$attributes->except(['wire:model', 'wire:model.live', 'wire:model.complete'])">
<div {{ $attributes->class($fieldClasses) }}
x-data="{ code: '', digits : [], length: {{$length}} }"
x-on:click="if(!$el.contains(document.activeElement)) { $el.querySelector('input').focus() }"
x-modelable="code"
x-init="$watch('digits', value => {
code = value.join('');
@if($wireModelComplete)
if (value.length === length && value.every(d => d !== '')) {
$wire.set('{{ $name }}', code);
}
@if($valid)
if( $wire.get('{{ $valid }}') == true ){
$wire.set('{{ $name }}', code);
}
@endif
@endif
} );
@if($valid)
$wire.watch('{{ $valid }}', value => value ? ($el.dataset.fluxCodeValid = value) : (delete $el.dataset.fluxCodeValid));
@endif
@if($invalid)
$wire.watch('{{ $invalid }}', value => value ? ($el.dataset.fluxCodeInvalid = value) : (delete $el.dataset.fluxCodeInvalid));
@endif
"
wire:ignore
data-flux-code-input
>
<?php if (is_string($iconLeading)): ?>
<div class="pointer-events-none flex items-center justify-center text-xs text-zinc-400/75 ps-3 start-0">
<flux:icon :icon="$iconLeading" :variant="$iconVariant" :class="$iconClasses" />
</div>
<?php elseif ($iconLeading): ?>
<div {{ $iconLeading->attributes->class('flex items-center justify-center text-xs text-zinc-400/75 ps-3 start-0') }}>
{{ $iconLeading }}
</div>
<?php endif; ?>
@php $j = 0; $offset = 0; @endphp
@for($i = 0; $i < $length; $i++)
@php
$j = $i + $offset;
$inputProps = new \Illuminate\View\ComponentAttributeBag();
if($obfuscate){
$inputProps = $inputProps->merge([
"type" => "password"
]);
}
if($i < 5){
$inputProps = $inputProps->merge([
"x-on:keyup" => "if(\$el.value.length == 1 && \$event.key !== 'ArrowLeft' && \$event.key !== 'ArrowRight' && \$event.key !== 'Delete' ){ siblings[index + 1].select() }",
"x-on:keyup.right" => "siblings[index + 1].select()"
]);
}
if($i > 0){
$inputProps = $inputProps->merge([
"x-on:keyup.backspace" => "\$el.value = ''; siblings[index - 1].select()",
"x-on:keyup.left" => "siblings[index - 1].select();"
]);
}
@endphp
@if($pattern && !preg_match('/[0-9A-Za-z]{1}/', $pattern[$j]) )
<div>{{$pattern[$j]}} </div>
@php $offset++ @endphp
@endif
<flux:input :attributes="$inputProps->class($inputClasses)"
x-model="digits[{{ $i }}]"
:$size
autocomplete="new-password"
maxlength="1"
x-on:click="$el.select()"
:tabindex="$i === 0 ? 0 : -1"
x-on:paste.prevent="digits = $event.clipboardData.getData('text').slice(0, 6).split(''); siblings[siblings.length - 1].focus()"
x-on:keyup.delete="digits.splice({{ $i }}, 1);"
data-flux-code-input-item
loading="false"
x-data="{ siblings : [], index: null }"
x-init="
siblings = Array.from($el.closest('[data-flux-code-input]').querySelectorAll('input'));
index = siblings.indexOf($el);
"
/>
@endfor
<?php if ($valid && $validModifiers->contains('show')): ?>
<flux:icon name="check" wire:loading.remove :wire:show="$valid" x-cloak />
<?php endif; ?>
<?php if ($clearable): ?>
<div x-show="{'opacity-0' : !digits.length }" class="contents">
<flux:button variant="subtle" wire:loading.remove :wire:target="$wireTarget" icon="tabler.x" x-on:click="digits = []"
:size="$size === 'sm' || $size === 'xs' ? 'xs' : 'sm'"
/>
</div>
<?php endif; ?>
<?php if ($viewable): ?>
<jolt:key.viewable inset="left right" :$size />
<?php endif; ?>
<?php if ($loading): ?>
<flux:icon name="loading" :variant="$iconVariant" :class="$iconClasses" wire:loading :wire:target="$wireTarget" />
<?php endif; ?>
<?php if (is_string($iconTrailing)): ?>
<?php
$trailingIconClasses = clone $iconClasses;
$trailingIconClasses->add('pointer-events-none text-zinc-400/75');
?>
<flux:icon :icon="$iconTrailing" :variant="$iconVariant" :class="$trailingIconClasses" />
<?php elseif ($iconTrailing): ?>
{{ $iconTrailing }}
<?php endif; ?>
</div>
<flux:error :$name />
</flux:with-field> |
Beta Was this translation helpful? Give feedback.
-
yes please |
Beta Was this translation helpful? Give feedback.
-
+1 for this |
Beta Was this translation helpful? Give feedback.
-
Yes. I'm currently migrating a page from Vue to Flux UI, and this is EXACTLY what I was looking for. 👍 |
Beta Was this translation helpful? Give feedback.
-
+1 this |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Would be nice to have OTP, PIN input some day.
Beta Was this translation helpful? Give feedback.
All reactions