palauaandsons/nova-tags-field
A Laravel Nova field for adding tag-style input to your resources. Lets editors attach multiple tags through a clean, searchable interface, making it easy to organize, filter, and manage records from the Nova admin panel.
Preconditions. Laravel + a licensed Nova install. This is a Nova form field, not a general tagging library — it does nothing outside a Nova resource's fields() method. If you're not on Nova, stop here.
Install and pin. Don't take a floating constraint; the package's last release was 2023-03-31 and Nova 5 compatibility is unverified.
composer require palauaandsons/nova-tags-field:4.0.1
php artisan nova:assets
php artisan nova:publish # only if you intend to fork the Vue components
Where to look first — and this is not optional. Before you write a single field definition, open the shipped source in vendor/palauaandsons/nova-tags-field/ and answer one question: does this persist a JSON array on the model, or does it write through cartalyst/tags polymorphic pivots with migrations? The package description and the integration guidance are in direct conflict on this, and the answer changes your schema, your filtering strategy, and your reporting ceiling. Read composer.json (for the nova version constraint and whether cartalyst/tags is actually a dependency), src/, and the README. The marketing copy is not a source of truth here.
cartalyst/tags is a real dependency: expect taggable pivots, a shared tags table, and migrations you must run.string/json column with an array or AsArray cast, and per-record tag lists.First use case. One resource, one column, end to end. Add a tags JSON column, cast it, drop the field in, create and edit a record, then dd() the model to confirm what actually landed in the database. Do this in a throwaway branch before you design anything else.
// migration
$table->json('tags')->nullable();
// app/Models/Post.php
protected $casts = ['tags' => 'array'];
// app/Nova/Post.php
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
\Palauaandsons\NovaTagsField\Tags::make('Tags'),
];
}
Confirm the exact class and namespace from src/ on install — the package is small enough that guessing wastes more time than reading.
Normalize at the model, not at the field. The field is a thin UI layer; Nova validation only guards Nova writes. Every API endpoint, artisan command, seeder, and queue job bypasses it. Put trim/dedupe/lowercase in a mutator so all writers converge:
public function setTagsAttribute($value): void
{
$tags = is_array($value) ? $value : explode(',', (string) $value);
$this->attributes['tags'] = json_encode(
collect($tags)->map(fn ($t) => Str::lower(trim($t)))->filter()->unique()->values()->all()
);
}
Mirror Nova rules at the model layer. Whatever constraints you express in rules() (max count, max length, allowed characters) need an equivalent booted() hook or FormRequest, or API writes will drift from admin-entered data.
Scope field visibility deliberately. Tag chips render per-row on index views; on large tables that's wasted cycles. Use onlyOnDetail() for read-mostly resources, and hideFromIndex() where the tag list is long.
Tags::make('Tags')
->onlyOnDetail()
->help('Comma-separated; duplicates are removed automatically.'),
Query patterns follow the storage decision. If JSON:
Post::whereJsonContains('tags', 'laravel')->get();
That works, but it doesn't use a standard index and won't scale past tens of thousands of rows. If you find yourself writing tag-based filters, aggregates, or "most used tags" dashboards, you've crossed from labelling into reporting — at that point own the schema and migrate to a real tags + pivot before the data grows, not after.
Seed and write through the same normalizer. Seeders, imports, and API controllers should set $model->tags = [...] and let the mutator handle it, rather than writing raw JSON strings.
Test the contract, not the widget. A Pest/PHPUnit test asserting the persisted attribute array after a save is worth more than any UI test here, because it catches the cast bug and the normalizer in one shot:
it('normalizes and dedupes tags', function () {
$post = Post::factory()->create(['tags' => ['Laravel ', 'laravel', 'Nova']]);
expect($post->fresh()->tags)->toBe(['laravel', 'nova']);
});
Add a snapshot of the field definition so a version bump that silently changes config surfaces in CI rather than in production.
The storage-model contradiction is the whole ballgame. Everything upstream of this decision is reversible in hours; the schema is not. Timebox a spike and write the answer down before you ship to more than one resource.
The missing cast is the single most common integration bug. Without 'tags' => 'array' on the model, you'll get a string back where you expect an array, JSON-in-JSON double-encoding, and whereJsonContains returning nothing. If tags "save but don't come back," check the cast first.
Rebuild assets after every install or upgrade. php artisan nova:assets plus a hard browser refresh. A stale compiled bundle is the second most common cause of "the field doesn't appear" — verify the input renders correctly in both light and dark themes, since 4.0.1's only contribution was a dark-mode CSS patch.
Pin the version in the lockfile and write down why. Nova field packages are tightly coupled to Nova release lines, and this one's cadence stopped in early 2023 with zero dependents and no evidence of production use elsewhere. When you plan a Nova major upgrade, treat "does the tag field still work" as a named task, not a surprise. Pre-agree the fork trigger — a Nova major bump, or a month of broken UI — and name an owner now rather than during an incident.
Extension path is "publish and fork the Vue components." If you need custom chip rendering, async tag lookup, tag hierarchies, per-tag permissions, or tenant-scoped vocabularies, that path is most of the build anyway. Skip the dependency and build it — you'll get a better result and won't be maintaining a fork of someone else's abandoned fork.
Treat rendered tag text as user input. Chips display whatever an admin typed; if the field's Vue component doesn't escape, you have a stored-XSS surface inside your admin panel. Worth a five-minute check in the source if your admin users aren't strictly trusted.
Don't let tags become a product surface while you're not looking. The moment tags get filtered in a customer-facing view, aggregated into a report, or synced to an external taxonomy, you need first-party ownership of the schema and the write path. The field can stay as the admin UI — but the data model should stop being an artifact of a 10-star package's implementation detail.
Document the exit. One paragraph in the repo: what the package does, what column it touches, which resources use it, and the degraded fallback (a plain comma-separated Text field is a few hours of work). That paragraph is what makes this dependency cheap to remove in eighteen months.
How can I help you explore Laravel packages today?