Skip to main content

Forms

Float Input code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/float-input.blade.php

@props([
    'name' => null,
    'value' => null,
    'min' => null,
    'max' => null,
    'step' => 0.1,
    'decimals' => null,
    'placeholder' => null,
    'size' => 'base',
    'icon' => null,
    'invalid' => false,
    'disabled' => false,
    'controls' => true,
])

@php
    /*
    | Decimal (floating-point) number input. Allows a single decimal point,
    | strips other non-numeric characters as you type, clamps to min/max and
    | optionally rounds to a fixed number of `decimals` on blur. Stepper buttons
    | and up/down arrow keys nudge by `step`.
    |
    | Usage:
    |   <x-ui.float-input name="price" :value="9.99" :min="0" :step="0.01" :decimals="2" icon="currency-dollar" />
    |   <x-ui.float-input name="weight" :step="0.5" :controls="false" />
    */
    $sizes = [
        'sm' => 'text-sm py-1.5',
        'base' => 'text-sm py-2.5',
        'lg' => 'text-base py-3',
    ];

    $pad = 'px-3.5';
    if ($icon) { $pad = 'ps-10 pe-3.5'; }
    if ($controls) { $pad = $icon ? 'ps-10 pe-10' : 'ps-3.5 pe-10'; }

    $ring = $invalid
        ? 'border-red-400 dark:border-red-500 focus:border-red-500 focus:ring-red-500'
        : 'border-gray-300 dark:border-gray-700 focus:border-brand-500 focus:ring-brand-500';

    $classes = implode(' ', [
        'block w-full rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 shadow-sm transition focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed',
        $sizes[$size] ?? $sizes['base'],
        $pad,
        $ring,
    ]);
@endphp

<div
    x-data="{
        value: @js($value !== null ? (string) (float) $value : ''),
        min: @js($min !== null ? (float) $min : null),
        max: @js($max !== null ? (float) $max : null),
        step: @js((float) $step),
        decimals: @js($decimals !== null ? (int) $decimals : null),
        disabled: @js((bool) $disabled),
        sanitize() {
            let v = this.value.toString().replace(/[^0-9.\-]/g, '');
            v = v.charAt(0) + v.slice(1).replace(/-/g, '');
            let parts = v.split('.');
            if (parts.length > 2) { v = parts.shift() + '.' + parts.join(''); }
            this.value = v;
        },
        format(n) {
            return this.decimals !== null ? n.toFixed(this.decimals) : n.toString();
        },
        clamp() {
            if (this.value === '' || this.value === '-' || this.value === '.') { return; }
            let n = parseFloat(this.value);
            if (isNaN(n)) { this.value = ''; return; }
            if (this.min !== null && n < this.min) { n = this.min; }
            if (this.max !== null && n > this.max) { n = this.max; }
            this.value = this.format(n);
        },
        nudge(dir) {
            if (this.disabled) { return; }
            let n = parseFloat(this.value);
            if (isNaN(n)) { n = this.min !== null ? this.min : 0; }
            else { n += dir * this.step; }
            if (this.min !== null && n < this.min) { n = this.min; }
            if (this.max !== null && n > this.max) { n = this.max; }
            let dp = this.decimals !== null ? this.decimals : (this.step.toString().split('.')[1] || '').length;
            this.value = parseFloat(n.toFixed(dp)).toString();
        },
    }"
    class="relative"
>
    @if ($icon)
        <span class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3 text-gray-400 dark:text-gray-500">
            <x-ui.icon :name="$icon" size="base" />
        </span>
    @endif

    <input
        type="text"
        inputmode="decimal"
        @if ($name) name="{{ $name }}" @endif
        @if ($placeholder) placeholder="{{ $placeholder }}" @endif
        @disabled($disabled)
        @if ($invalid) aria-invalid="true" @endif
        x-model="value"
        @input="sanitize()"
        @blur="clamp()"
        @keydown.arrow-up.prevent="nudge(1)"
        @keydown.arrow-down.prevent="nudge(-1)"
        {{ $attributes->merge(['class' => $classes]) }}
    >

    @if ($controls)
        <div class="absolute inset-y-0 end-0 flex flex-col border-s border-gray-200 dark:border-gray-700">
            <button type="button" tabindex="-1" @click="nudge(1)" @disabled($disabled) aria-label="Increment" class="flex flex-1 items-center justify-center px-2.5 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition">
                <x-ui.icon name="caret-up" weight="bold" size="xs" />
            </button>
            <button type="button" tabindex="-1" @click="nudge(-1)" @disabled($disabled) aria-label="Decrement" class="flex flex-1 items-center justify-center px-2.5 border-t border-gray-200 dark:border-gray-700 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition">
                <x-ui.icon name="caret-down" weight="bold" size="xs" />
            </button>
        </div>
    @endif
</div>