Livewire Packagev2.0.0MIT License

Livewire Tagify

Livewire Tagify turns an <input> or <textarea> in a Livewire component into a Tagify field that stays in sync with the component. Put data-pharaonic="tagify" and wire:model on the field, load one Blade directive, and the tags follow the property in both directions: what the user adds or removes reaches the component, and what the server sets shows in the field. It is built on Livewire Assistant, which finds the elements, binds each one to the component that owns it, and cleans them up when Livewire removes them.

Two-way binding

Tag changes update the property, and values the server sets update the field, without echo requests.

Plain values or tag objects

Bind plain values such as php and laravel with data-direct, or keep Tagify's tag objects.

Tags, select, and mix modes

Many tags, one choice from a list, or text with mentions, each with its own bound value.

Suggestions in one attribute

Suggestion and block lists, an enforced list, a maximum, and delimiters as plain attributes.

Right component, always

Each field binds to the component around it, so nested components and many fields per page just work.

No Node build required

The script ships prebuilt in the Composer package, or as an ES module for bundlers such as Vite.

Livewire 2 line

This documentation covers the 2.x line, which supports Livewire 2. Each Livewire major gets its own line. Coming from 1.2? See Upgrading from 1.2.

#Installation

Install the package with Composer, keep Tagify on the page, and load the scripts once.

#Requirements

  • PHP 8.0 to 8.3, as supported by your Laravel version
  • Laravel 7.30+, 8.75+, 9, or 10
  • Livewire 2.12.7 or later (see Compatibility)
  • Tagify 4, loaded by your application
  • A modern browser (ES2019); Internet Explorer is not supported

#Composer Installation

Terminal
composer require pharaonic/livewire-tagify

Composer also installs pharaonic/livewire-assistant. Laravel discovers both service providers automatically. There is nothing to publish and no configuration file.

Composer and unmaintained releases

Livewire 2.12.7 and later have no known advisories, but every Laravel 7, 8, 9, and 10 release has security advisories that no release in its major fixes. Recent Composer versions refuse to install or update the framework until you allow them. If you accept the risk, allow these packages in your application's composer.json:

LaravelPackages to allow
7laravel/framework, league/commonmark, league/flysystem
8laravel/framework, league/flysystem
9 on PHP 8.0laravel/framework, symfony/http-foundation, symfony/mailer, symfony/mime, symfony/process, symfony/routing, symfony/yaml
9 on PHP 8.1+, 10laravel/framework
Terminal
composer config --json policy.advisories.ignore '{"laravel/framework": "No fixed release within this Laravel major."}'

Read Compatibility before accepting the risk.

#Load the Scripts

Load Tagify and its stylesheet the way you already do, then add @livewireTagifyScripts to your layout. It can go before or after @livewireScripts.

resources/views/layouts/app.blade.php
1<head>
2 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@yaireo/[email protected]/dist/tagify.css">
3 <script src="https://cdn.jsdelivr.net/npm/@yaireo/[email protected]/dist/tagify.js"></script>
4 @livewireStyles
5</head>
6<body>
7 {{ $slot }}
8 
9 @livewireScripts
10 @livewireTagifyScripts
11</body>

The directive renders two <script> tags: the Livewire Assistant runtime and this integration. Each is rendered once per page, even when layouts, partials, and @livewireAssistantScripts ask for it again.

1<script src="https://example.com/pharaonic/livewire-assistant/assistant.js?id=3f9a1c2b7d4e5f60" data-turbo-eval="false" data-turbolinks-eval="false"></script>
2<script src="https://example.com/pharaonic/livewire-tagify/tagify.js?id=0d8eee40cb71f6d7" data-turbo-eval="false" data-turbolinks-eval="false"></script>

Tagify is read from window.Tagify when a field mounts, not when the script loads, so its order relative to the directive does not matter. If it is still missing when the page has finished loading, the field is reported as mount-failed with the tagify-missing cause.

#Vite and Other Bundlers

When Tagify comes from npm, expose it on window:

resources/js/app.js
1import Tagify from '@yaireo/tagify'
2import '@yaireo/tagify/dist/tagify.css'
3 
4window.Tagify = Tagify

Keep @livewireTagifyScripts in the layout, or import the ES modules instead (see below).

#Content Security Policy

Pass a nonce when your CSP uses one. Both tags get it, escaped.

resources/views/layouts/app.blade.php
1@livewireTagifyScripts(['nonce' => $cspNonce])

nonce is the only option. An unknown option or a non-string nonce throws an InvalidArgumentException while the view renders. The scripts never use eval or inline code.

#How the Script Is Served

RouteName
GET /pharaonic/livewire-tagify/tagify.jspharaonic.livewire-tagify.script

The route serves dist/livewire-tagify.js from the installed package, without session or cookies. The id query value is a hash of the file: a matching id is cached for a year, anything else is served with no-cache. After composer update the hash changes, so browsers fetch the new script on the next page load. Nothing needs publishing, and the route works with php artisan route:cache.

#ES Module

If you bundle your JavaScript, import the ES modules from the Composer packages instead of using the directive. Import the Assistant too: the integration registers with it but never loads it.

resources/js/app.js
1import Tagify from '@yaireo/tagify'
2import '../../vendor/pharaonic/livewire-assistant/dist/livewire-assistant.esm.js'
3import '../../vendor/pharaonic/livewire-tagify/dist/livewire-tagify.esm.js'
4 
5window.Tagify = Tagify

The ES module and the browser build share one integration per page, so loading both is harmless.

Installation Complete

Mark a field with data-pharaonic="tagify" and you are done. See Basic Usage.

#Basic Usage

A Tagify field is a normal <input> with data-pharaonic="tagify" and wire:model, wrapped in wire:ignore.

#The Component

app/Http/Livewire/Profile.php
1<?php
2 
3namespace App\Http\Livewire;
4 
5use Livewire\Component;
6 
7class Profile extends Component
8{
9 public $skills = ['php'];
10 
11 public $keywords = [];
12 
13 public function render()
14 {
15 return view('livewire.profile');
16 }
17}

#The View

resources/views/livewire/profile.blade.php
1<div>
2 <div wire:ignore>
3 <input data-pharaonic="tagify" wire:model="skills" data-direct data-suggest
4 data-suggest-list='["php","laravel","livewire"]'>
5 </div>
6 
7 <div wire:ignore>
8 <input data-pharaonic="tagify" wire:model="keywords" data-max="5">
9 </div>
10</div>

That is all. The first field shows the tag php; adding laravel sets $skills to ["php","laravel"]; $this->skills = ['livewire'] on the server shows that tag instead. The second field holds Tagify's tag objects, such as [{"value":"seo"}], and stops at five tags. See Binding for the value shapes.

#Why wire:ignore

Livewire updates a component's HTML after each request to match the server. Tagify hides the field and adds its own markup before it, which that update would remove. wire:ignore tells Livewire to leave the block alone, so the field survives every request. The package keeps the value in sync through the component, not through the HTML.

A field can also live outside wire:ignore. The package then rebuilds it after each Livewire update. See Dynamic Fields.

#The Attributes

AttributePurpose
data-pharaonic="tagify"Marks the element; required
wire:model="property"The property to bind, with its modifiers
data-property="property"The property to bind, when you do not use wire:model
data-direct, data-keyBind plain values instead of tag objects (see Binding)
data-suggest-list, data-suggest, data-suggest-enforce, data-black-list, data-max, data-delimiters, data-classname, data-valueShorthands for common Tagify settings (see Options)
placeholder, readonly, disabled, requiredRead by Tagify itself
data-options='{...}'A JSON object passed to Tagify, for everything else
data-component-idOptional: bind to a specific component instead of the one around the field

Without wire:model or data-property, the field becomes a Tagify field that is not bound to anything, and Tagify's own change event reaches your listeners as usual.

#Labels and Validation Messages

Livewire never updates anything inside wire:ignore, including an @error message placed there. Keep labels and messages outside the ignored block, so they update with the component:

1<label for="skills">Skills</label>
2 
3<div wire:ignore>
4 <input id="skills" data-pharaonic="tagify" wire:model="skills" data-direct>
5</div>
6 
7@error('skills') <span class="text-danger">{{ $message }}</span> @enderror

#Binding

The package binds a field to one component property, in both directions.

#wire:model or data-property

The property comes from wire:model on the field, or from data-property. When both are present, data-property wins.

1<input data-pharaonic="tagify" wire:model="skills">
2<input data-pharaonic="tagify" data-property="filters.tags">

Both accept a property name or a dot path (filters.tags, rows.2.labels).

#Modifiers on Livewire 2

The modifiers mean what they mean in Livewire 2:

AttributeThe change is sent
wire:modelright away
wire:model.deferwith the component's next request, such as a wire:click
wire:model.lazy, wire:model.debounce.500msright away: Tagify reports a change once the tags have changed, so there is nothing to wait for

data-property always sends right away.

#Value Shapes

What the property holds depends on data-direct and on Tagify's mode.

FieldProperty after the user adds php and laravel
Tags, default[{"value":"php"},{"value":"laravel"}]
Tags, data-direct["php","laravel"]
Tags, data-direct data-key="id"each tag's id, such as [1,2]
select modeone tag object, such as {"value":"php"}, or null
select mode with data-directone value, such as "php", or null
mix modethe text with its tags, such as Hi [[{"value":"Mona"}]]

Tag objects keep every field Tagify holds for the tag, such as the extra fields of a suggestion object, without Tagify's internal ones. See Select and Mix Modes for those two modes.

#What the Server Can Set

The server can set the property to any of the shapes above, and the field shows it:

Property valueTags mode showsSelect mode shows
null, '', or []no tagsnothing
'php'one tag phpphp
['php', 'laravel']two tagsset one value instead
[['value' => 'php']]the tag object as it isthe tag object as it is
[1, 2] with data-key="id"the suggestions whose id is 1 and 2set one value instead

A plain value shows the suggestion it names: the entry of the suggestion list whose data-key field (or text) equals it. A value with no matching suggestion becomes a tag that shows the value as text.

#How Values Move

  • Initial value. When the field mounts, it shows the property's current value. The field's own value attribute is ignored for a bound field.
  • User to server. Adding, removing, or editing a tag sends the new value to the property in a Livewire request.
  • Server to field. When a Livewire update changes the property, from an action, a lifecycle hook, or another field, the field shows the new value.
  • No echo. Values the server sets are shown without being sent back, and a change that leaves the value as Livewire holds it sends nothing.

#Reacting to Changes

wire:change on the field runs after the new value is written, so the action sees it. With wire:model.defer, the deferred value travels in the wire:change request:

1<div wire:ignore>
2 <input data-pharaonic="tagify" wire:model="skills" data-direct wire:change="skillsChanged">
3</div>

Tagify fires its own change event with its value as text. On a bound field, or one with wire:change, that event never reaches Livewire or your listeners; the package fires a plain change event once the value is written instead. An updated hook in the component needs no event at all:

app/Http/Livewire/Profile.php
1public function updatedSkills($value)
2{
3 $this->suggestions = Skill::related($value)->pluck('name');
4}

#Options

There are three ways to configure Tagify, from the simplest:

  1. the package's shorthand attributes, for the most common settings;
  2. the attributes Tagify reads itself;
  3. data-options, a JSON object, for everything else.

#Shorthand Attributes

1<input data-pharaonic="tagify" wire:model="groups" data-direct data-suggest-enforce
2 data-suggest-list='["Team A","Team B"]' data-max="3">
AttributeTagify setting
data-suggest-list='[...]'whitelist: the suggestions
data-black-list='[...]'blacklist: values that are never added
data-suggestdropdown.enabled: 0: suggestions open on focus
data-suggest-enforceenforceWhitelist: true and dropdown.enabled: 0: only suggestions can be added
data-max="5"maxTags
data-delimiters=",|;"delimiters: what splits typed or pasted text into tags
data-classname="menu"dropdown.classname: a class for the suggestion dropdown
data-value="name"tagTextProp, dropdown.mapValueTo, and dropdown.searchKeys: the field of a suggestion object that is shown and searched
data-directbinds plain values instead of tag objects (see Binding)
data-key="id"the field data-direct binds; value when absent

data-suggest, data-suggest-enforce, and data-direct are on when present, with or without a value. "false" turns them off, which helps when Blade decides: data-direct="{{ $plain ? 'true' : 'false' }}".

data-suggest-list and data-black-list take a JSON array of strings or objects. Anything that is not a JSON array is read as a list split by the delimiters (, unless set): data-suggest-list="php, laravel". data-max takes a positive whole number; any other value is ignored.

Suggestion objects need a value field, as Tagify requires; add others, such as name with data-value="name":

1<input data-pharaonic="tagify" wire:model="country" data-direct data-value="name"
2 data-suggest-list='@json([["value" => "eg", "name" => "Egypt"], ["value" => "jo", "name" => "Jordan"]])'>

#Defaults

Without attributes, every field gets the dropdown of releases 1.0 to 1.2:

1{
2 "tagTextProp": "value",
3 "dropdown": { "maxItems": 25, "enabled": 1, "closeOnSelect": false, "classname": "", "mapValueTo": "value", "searchKeys": ["value"] }
4}

Suggestions show after one typed character, up to 25 at a time, and the dropdown stays open after a pick.

#Tagify's Own Attributes

Tagify reads placeholder, readonly, disabled, and required from the field, and data-whitelist and data-blacklist as lists split by the delimiters. These need no help from the package.

#data-options

Anything else goes in data-options as a JSON object:

1<input data-pharaonic="tagify" wire:model="emails" data-options='{"pattern": "^\\S+@\\S+$", "duplicates": false, "editTags": 1}'>

Use single quotes around the attribute so the JSON can use double quotes. In Blade you can build it from PHP:

1<input data-pharaonic="tagify" wire:model="labels" data-options='@json(["maxTags" => $limit, "dropdown" => ["maxItems" => 10]])'>

Invalid JSON, or JSON that is not an object, is reported as invalid-options and the field is not mounted. Settings that are JavaScript functions, such as transformTag or validate, cannot be set from markup.

#Which Wins

data-options is merged over the defaults and the shorthands key by key, also inside nested objects: {"dropdown": {"maxItems": 5}} keeps the other dropdown settings, including the ones data-suggest and data-classname set. Tagify's own data-whitelist and data-blacklist win over both.

#Changing Options

Inside wire:ignore, Livewire never changes the field, so new attributes from the server are not applied. To change settings or the suggestion list, let Livewire replace the block with a new wire:key. See Dynamic Fields.

#Select and Mix Modes

Tagify has three modes, set with mode in data-options. Every mode binds the same way; only the value differs.

#Tags

The default. The property holds an array: tag objects, or plain values with data-direct. See Binding.

#Select

One value from a list, like a searchable select.

1<div wire:ignore>
2 <input data-pharaonic="tagify" wire:model="country" data-direct data-value="name"
3 data-options='{"mode": "select", "enforceWhitelist": true, "dropdown": {"enabled": 0}, "whitelist": [{"value": "eg", "name": "Egypt"}, {"value": "jo", "name": "Jordan"}]}'>
4</div>

The property holds the chosen value ("jo") or null. Without data-direct it holds the tag object ({"value":"jo","name":"Jordan"}). The server can set either, or null to clear the field. Use "dropdown": {"enabled": 0} so the list opens on click.

#Mix

Text with tags inside it, such as mentions. Use a <textarea>:

1<div wire:ignore>
2 <textarea data-pharaonic="tagify" wire:model="message"
3 data-options='{"mode": "mix", "pattern": "@", "whitelist": ["Mona", "Omar"], "dropdown": {"enabled": 1, "position": "text"}}'></textarea>
4</div>

The property holds the text, with each tag written as JSON between [[ and ]]:

1$this->message; // 'Hi [[{"value":"Mona","prefix":"@"}]], see you soon'

The server sets the same format, and null or '' empties the field. data-direct has no effect in mix mode, because the value is text. Tagify's mixTagsInterpolator setting changes the [[ and ]] markers. Tagify's mixMode.integrated, which has no original field, cannot be bound.

Reading mentions on the server

Pull the tags out of the text when you save it:

1preg_match_all('/\[\[(.+?)\]\]/', $this->message, $matches);
2$mentioned = array_map(fn ($json) => json_decode($json, true)['value'], $matches[1]);

#Dynamic Fields

Livewire adds, removes, and re-renders elements. The package follows it.

#Fields Livewire Adds or Removes

Fields that appear after a Livewire update, such as a new row from wire:click="addRow", mount and bind after that update. Fields that disappear are cleaned up: their listeners are removed and Tagify is destroyed.

resources/views/livewire/products.blade.php
1@foreach ($rows as $index => $row)
2 <div wire:key="row-{{ $row['id'] }}">
3 <div wire:ignore>
4 <input data-pharaonic="tagify" wire:model="rows.{{ $index }}.tags" data-direct>
5 </div>
6 </div>
7@endforeach
8 
9<button wire:click="addRow">Add row</button>
Key rows by something stable

Give each row a wire:key that stays with the row, such as an id, not its position. When a row in the middle is removed, position keys shift, and Livewire keeps the wrong fields.

#New Suggestions from the Server

