superlatif/nova-tag-input
A Laravel Nova field that adds a clean, interactive tag input to your resource forms. Users can easily add, remove, and manage multiple tags, making it simple to organize and categorize models directly within Nova.
Architecture fit. This is not a library that touches the application domain — it is a single Nova form field: a PHP Field subclass that registers one Vue component. The blast radius is confined to the admin surface, which makes it unusually easy to justify as a contained dependency. The data model it implies (nullable JSON column + array cast on the Eloquent model) is idiomatic Laravel and does not force a schema or persistence pattern on the rest of the system. Tags persist as a plain attribute; nothing else in the app has to know the package exists. That isolation is the package's strongest architectural property and the main reason a TPM should feel comfortable piloting it.
The counterweight: it is built against Nova's private Vue/frontend conventions, not a public contract. That means the real architecture-dependency is not on superlatif/nova-tag-input — it's on Nova itself. Adopting this field is an implicit vote to keep Nova as the internal admin layer. Surface that explicitly rather than letting it happen as a side effect of a one-line composer require.
Integration feasibility. High, with caveats. The work is well-understood and bounded:
composer require plus php artisan nova:publish for asset registration.src/ — the docs call it Tag, but the import path is an assumption until verified.Realistically a one-day spike to a working field on a single resource, plus a second day for validation rules, normalization, and a feature test. There is no autocomplete, no tag library, no suggestion engine, no merge/rename workflow. If a stakeholder has seen a chip UI and inferred "typeahead against our existing taxonomy," that misconception must be corrected before the demo — it is a materially larger project.
Technical risk. Dominant risk is Nova major-version drift: a Nova upgrade can make the component throw or silently fail to render, with no Vue-level error surfacing in PHP logs (it appears in the browser console, if at all). With 13 stars, zero dependents, a single maintainer, and a release cadence of roughly one (2024-12-30), we are almost certainly the largest consumer of this package. There is no downstream ecosystem to absorb a regression on our behalf.
Secondary risks, all manageable but real:
null, [], and "" mean three different things to the field, the cast, and validation. This is the source of most "why is there a blank chip" tickets.readonly / canSee. Needs verification with a non-admin user.max rule it degrades both data quality and form render performance.Key questions for engineering (answers gate the go/no-go):
composer.json actually constrain for nova, laravel/framework, and php — pinned, or floating? Floating constraints on a low-adoption package are a red flag.dist/, or does it require us to run a Node build step?canSee / readonly under a non-admin role?taggables join table and a generated column.Stack fit. Strong, conditional on Nova version alignment. Laravel + Nova + JSON column + array cast is the canonical shape the package was designed around. The recommended registration is minimal:
Tag::make('Tags')
->placeholder('Add a keyword and press enter…')
->rules(['array', 'max:10'])
->help('Max 10 tags, 40 characters each.');
Wrap this in a static factory (Fields::tags()) rather than copy-pasting chained calls across resources — when the package needs patching or replacing, there is exactly one place to change.
Migration path. Two distinct scenarios, and they should not be conflated:
Greenfield tagging. Add a json('tags')->nullable() column, cast to array (or AsArrayObject), register the field on one resource. Lowest-risk path; pilot here.
Replacing an existing comma-separated text column. This is a data migration, not a UI change, and it carries reporting/export/analytics impact — a PM-level call, not a field-swap. Sequence: add the JSON column alongside the old one → backfill with an idempotent command that splits, trims, de-dupes and re-encodes → verify against a production-like snapshot → cut the field over → dual-read for one release cycle → retire the old column only after confirmed parity. Do not repoint the field at the text column and hope.
Either way, write the persisted shape down as an internal contract next to the field registration. Future-you will not remember whether it is ["a","b"] or "a,b". Pick JSON array; it keeps tags queryable and removes parsing ambiguity from validation and exports.
Compatibility. The make-or-break axis is Nova 4 vs Nova 5. Validate on the exact pinned staging Nova version, not "latest." Pin the package version explicitly in composer.lock and add it to the dependency-review list — floating constraints here are how a silent storage-format change ships into production.
Also reconcile the three validation surfaces up front. Nova field rules() apply to the attribute as a whole; per-element constraints need dotted keys or a closure, and dotted-rule binding through Nova is not guaranteed across majors. If it doesn't bind reliably, move the constraint into a FormRequest or a model guard. Three rules are non-negotiable on day one: max total tags, max length per tag, non-empty after trimming.
Sequencing. Recommend a four-stage rollout:
src/ — the Field subclass, its component() method, and the service provider. Do not trust the README for the FQCN. Confirm asset registration mechanism and whether dist/ ships.Post, Article, or Customer. Create a record with 2–3 tags, save, reload detail, re-open edit, confirm chips return. That single round trip validates component registration, cast, storage format, and read path simultaneously.Maintenance. The recurring cost is modest but must be owned, not assumed away. Budget roughly one to two days per year for Nova-upgrade revalidation, plus the pinning and dependency-review overhead. The package is a few hundred lines — small enough to fork if needed, which is the key property that makes the maintenance burden acceptable. Name a specific owner for this dependency before adoption, and record the trigger conditions for each escape hatch: (a) subclass the field and override component() to point at our own Vue component; (b) fork the package; (c) replace with an in-house Nova field. Deciding now, in writing, is the difference between a pragmatic dependency and a future incident.
Support. Effectively community-supported with a single maintainer and a small user base. Treat upstream as "best effort, may go quiet." Practical consequence: any bug that blocks us becomes our bug. That is tolerable for a field on an internal admin screen with a one-line fallback to a plain textarea; it would not be tolerable for anything customer-facing or revenue-adjacent. Scope the adoption accordingly.
Scaling. Backend cost is one column read and write — negligible. Rendering is client-side and bounded by tag count per record. The only realistic scaling concern is someone pasting 300 tags into a single record and making the edit form sluggish; the max:10-style rule is a performance guardrail, not just hygiene. Query-side is the more interesting question: a JSON column does not behave like text, and LIKE against it will be wrong or slow. Use whereJsonContains on MySQL/Postgres. If admin tag filtering becomes a first-class need, plan for a generated column or a proper taggables join table — the field is an input widget, not a query layer, and should not be mistaken for one.
Failure modes. Ranked by likelihood and impact:
| Failure | Symptom | Detection | Mitigation |
|---|---|---|---|
| Nova upgrade breaks component | Field blank, no PHP exception | Browser console, not storage/logs |
Keep textarea fallback behind a switch |
| Empty-state ambiguity | Stray empty chip | Feature test on null/[]/"" | Normalize to one value at model layer |
| Dotted validation doesn't bind | Over-limit tags persist | Test posting 11 tags | Move rules to FormRequest |
| Read-only bypass | Non-admin can edit tags | Manual test with restricted role | Verify canSee/readonly pre-rollout |
| Silent upstream change | Storage shape shifts | composer.lock diff review |
Exact version pin |
| Long/special-char tags | Broken layout, injection surface | Test <script>, quotes, emoji, RTL |
Max-length rule; verify escaping |
When the field renders blank, work the diagnostic ladder in order: did nova:publish run and is the compiled asset in public/vendor/; does component() match a registered Vue component; what does the browser console say; does the package ship dist/ at all. That ordered checklist belongs in the runbook, because the failure presents as silence.
Ramp-up. Low for engineers already fluent in Nova — the field registration is a few lines and the model-side pattern is standard Laravel. The genuine ramp-up cost is organizational: getting the team to treat the persisted shape as an internal contract, to normalize at the model boundary rather than in the form, and to resist the assumption that "tagging" means taxonomy. Set stakeholder expectations explicitly on deferred scope — autocomplete against large tag libraries, hierarchical taxonomies, merge/rename workflows, per-role permissions, audit trails, bulk operations — and write that deferred list down at the same time as the ship decision.
Bottom line for the TPM. Adopt as a constrained pilot, not a strategic dependency: one resource, feature-flagged, version-pinned, with a named owner and a documented exit path. The value is converting a small custom-field project into a bounded dependency-management decision. The accepted cost is ownership of a low-adoption package inside a platform (Nova) that is itself the real long-term commitment being made.
How can I help you explore Laravel packages today?