Skip to content

Instantly share code, notes, and snippets.

@devhammed
Created October 25, 2024 13:09
Show Gist options
  • Save devhammed/f6b083be1ae5b7f69300f48cb9243207 to your computer and use it in GitHub Desktop.
Save devhammed/f6b083be1ae5b7f69300f48cb9243207 to your computer and use it in GitHub Desktop.
Filament PIN Input
<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>
<?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