Inside wire:ignore, Livewire never changes the field, so a new data-suggest-list from the server is not applied. Give the ignored block a wire:key that changes with the data: Livewire replaces the block, the old field is cleaned up, and the new one mounts with the new list.

1<div wire:ignore wire:key="tags-{{ $category }}">
2 <input data-pharaonic="tagify" wire:model="tags" data-direct data-suggest data-suggest-list='@json($suggestions)'>
3</div>

#Fields Outside wire:ignore

A field outside wire:ignore works too. Livewire's update removes the markup Tagify added, and the package rebuilds the field after each update, with the property's current value. Every request that re-renders the component rebuilds the field and closes an open dropdown, so prefer wire:ignore.

Key ignored blocks that come and go

Without keys, Livewire compares siblings by position. When ignored blocks can appear or disappear next to each other, give each a unique wire:key.

#Nested Components and Many Fields

Each field binds to the closest Livewire component around it. A field in a nested component writes to that component, never to the parent. Any number of fields can share a page.

data-component-id binds a field to a specific component wherever it is. In a Livewire 2 view the id is $this->id. An empty value means the closest component.

#Fields You Add Yourself

Fields you add with JavaScript, outside a Livewire update, need one call:

1Pharaonic.assistant.scan(container) // mounts the fields in container
2Pharaonic.assistant.destroy(container) // cleans them up

Livewire 2 works with Turbolinks and Turbo through Livewire's livewire-turbolinks adapter. Fields are cleaned up before a page is cached and mounted again when it comes back.

#Upgrading from 1.2

Releases 1.0 to 1.2.1 supported Livewire 2. Their successor is this 2.x line. Your markup keeps working, attributes included.

#Install 2.x

A ^1.2 constraint never moves to 2.x, so require the new line:

Terminal
composer require pharaonic/livewire-tagify:^2.0

The requirements are higher than 1.2's:

1.22.x
PHP7.2+8.0 to 8.3
Livewire2.0+2.12.7+
Laravelthrough Livewire7.30+, 8.75+, 9, 10

Composer also installs pharaonic/livewire-assistant.

#Scripts

Use the directive in new code:

resources/views/layouts/app.blade.php
1{{-- 1.2 --}}
2<x-pharaonic-tagify::scripts />
3 
4{{-- 2.x --}}
5@livewireTagifyScripts

<x-pharaonic-tagify::scripts /> renders exactly the same tags, so you can switch whenever you like.

If you published vendor/pharaonic/pharaonic.tagify.min.js and load it with your own <script> tag, switch to the directive now: the file needs the Livewire Assistant runtime, which the directive loads. Then delete the published file.

Deprecated, not removed

The scripts component, the publish tag, and the old JavaScript functions still work on every 2.x release. New code should use their replacements. They will be removed no earlier than a future major line.

#Attributes

1.22.x
data-component-id="{{ $this->id }}"Optional: the closest component is used
wire:model, wire:model.deferUnchanged; .lazy and .debounce are bound too
data-direct, data-key, data-valueUnchanged
data-suggest-list, data-black-listUnchanged; a delimited list works too
data-suggest, data-suggest-enforce, data-max, data-classnameUnchanged; "false" now turns a switch off

New: data-property to bind without wire:model, data-delimiters, and data-options for any other Tagify setting. See Options.

#Behaviour Changes

  • Server-side changes to the property now show in the field.
  • The field shows the property's value when it mounts. On a bound field, its own value attribute is ignored; 1.2 read its tags from it.
  • Fields without data-component-id now mount; 1.2 skipped them.
  • Fields that Livewire adds later mount by themselves; you no longer need to dispatch pharaonic.tagify.init.
  • Tagify's select and mix modes bind one value and the mixed text. See Select and Mix Modes.
  • Tagify's own change event no longer reaches Livewire or your listeners on a bound field, so a field sends one request per change. wire:change on the field now runs, after the value is written.
  • Tagify is read from window.Tagify when a field mounts, not when the script loads.

#JavaScript

Pharaonic.Tagify.init(), Pharaonic.Tagify.load(target, componentId), and the pharaonic.tagify.init and pharaonic.tagify.load events still work: they call Pharaonic.assistant.scan(). In new code, call that directly for fields you add yourself.

#API Reference

#Markup

AttributeValue
data-pharaonictagify
wire:modelProperty name or dot path; .defer waits for the next request; every other form is sent right away
data-propertyProperty name or dot path; wins over wire:model
data-directBind each tag's data-key instead of tag objects; "false" turns it off
data-keyThe tag field data-direct binds; value when absent
data-suggest-listwhitelist, a JSON array or a delimited list
data-black-listblacklist, a JSON array or a delimited list
data-suggestdropdown.enabled: 0; "false" turns it off
data-suggest-enforceenforceWhitelist: true and dropdown.enabled: 0; "false" turns it off
data-maxmaxTags, a positive whole number
data-delimitersdelimiters
data-classnamedropdown.classname
data-valuetagTextProp, dropdown.mapValueTo, and dropdown.searchKeys
data-optionsJSON object passed to Tagify, merged over the shorthands key by key
wire:changeRuns after the new value is written
data-component-idComponent id ($this->id); empty or absent means the closest component

#Bound Values

ModeDefaultWith data-direct
tagsarray of tag objectsarray of each tag's data-key
selectone tag object or nullone data-key value or null
mixtext with [[{...}]] tagstext with [[{...}]] tags

#Browser API

window.Pharaonic.tagify is present once the integration has loaded; it has no methods in this release.

Deprecated aliases, on window.Pharaonic.Tagify:

MethodDescription
init()Calls Pharaonic.assistant.scan(document)
load(target, componentId)Calls Pharaonic.assistant.scan(target); target is an element or a selector, and componentId sets data-component-id when the element has none

#Events

EventDirectionPurpose
changedispatched on a field with wire:changeAfter the new value is written
pharaonic:assistant:errordispatched on windowFailures while mounting, updating, or cleaning up (see the Livewire Assistant diagnostics)
pharaonic.tagify.initlistened on windowDeprecated: scans the document
pharaonic.tagify.loadlistened on windowDeprecated: scans event.detail.target with event.detail.component as the component id

#Error Codes

CodeRaised whenChannel
tagify-missingTagify is not on window.Tagify once the page has loadedcause of mount-failed

#Laravel

NamePurpose
@livewireTagifyScripts / @livewireTagifyScripts(['nonce' => $nonce])Renders the Assistant runtime and the integration once per page
pharaonic.livewire-tagify.scriptRoute name of GET /pharaonic/livewire-tagify/tagify.js
Pharaonic\Livewire\Tagify\TagifyServiceProviderAuto-discovered service provider
<x-pharaonic-tagify::scripts />Deprecated: renders @livewireTagifyScripts
livewire-tagify publish tagDeprecated: copies the script to public/vendor/pharaonic/pharaonic.tagify.min.js

#Compatibility

Every combination below installs with Composer and passes PHPStan and the PHP suite with lowest and highest dependencies. The real-browser suite (real Livewire 2 components, the real Livewire Assistant runtime, and Tagify, driven in real browsers) runs on the lowest PHP of each Laravel version, with lowest and highest dependencies.

LineLivewireLaravelPHPBrowsers
2.x2.12.7+7.30+8.0Chromium, Firefox, WebKit
2.x2.12.7+8.75+8.0, 8.1Chromium, Firefox, WebKit
2.x2.12.7+98.0, 8.1, 8.2Chromium, Firefox, WebKit
2.x2.12.7+108.1, 8.2, 8.3Chromium, Firefox, WebKit

The browser suite uses Tagify 4.39.

#Why Livewire 2.12.7

CVE-2024-47823 (remote code execution through file uploads) affects every Livewire release before 2.12.7. Requiring 2.12.7 keeps it out of every application that installs this line, and every Livewire 2 application can update to it within its major.

Unmaintained Laravel versions

Laravel 7, 8, 9, and 10 no longer receive security fixes. This line supports them because Livewire 2 does, but your application still carries their advisories. Plan your upgrade to a maintained Laravel version, which means Livewire 3 or later.

#Examples

#1. Post Tags with Suggestions

Plain tag names, suggestions from the database, and new tags allowed.

