Created
October 25, 2024 13:09
-
-
Save devhammed/f6b083be1ae5b7f69300f48cb9243207 to your computer and use it in GitHub Desktop.
Filament PIN Input
This file contains 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
<x-dynamic-component | |
:component="$getFieldWrapperView()" | |
:field="$field" | |
> | |
<fieldset | |
x-data="{ | |
length: @js($getLength()), | |
submit: @js($getSubmit()), | |
validation: {{ $getRegexPattern() }}, | |
value: $wire.{{ $applyStateBindingModifiers("\$entangle('{$getStatePath()}')") }}, | |
init() { | |
this.$nextTick(() => { | |
const firstInput = this.$refs[0]; | |
if (firstInput) { | |
firstInput.focus(); | |
} | |
}); | |
}, | |
updateValue() { | |
const values = []; | |
for (var i = 0; i < this.length; i++) { | |
const ref = this.$refs[i]; | |
if (ref) { | |
values.push(ref.value || ' '); | |
} | |
} | |
this.value = values.join(''); | |
if (this.value.trim().length === this.length && this.submit) { | |
const form = document.querySelector('#' + this.submit); | |
if (!form) { | |
return; | |
} | |
const submit = form.querySelector('button[type=\'submit\']'); | |
if (!submit) { | |
return; | |
} | |
submit.click(); | |
} | |
}, | |
handleInput(pin) { | |
const value = pin.value.match(this.validation); | |
if (!value || !value.length) { | |
pin.value = ''; | |
this.updateValue(); | |
return; | |
} | |
pin.value = value; | |
this.updateValue(); | |
this.focusNextRef(pin.getAttribute('x-ref')); | |
}, | |
handlePaste(event) { | |
const text = event.clipboardData.getData('Text').match(this.validation); | |
if (!text || !text.length) { | |
return; | |
} | |
// Get the starting input, then slice the text to match the remaining inputs | |
const pastedFrom = parseInt(event.target.getAttribute('x-ref'), 10); | |
const remainingInputs = this.length - pastedFrom; | |
const value = text.slice(0, remainingInputs).join(''); | |
// Figure out what inputs we need to update | |
const inputsToUpdate = []; | |
for (var i = 0; i < remainingInputs; i++) { | |
inputsToUpdate.push(pastedFrom + i); | |
} | |
const valuesToUpdate = inputsToUpdate.slice(0, value.length); | |
// Update the values | |
for (let j = 0, len = valuesToUpdate.length; j < len; j++) { | |
this.$refs[valuesToUpdate[j]].value = value[j]; | |
} | |
// Focus the last input we updated | |
this.focusNextRef(valuesToUpdate[valuesToUpdate.length - 1]); | |
this.updateValue(); | |
}, | |
handleBackspace(event) { | |
if (event.target.value) { | |
return; | |
} | |
this.focusPreviousRef(event.target.getAttribute('x-ref')); | |
}, | |
focusNextRef(current) { | |
const next = parseInt(current, 10) + 1; | |
const nextInput = this.$refs[next]; | |
if (!nextInput) { | |
const lastInput = this.$refs[this.length - 1]; | |
if (lastInput) { | |
lastInput.focus(); | |
lastInput.select(); | |
} | |
return; | |
} | |
nextInput.focus(); | |
nextInput.select(); | |
}, | |
focusPreviousRef(current) { | |
const previous = parseInt(current, 10) - 1; | |
const previousInput = this.$refs[previous]; | |
if (!previousInput) { | |
return; | |
} | |
previousInput.focus(); | |
previousInput.select(); | |
}, | |
}" | |
id="{{ $getId() }}-wrapper" | |
x-on:paste.prevent="handlePaste($event)" | |
class="grid grid-cols-[--pin-entry-grid] gap-2 justify-center" | |
style="--pin-entry-grid: repeat({{ $getLength() }}, minmax(0, 1fr));" | |
> | |
@for($i = 0; $i < $getLength(); $i++) | |
<input | |
type="text" | |
minlength="1" | |
maxlength="1" | |
placeholder=" " | |
x-ref="{{ $i }}" | |
autocomplete="off" | |
data-lpignore="true" | |
x-on:input.prevent="handleInput($event.target)" | |
x-on:keydown.backspace="handleBackspace($event)" | |
aria-label="{{ sprintf('%s %s', $getLabel(), $i + 1) }}" | |
id="{{ $i === 0 ? $getId() : sprintf('%s.%s', $getId(), $i + 1) }}" | |
wire:key="{{ $i === 0 ? $getStatePath() : sprintf('%s.%s', $getStatePath(), $i + 1) }}" | |
class="h-[2.347rem] bg-white/0 rounded-[0.6248125rem] border-2 border-primary focus:border-primary text-center text-sm font-normal text-primary appearance-none md:font-bold md:text-xl md:h-[3.90513rem] focus:ring-primary placeholder-shown:border-gray-300" | |
/> | |
@endfor | |
</fieldset> | |
</x-dynamic-component> |
This file contains 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
<?php | |
namespace App\Frontend\Forms\Components; | |
use Closure; | |
use Filament\Forms\Components\Field; | |
class PinInput extends Field | |
{ | |
protected Closure|int|null $length = null; | |
protected Closure|string|null $submit = null; | |
protected string $view = 'frontend.forms.components.pin-input'; | |
public static function make(string $name): static | |
{ | |
return parent::make($name) | |
->hiddenLabel() | |
->columnSpanFull() | |
->regex('/^\d+$/') | |
->length(6) | |
->rule(fn(PinInput $component): Closure => function ( | |
string $attribute, | |
?string $value, | |
Closure $fail | |
) use ($component) { | |
$value = trim($value ?? ''); | |
$length = $component->getLength(); | |
$validationAttribute = ucfirst($component->getValidationAttribute()); | |
if ($length && strlen($value) !== $length) { | |
$fail(__(':validationAttribute must be :length characters long.', compact('validationAttribute', 'length'))); | |
} | |
}); | |
} | |
public function submit(Closure|string $submit): static | |
{ | |
$this->submit = $submit; | |
return $this; | |
} | |
public function getSubmit(): ?string | |
{ | |
return $this->evaluate($this->submit); | |
} | |
public function length(Closure|int $length): static | |
{ | |
$this->length = $length; | |
return $this; | |
} | |
public function getLength(): ?int | |
{ | |
return $this->evaluate($this->length); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment