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>