app/Http/Livewire/PostForm.php
1<?php
2 
3namespace App\Http\Livewire;
4 
5use App\Models\Post;
6use App\Models\Tag;
7use Livewire\Component;
8 
9class PostForm extends Component
10{
11 public Post $post;
12 
13 public $tags = [];
14 
15 public function mount(Post $post)
16 {
17 $this->post = $post;
18 $this->tags = $post->tags()->pluck('name')->all();
19 }
20 
21 public function save()
22 {
23 $this->validate(['tags' => 'array|max:10', 'tags.*' => 'string|max:30']);
24 
25 $ids = collect($this->tags)->map(fn ($name) => Tag::firstOrCreate(['name' => $name])->id);
26 $this->post->tags()->sync($ids);
27 }
28 
29 public function render()
30 {
31 return view('livewire.post-form', ['suggestions' => Tag::orderBy('name')->pluck('name')]);
32 }
33}
resources/views/livewire/post-form.blade.php
1<form wire:submit.prevent="save">
2 <label for="tags">Tags</label>
3 
4 <div wire:ignore>
5 <input id="tags" data-pharaonic="tagify" wire:model="tags" data-direct data-max="10"
6 data-suggest-list='@json($suggestions)' placeholder="Add tags">
7 </div>
8 
9 @error('tags') <span class="text-danger">{{ $message }}</span> @enderror
10 
11 <button type="submit">Save</button>
12</form>

#2. Assign Users by Id

Suggestions are objects; the property holds user ids, and the field shows names.

resources/views/livewire/task-form.blade.php
1<div wire:ignore>
2 <input data-pharaonic="tagify" wire:model="assignees" data-direct data-key="id" data-value="name"
3 data-suggest-enforce
4 data-suggest-list='@json($users->map(fn ($user) => ["id" => $user->id, "value" => (string) $user->id, "name" => $user->name]))'>
5</div>

$assignees = [3, 7] shows the names of users 3 and 7, and picking another user adds its id.

#3. A Searchable Single Choice

resources/views/livewire/shipping.blade.php
1<div wire:ignore>
2 <input data-pharaonic="tagify" wire:model="country" data-direct data-value="name"
3 data-options='@json(["mode" => "select", "enforceWhitelist" => true, "dropdown" => ["enabled" => 0], "whitelist" => $countries])'>
4</div>

With $countries as [['value' => 'eg', 'name' => 'Egypt'], ...], $country holds "eg" or null.

#4. Mentions in a Comment

resources/views/livewire/comment-box.blade.php
1<div wire:ignore>
2 <textarea data-pharaonic="tagify" wire:model="body"
3 data-options='@json(["mode" => "mix", "pattern" => "@", "whitelist" => $members, "dropdown" => ["enabled" => 1, "position" => "text"]])'></textarea>
4</div>
5 
6<button wire:click="post">Post</button>

$body holds the text with each mention written as [[{"value":"Mona","prefix":"@"}]]. See Select and Mix Modes to read the mentions on the server.

#5. Email Recipients

Typed or pasted addresses split on commas, semicolons, and spaces, and only valid ones become tags.

resources/views/livewire/invite.blade.php
1<div wire:ignore>
2 <input data-pharaonic="tagify" wire:model="emails" data-direct data-delimiters=",|;| "
3 data-options='{"pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "keepInvalidTags": false}'>
4</div>

#Troubleshooting

#A validation message does not show

The @error is inside the wire:ignore block, which Livewire never updates. Move it outside, next to the block. See Basic Usage.

#Nothing happens and there is no field

Check the console. tagify-missing means Tagify is not on window.Tagify: load it, or with a bundler set window.Tagify (see Installation). Make sure @livewireTagifyScripts is on the page.

#The property holds objects, but I want strings

Add data-direct. Without it the property holds Tagify's tag objects. See Binding.

#Picking a suggestion does nothing

Tagify needs a value field on every suggestion object. Add one, and use data-value to show another field, such as name.

#A plain value from the server shows as a number or an id

The value has no matching suggestion. Check that data-key names the field the values match, and that the suggestion list holds them.

#New suggestions from the server do not appear

Inside wire:ignore, Livewire never changes the field. Give the ignored block a wire:key that changes with the data. See Dynamic Fields.

#The field resets or its dropdown closes after a request

The field is outside wire:ignore and the package rebuilds it after each update, or Livewire replaced the block. Wrap the field in wire:ignore.

#My change listener no longer runs

On a bound field, Tagify's own change event is kept from Livewire and from your listeners, because it carries the value as text. Use wire:change, the component's updated hook, or Tagify's events through element.__tagify.on('change', ...).

#A field in a nested component updates the parent

Remove a data-component-id that names the parent, or leave the attribute out: the closest component is used.

#Composer refuses to install or update Laravel 7, 8, 9, or 10

Those releases have unfixed security advisories. See the warning in Installation.

Contributors

Amazing people who made this package possible

This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Tagify!

Moamen Eltouny

@MoamenEltouny

21 contributions

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.