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

dillingham/nova-items-field

A Laravel Nova field for managing repeatable items stored in a JSON column. Add, remove, reorder, and search items with a clean, customizable UI. Supports validation, custom templates, and easy integration with Nova resources—ideal for tags, lists, and key-value data.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Verify Nova 3.x first — this gates everything. Run composer show laravel/nova. The field is a Vue 2 / Laravel Mix component (one PHP class in src/, one Vue component in resources/js/). On Nova 4 (Vue 3) or Nova 5 (Vite/PHP 8.1+) it will fail at build time. If you're not on Nova 3, stop and fork instead of installing.

2. Install and pin to an exact minor. Pre-1.0 means a minor bump can break:

composer require dillingham/nova-items-field:0.6.*

Pin 0.6.* (not ^0.6) — 0.6 is where nullable-column support landed (PR #14), and you don't want to drift onto an unexpected release.

3. Create a json column and cast it. The field maps one array to one column:

$table->json('features')->nullable();
// Model
protected $casts = ['features' => 'array'];

The cast is not optional — without it the field will render empty and saves will behave unpredictably.

4. Register it on the resource. The sub-fields you pass to ->fields() define the shape of every repeated row:

use Dillingham\NovaItemsField\Items;

public function fields(Request $request)
{
    return [
        Items::make('Features')
            ->fields([
                Text::make('Label', 'label'),
                Text::make('URL', 'url'),
            ]),
    ];
}

5. First use case — prove the round-trip. Pick a resource with a simple flat ordered list (feature bullets, menu links, FAQ entries). Create a record, add two items, drag to reorder, save, reload, and inspect the raw JSON in the DB. Then create a record with null in the column and confirm it loads and saves without coercing to [].

Where to look first. Read the PHP field class and the Vue component in full before touching config — that is the entire package. There is no service provider hooking the request lifecycle, so debugging is confined to the form component and how it serializes to the model.

Implementation Patterns

Sub-fields are the schema. Every item renders the exact field set you pass. Keep it flat — repeatable list, not nested repeater.

Items::make('Links')
    ->fields([
        Text::make('Title', 'title')->rules('required|string|max:100'),
        Text::make('URL', 'url')->rules('required|url'),
    ])
    ->addLabel('Add link')
    ->removeLabel('Remove');

Put validation in the resource, not the package. The field does not enforce shape. Encode the contract with wildcard rules plus an array-level bound:

public function rules(Request $request)
{
    return [
        'links'         => ['array', 'max:10'],   // add 'nullable' only if null is meaningful
        'links.*.title' => ['required', 'string', 'max:100'],
        'links.*.url'   => ['required', 'url'],
    ];
}

Roll out read-only first. Because the package writes the whole array on save, ship it gated and watch a full lossless round-trip (including null) before granting edit access. Nova's standard field API covers this — the package adds nothing:

Items::make('Features')->readonly(fn () => ! Auth::user()->can('edit-features'))

Reorder is free, but order is not a contract. Drag-and-drop persists as array order in the JSON with no config. Don't depend on that order server-side for anything needing deterministic sorting — sort explicitly on read.

Standardize on one "empty." If null isn't semantically distinct from [] in your domain, normalize in the resource layer (mutator or accessor) so every consumer sees one representation. If it is distinct, document it once and validate accordingly.

Treat the shape as a small public contract. It's a leaf field with no queue, auth, or routing dependencies. Keep the JSON shape internally owned and stable, and validate it in exactly one place. A migration adding a sub-key is a data migration, not just a code change — old rows won't have it:

// backfill when adding a key to an existing shape
DB::table('pages')->whereNotNull('features')->orderBy('id')->each(function ($row) {
    $features = collect(json_decode($row->features, true))
        ->map(fn ($f) => $f + ['external' => false])
        ->all();
    DB::table('pages')->where('id', $row->id)->update(['features' => json_encode($features)]);
});

Testing. In factories/tests, set the column as a PHP array and let the cast handle it. Add one feature test asserting the exact persisted JSON, and one asserting a null record survives an update untouched.

Gate against the relational temptation. The moment you want to search, filter, sort server-side, or relate items to other models, stop — use hasMany plus an inline relationship field. This package is for lists edited as a whole.

Gotchas and Tips

  • Nova 4/5 breaks at build time. Loud staging failure, not silent corruption — honest, but it dates your adoption to your next Nova upgrade. Budget a fork (Vue 3 + Vite, ~3–5 engineer-days with tests) and fork from 0.6 so you inherit the nullable fix.
  • Whole-array write = lost updates. Two admins editing different rows of the same record silently clobber each other. No per-item locking. Fine for low-concurrency internal ops; move to hasMany if concurrent editing is expected.
  • No schema enforcement on the JSON. Types drift ("1" vs 1, null vs [], orphaned keys after a shape change). Any consumer outside Nova — API, export, analytics — must handle it, or normalize in a dedicated resource layer.
  • null vs [] is now a real distinction. 0.6 makes null storable and editable, which means every consumer branches on two "empty" states. Decide the semantics once and document it.
  • Pin exactly. Pre-1.0 semantics: 0.6.*, never ^0.6.
  • CI needs Nova credentials. The private laravel/nova dependency means builds require Nova repo auth in CI, and security scanning on that private dep is limited. Everyone who touches the field inherits that.
  • No vendor support. A Nova license does not extend to third-party fields. Support is GitHub issues. Name an internal fork owner before adopting.
  • Debugging checklist.
    • Field renders empty → confirm the column is json (not text) and the model casts it to array.
    • Save silently no-ops → check the array-level validation rule isn't rejecting the whole key; a failed array rule kills the write without an obvious field-level error.
    • Drag-drop order doesn't persist → confirm the whole array is submitted; partial writes aren't supported.
    • Index view looks wrong → the field is built for form and detail views; don't assume it renders meaningfully on index.
  • Extension points are minimal by design. Min/max item counts, nested repeaters, per-row conditionals, and complex nested shapes are not in 0.6 (which adds nullability only). Verify in a spike before promising them.
  • Don't use it for queryable data. If the list needs search, filter, sort, or relationships, this is the wrong architecture — adopting it for first-class entities converts an accelerator into technical debt.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager