blendbyte/nova-items-field
A Laravel Nova field for managing repeatable items as JSON. Define item fields in PHP, let editors add, reorder, and remove rows, and store structured array data on your models—ideal for flexible content blocks and simple repeaters.
Architecture fit
This is a presentation-layer primitive, not a subsystem. It is a single Nova field class (~200 lines) that is a thin withMeta() builder over a Vue form component, with no service provider, no config file, no migrations, and no runtime hooks into the application. That means:
composer remove plus a namespace sweep, not an untangling exercise.{key, value} pairs in one JSON column. That is a legitimate "defer normalization" call for small, uniformly-shaped lists — spec links, opening hours, feature bullets, redirect maps, pricing tiers — and it eliminates the migration + model + relation + policy + admin surface that a HasMany would require.key and value are hard-coded in the Vue component; keyLabel()/valueLabel() relabel them cosmetically. Anything with three or more attributes per row is out of scope by construction, and forcing it means stuffing JSON into value, which is where this pattern goes bad. That boundary should be written down as a team convention, because it is the single most common misuse.Integration feasibility
High. The realistic integration is three edits: a json column in a migration, an 'array' cast on the model, and one Items::make() in a resource's fields(). The actual effort — half a day to a day per application — is not in wiring the field but in the adjacent work the package deliberately does not do:
rules() in dot notation (links.*.key, links.*.value), keyed on the attribute name, not the label. This is the classic Nova array-field trap and needs to be a documented convention, not discovered per-resource.Text/Badge field with onlyOnIndex()/onlyOnDetail() is effectively mandatory to keep the index query cheap and to control escaping. Editors see the drag/drop form; the index shows a computed summary.->default() fires only when the attribute is null on form load. It will not backfill existing rows and will not fire against []. Any migration-time seeding has to be done explicitly.Technical risk
| Risk | Severity | Mitigation cost |
|---|---|---|
| Bus factor / supply chain. 14 stars, 0 dependents, one maintainer, releases driven by outside PRs (1.3.5 is a first-time contributor's one-line fix). | Medium–High | Low. MIT, tiny surface. Pin the version; vendor into nova-components/ if it stalls. |
Tag confusion. The 1.3.5 changelog header reads compare/2.0.1...1.3.5 for structural reasons; 2.0.x tags exist. Floating ^1.0 can cross a boundary nobody has read. |
Medium | Low. Pin ^1.3.5 explicitly and lock. Never infer 1.3.5 is a downgrade. |
| PHP 8.4 deprecation noise. Older 1.3.x signatures are implicitly nullable and emit a deprecation per request. | Medium (if unpinned) | Trivial. ^1.3.5 minimum is signature-only, no behavior or storage change. |
| Concurrent edit clobbering. Whole-array overwrite on save; no per-row IDs; last write wins for the entire list. | Medium | Process, not code. Decide which content types are safe for multi-admin editing. |
Not searchable. searchable() will not reach inside the JSON without DB-specific JSON path wiring. |
Medium | Denormalize via observer, or accept the limitation. |
| Rendering/XSS. The field stores exactly what the user types; no sanitization. | Low | e() in every render closure — a lintable convention. |
Empty-vs-null ambiguity. The field submits [] and will overwrite a null column; array rules then fail on null. |
Low | Pick one canonical form, normalize in an accessor. |
Key questions for the team
HasMany is the cheaper long-term call.^1.3.5 in the root composer.json as policy, or trust transitive resolution?Stack fit
Exact and narrow: Laravel Nova 5, PHP 8.4+ (with the 1.3.5 minimum), any Laravel version Nova 5 supports. It does not conflict with anything, but it cannot coexist in composer.json with dillingham/nova-items-field — same class name, different vendor segment. That is a hard constraint for teams mid-migration or running mixed applications.
Migration path
Two distinct migrations are in play and should be sequenced separately:
composer remove dillingham/nova-items-field && composer require blendbyte/nova-items-field, then sweep every resource for Dillingham\NovaItemsField\Items → Blendbyte\NovaItemsField\Items. No data migration — the stored JSON shape is byte-identical. The real work is re-QA on index and detail views, because the field component contract changed on Nova 5.For net-new adoption, there is no migration — just the column, the cast, and the field.
Compatibility
^1.3.5 in the root composer.json and commit the lockfile. Do not float ^1.0 across the 1.3/2.0 tag boundary; read composer show blendbyte/nova-items-field --all before any version change.dist/, so no npm install is needed in CI for normal use. Only customizing the Vue source requires a rebuild (npm run prod) and php artisan nova:publish — skipping the publish is the standard "my change did nothing" because Nova serves the cached copy from public/vendor.Sequencing
rules() in dot notation, keyed to the attribute name.Items on forms only; a computed read-only field on index/detail; e() on every interpolation.[] vs null) and where it gets normalized.Maintenance
Low touch, which is both the appeal and the trap. There is no config to drift, no migrations to run, no background process. The maintenance surface is: the pinned version, the namespace references, and the conventions above. The real maintenance risk is that the low activity looks like a solved problem until a PHP runtime bump lands and an implicit-nullable deprecation starts emitting on every request — so the version pin should be owned, not just set. Budget an hour or two per quarter to check tags and confirm the runtime is still supported.
Support
Two distinct audiences:
->help() on every instance to say so, along with any business rule (e.g., "blank URLs are hidden on the site"). Without that, reorder support tickets are guaranteed.Scaling
nullable|array|max:20 as a rule, and keep index views on a computed summary rather than rendering the full list.HasMany.links_text column via an observer — and be honest that at that point a relational model may be the simpler answer.Nova::script()/Nova::style() or replacing the field. At ~200 lines, a purpose-built field is frequently cheaper than fighting the layout — so treat any "we need a bespoke layout" request as a signal to re-evaluate rather than extend.Failure modes (worth putting in a runbook)
| Symptom | Likely cause | First check |
|---|---|---|
| Form looks right, model is wrong | Double-encoded or empty value | getRawOriginal('links') vs $model->links — expect "[]", null, or double-encoded |
| Per-row errors stuck at field level | Rules keyed to label, not attribute | Rule keys must be links.*.key, matching Items::make('Links', 'links') |
| Custom UI change has no effect | Assets not published | php artisan nova:publish after npm run prod |
| Deprecation logged every request | PHP 8.4 against pre-1.3.5 | Version pin in the lockfile |
| Editor's ordering silently reverted | Accessor/cast re-sorting | Grep for sortBy in casts and accessors |
| Last editor's changes vanished | Concurrent whole-array overwrite | Edit history / who-saved-when |
Ramp-up
->help() explaining the drag handle.nova-components/ if the upstream stalls. Given MIT licensing and the small surface area, that insurance is cheap; the decision to not carry it should be made deliberately, not by default.How can I help you explore Laravel packages today?