Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Items Field Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install — Composer only. No service provider, no config file, no migrations shipped by the package.

composer require blendbyte/nova-items-field

Hard requirement: Laravel Nova 5. If the project is on Nova 4, you want the original dillingham/nova-items-field instead — the two packages collide in composer.json (same class name concept, different vendor segment) but produce byte-identical JSON, so switching later needs no data migration.

If the runtime is PHP 8.4+, pin ^1.3.5 as the floor:

"blendbyte/nova-items-field": "^1.3.5"

Earlier 1.3.x tags declare the Items constructor with implicit nullable parameters and emit a deprecation notice on every boot.

Where to look first, in order

  1. README.md — it's short, and it is the API contract. The builder chain (keyLabel, valueLabel, heading, help, default, hideFromIndex/hideFromDetail, rules) is the entire public surface.
  2. src/Items.php — roughly 200 lines. Every builder method is a thin withMeta() call, so reading it tells you exactly what props the Vue component receives. This is the file to open when a fluent method "doesn't do anything."
  3. resources/js/components/FormField.vue — defines the drag/drop behaviour and the exact persisted shape. Read before customising.
  4. dist/ — compiled assets are committed to the repo, so no npm install is required for normal use.

The data shape is one JSON column holding an ordered array of exactly two keys:

[
  { "key": "GitHub", "value": "https://github.com/blendbyte" },
  { "key": "Docs",   "value": "https://example.com/docs" }
]

Minimum viable wiring

// migration
$table->json('links')->nullable();

// model
protected $casts = ['links' => 'array'];
use Blendbyte\NovaItemsField\Items;

public function fields(Request $request)
{
    return [
        Items::make('Links')
            ->keyLabel('Label')
            ->valueLabel('URL')
            ->heading('Links')
            ->help('Drag the handle to reorder. Blank URLs are hidden on the site.')
            ->rules('array')
            ->onlyOnForms(),

        Text::make('Links', fn () => collect($this->links ?? [])
                ->map(fn ($l) => '<a href="'.e($l['value']).'">'.e($l['key']).'</a>')
                ->implode(' · '))
            ->asHtml()
            ->onlyOnIndex(),
    ];
}

First use case to ship — a "spec links" or "opening hours" block on a settings/page resource. Two attributes per row, ordering matters, no need to query across rows. That's the sweet spot and it takes under ten minutes end-to-end.


Implementation Patterns

It is a "list of pairs" primitive, not a links field

The columns are hard-coded as key and value; the label methods are cosmetic only. Reach for it whenever your row shape is exactly two fields:

Use case keyLabel valueLabel
Spec links Label URL
Opening hours Day Hours
Feature bullets Title Description
Redirect map From To
Product specs Name Value
Pricing tiers Plan Price

Three or more attributes per row means you've picked the wrong tool. The failure mode is stuffing a second JSON object into value and then hand-decoding it in every render closure.

Keep the field editable-only; render elsewhere

The pattern that holds up in production: Items on forms only, a computed read-only field on index and detail. That keeps index queries cheap and puts escaping and link targets under your control.

Text::make('Links')
    ->displayUsing(fn () => count($this->links ?? []).' link(s)')
    ->onlyOnIndex(),

Validation lives in dot notation, in the resource

->rules('array') on the field only validates the outer envelope. Per-row rules belong in rules() or a form request:

public static function rules(NovaRequest $request)
{
    return [
        'links'         => 'nullable|array|max:20',
        'links.*.key'   => 'required|string|max:100',
        'links.*.value' => 'nullable|url|max:2048',
    ];
}

Per-row error messages are the main reason to pick this over a Textarea full of raw JSON. Keep the top level nullable — an empty list can submit as [] or be omitted entirely, and a bare array rule fails on null.

Ordering is data — never re-sort in an accessor

Drag/drop is client-side only; the persisted array order is the sort order. An accessor doing sortBy('key') silently discards whatever the editor arranged. If a particular surface needs a deterministic order, sort inside the render closure, never in the cast or accessor.

Defaults, factories, seeding

Items::make('Links')->default([
    ['key' => 'Website', 'value' => 'https://example.com'],
]),
// factory
'links' => [['key' => 'Twitter', 'value' => 'https://twitter.com/acme']],

->default() fires only when the attribute is null on form load. It will not backfill existing records, and it will not fire when the column already holds [].

Denormalise when you need search

Nova's searchable() cannot reach inside the JSON blob. If link labels must be searchable, mirror them into a flat column via an observer:

static::saved(function ($page) {
    $page->links_text = collect($page->links)->pluck('key')->implode(' ');
    $page->saveQuietly();
});

Then Text::make('Links', 'links_text')->searchable() works normally. Once you need this plus per-row identity, a HasMany is usually the cheaper answer.

Migrating off the dillingham original

  1. composer remove dillingham/nova-items-field && composer require blendbyte/nova-items-field
  2. Sweep every resource: Dillingham\NovaItemsField\Items → Blendbyte\NovaItemsField\Items
  3. No data migration — same JSON shape, same keys.
  4. Re-QA index and detail on Nova 5; the field component contract changed, which is the reason the fork exists at all.

Composing within a form

  • ->onlyOnForms() plus a computed read-only field is the default wiring.
  • ->heading() draws a label above the list — enough visual grouping to avoid a custom panel component in most cases.
  • ->help() is the only place you can advertise drag/drop; nothing in the UI hints at it beyond the handle itself.
  • Nest a Section around it and you get a reasonable "Structured content" grouping for free.

Gotchas and Tips

Pin ^1.3.5 on PHP 8.4+. Older 1.3.x signatures are implicitly nullable and emit a deprecation per request. The fix is signature-only, so the bump is low risk. Anyone floating on 2.0.x should run composer show blendbyte/nova-items-field --all and read the tags before assuming "downgrade."

The 1.3.5 changelog header is misleading. It opens with compare/2.0.1...1.3.5 and then states the real range as compare/1.3.4...1.3.5. GitHub picked a non-ancestor tag as the default diff base. Do not infer that 1.3.5 is a rollback of 2.0.1. Pin explicitly rather than letting ^1.0 float across an unread tag boundary.

Empty list is [], not null. The field happily submits [] over a previously null column. Pick one as canonical and normalise in an accessor, otherwise every consumer needs ?? [] and every count() needs a guard.

key and value are hard-coded in the Vue component. keyLabel() and valueLabel() only relabel. Different storage keys means forking the component — at which point writing a purpose-built field is usually cheaper.

Whole-array overwrite on save. One JSON column, no per-row IDs. Two admins editing different rows of the same record clobber each other; last write wins for the entire list, and you cannot diff or merge. If the data is genuinely collaborative, it belongs in HasMany.

Not searchable out of the box. See the observer pattern above; don't waste time trying ->searchable() on this field.

Validation keys follow the attribute, not the label. Items::make('Links', 'links_json') means your rules are links_json.*.key. Classic Nova array-field trap that surfaces as errors stuck at field level instead of attached to the offending row.

Escape on render. The field stores exactly what the user typed — no sanitising on write. Always e() values before interpolating into an anchor or an asHtml() field.

Patching assets requires rebuild and publish.

cd vendor/blendbyte/nova-items-field
npm install && npm run prod
php artisan nova:publish

Skipping nova:publish is the classic "my change did nothing" — Nova serves the cached copy from public/vendor.

Debug the raw payload, not the cast. When the form looks correct but the model is wrong:

dump($page->getRawOriginal('links')); // what actually hit the DB
dump($page->links);                   // what the cast produced

Nine times out of ten the raw value is "[]", null, or double-encoded because something json_encoded data that was already encoded.

Extension points are thin. Beyond the builder methods, customisation means overriding Vue components via Nova::script() / Nova::style(), or replacing the field outright. At ~200 lines, a bespoke implementation is frequently cheaper than fighting the layout.

Low bus factor — treat it accordingly. 14 stars, zero dependents, one maintainer, releases driven by outside PRs (1.3.5 is a first-time contributor's one-liner). It's MIT and tiny, so pin the version and keep vendoring into nova-components/ on the table as cheap insurance.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource