Skip to main content

Forms

Tags Input code

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

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

@props([
    'name' => null,
    'tags' => [],
    'placeholder' => 'Add a tag...',
    'max' => null,
    'invalid' => false,
    'disabled' => false,
    'allowDuplicates' => false,
])

@php
    /*
    | Tags / token input. Type and press Enter or comma to add a tag; click the
    | × on a tag or press Backspace in an empty field to remove the last one.
    | Each tag is submitted as `{name}[]` via hidden inputs, so it binds straight
    | to an array request field.
    |
    | Usage:
    |   <x-ui.tags-input name="tags" :tags="['laravel', 'tailwind']" />
    |   <x-ui.tags-input name="skills" :max="5" placeholder="Add a skill..." />
    */
    $initial = collect($tags)
        ->map(fn ($tag) => (string) $tag)
        ->filter(fn ($tag) => $tag !== '')
        ->values()
        ->all();

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

    $normalizedName = $name
        ? trim((string) preg_replace('/[^A-Za-z0-9\-_:.]+/', '-', str_replace(['[', ']'], '', (string) $name)), '-')
        : null;
    $inputId = $attributes->get('id') ?: $normalizedName;
    $providedAriaLabel = $attributes->get('aria-label');
    $providedAriaLabelledBy = $attributes->get('aria-labelledby');
    $fallbackLabel = null;

    if (! $providedAriaLabel && ! $providedAriaLabelledBy && ! $attributes->get('id')) {
        $fallbackLabel = $placeholder
            ? rtrim((string) $placeholder, '. ')
            : ($name ? Str::headline(str_replace(['-', '_'], ' ', $normalizedName ?? (string) $name)) : 'Tags');
    }
@endphp

<div
    x-data="{
        tags: @js($initial),
        draft: '',
        max: @js($max !== null ? (int) $max : null),
        disabled: @js((bool) $disabled),
        allowDuplicates: @js((bool) $allowDuplicates),
        get atLimit() {
            return this.max !== null && this.tags.length >= this.max;
        },
        addTag() {
            if (this.disabled) { return; }
            const value = this.draft.trim().replace(/,+$/, '').trim();
            this.draft = '';
            if (value === '' || this.atLimit) { return; }
            if (! this.allowDuplicates && this.tags.includes(value)) { return; }
            this.tags.push(value);
        },
        removeTag(index) {
            if (this.disabled) { return; }
            this.tags.splice(index, 1);
        },
        removeLast() {
            if (this.disabled || this.draft !== '' || this.tags.length === 0) { return; }
            this.tags.pop();
        },
    }"
    {{ $attributes->only('class')->merge(['class' => 'flex flex-wrap items-center gap-1.5 rounded-lg border bg-white dark:bg-gray-900 px-2 py-1.5 shadow-sm transition focus-within:ring-1 '.$ring.($disabled ? ' opacity-50 cursor-not-allowed' : '')]) }}
    @click="$refs.entry.focus()"
>
    <template x-for="(tag, index) in tags" :key="index">
        <span class="inline-flex items-center gap-1 rounded-md bg-brand-50 dark:bg-brand-950/60 py-1 ps-2.5 pe-1 text-sm font-medium text-brand-700 dark:text-brand-300">
            <span x-text="tag"></span>
            <button
                type="button"
                x-show="! disabled"
                @click.stop="removeTag(index)"
                class="flex h-4 w-4 items-center justify-center rounded text-brand-500 hover:bg-brand-100 dark:hover:bg-brand-900 hover:text-brand-700 dark:hover:text-brand-200 transition"
                :aria-label="'Remove ' + tag"
            >
                <x-ui.icon name="x" weight="bold" size="xs" />
            </button>
        </span>
    </template>

    @if ($name)
        <template x-for="(tag, index) in tags" :key="'input-' + index">
            <input type="hidden" name="{{ $name }}[]" :value="tag">
        </template>
    @endif

    <input
        type="text"
        x-ref="entry"
        @if ($inputId) id="{{ $inputId }}" @endif
        @if ($fallbackLabel) aria-label="{{ $fallbackLabel }}" @endif
        x-model="draft"
        @disabled($disabled)
        :disabled="atLimit || disabled"
        :placeholder="atLimit ? '' : @js($placeholder)"
        @keydown.enter.prevent="addTag()"
        @keydown.comma.prevent="addTag()"
        @keydown.backspace="removeLast()"
        @blur="addTag()"
        class="min-w-[8rem] flex-1 border-0 bg-transparent p-1 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-0 disabled:cursor-not-allowed"
        {{ $attributes->except(['class', 'id', 'aria-label']) }}
    >
</div>