dillingham/nova-grouped-field
A Laravel Nova field that groups multiple form fields into one visual panel, keeping long forms tidy and organized. Drop it into your resource, nest the fields, and Nova renders them together as a single grouped block.
Install and pin. The last release is 0.11 (2020-01-12), pre-1.0, single maintainer, zero dependents. Do not float:
composer require dillingham/nova-grouped-field:0.11.*
Read the source before you read the README. The whole package is a handful of files. Open these first, in this order:
vendor/dillingham/nova-grouped-field/composer.json — the only place a supported Nova/Laravel/PHP matrix would be declared. If it says nova: ^2.0 and you are on Nova 4/5, stop and run the compatibility spike before writing any resource code.vendor/dillingham/nova-grouped-field/src/Grouped.php — the field class. The public setters on this class are the API. The 0.11 release notes describe three features (date formatting, line-break separators, empty-field suppression) but do not name the methods, so confirm the exact method signatures here rather than guessing.src/GroupedServiceProvider.php (if present) — tells you whether assets/views are registered and whether there is anything to publish.First use case: collapse two scalar columns into one index column. Pick a low-traffic resource where related values currently sit in separate columns — contact details are the canonical example:
use Dillingham\NovaGroupedField\Grouped;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Grouped::make('Contact')
->fields([
Text::make('Email'),
Text::make('Phone'),
]),
// ... rest of the resource
];
}
Verify in this order, because each step is cheaper than the next:
null.Until steps 1–3 pass on your Nova major, treat the package as unproven regardless of what the README implies.
Replace column sprawl with a single declarative block. The main day-to-day win is deleting duplicated Text::make() definitions and displayUsing() closures. Before:
Text::make('Email', fn () => $this->email)->onlyOnIndex(),
Text::make('Phone', fn () => $this->phone)->onlyOnIndex(),
After:
Grouped::make('Contact')->fields([
Text::make('Email'),
Text::make('Phone'),
]),
One definition, both index and detail, no closure maintenance.
Stacked cells for dense tables. The 0.11 line-break separator is the difference between a legible name/email column and an unreadable one. Use it where the table is already wide:
Grouped::make('Customer')
->fields([
Text::make('Name'),
Text::make('Email'),
])
->separator("\n"),
Confirm in the source whether the setter is separator() and whether it escapes <br> versus a literal newline differently in index vs. detail — see Gotchas.
Format dates inside the group. The 0.11 date-formatting feature exists so timestamps do not render as raw defaults inside a grouped value:
Grouped::make('Timeline')
->fields([
DateTime::make('Created At'),
DateTime::make('Updated At'),
])
->dateFormat('M j, Y') // verify exact method name in src/Grouped.php
Test this against a non-date attribute and a null value before you trust it — the release notes describe the feature, not its failure modes.
Lean on empty-field suppression; it is the strongest reason to take the dependency. Without it you get Acme — — Amsterdam on partially filled records, which reads as corrupted data to ops staff and generates internal support noise. This is the behaviour worth regression-testing explicitly:
Grouped::make('Address')
->fields([
Text::make('Street'),
Text::make('City'),
Text::make('Postcode'),
])
->separator(', '),
Point it at a record with only City populated and confirm you get Amsterdam, not , Amsterdam,.
Keep grouping definitions inside the resource, never behind a service or model accessor. The package is display-only, so isolating it in fields() means the swap to a custom field or a displayUsing() closure is a one-line revert per resource. That isolation is the entire risk-management strategy.
Standardise separators with a tiny local factory. If you use the group in more than a handful of resources, wrap it so separator and format conventions do not drift between contributors:
// app/Nova/Fields/GroupedList.php
class GroupedList
{
public static function stack(string $name, array $fields): Grouped
{
return Grouped::make($name)->fields($fields)->separator("\n");
}
}
This is also where you would swap in-house if the dependency has to go — the call sites do not change.
Prefer fieldsForIndex / fieldsForDetail over fields when defining the group. The package documents index and detail rendering. Defining the group only where it renders keeps it out of the create/update form path, where a display-only field has no business being. See the first Gotcha.
Eager-load anything relationship-backed. If a grouped field resolves a relation, that resolution happens per row on the index — classic N+1. Group relationship attributes only when the resource's index query already eager-loads them.
A field in fields() renders on forms too. The package advertises index and detail views. If you drop Grouped::make(...) into fields(), Nova will also try to render it on create/update screens, and a display-only field typically produces a blank or broken form control there. Put the group in fieldsForIndex() / fieldsForDetail(), or constrain it with ->onlyOnIndex() / ->onlyOnDetail(). Test the create form explicitly — this is the most likely first failure a developer hits.
You lose per-column sort and filter. Grouping is a scanability trade, not a free win. If operators sort or filter by email, a grouped Contact column removes that affordance. Either keep a sortable duplicate column, or don't group that attribute. Nova will silently give you a non-sortable column header.
Pin exactly and re-validate on every Nova major. Last release predates Nova 4 and 5. The composer constraint is the only compatibility claim the package makes, and it is four years stale. Budget roughly half a day for a spike — read composer.json, render one column in a low-traffic resource — before committing to any rollout. Never assume forward compatibility across a Nova upgrade.
The grouped field's name is not a database column. Grouped::make('Contact') creates a virtual attribute label. Don't collide it with a real column name on the model, and don't expect $model->contact to exist or to be filterable via Nova's ->filterable().
Test all three empty states, not one. Partial (City only), fully empty (all grouped attributes null), and mixed-with-whitespace. Ask specifically: does an all-empty group render blank, a placeholder, or a stray separator? The 0.11 fix covers "prevents separator for empty fields" (PR #7) — verify whether that also covers the all-empty group or only the trailing/leading case. A stray separator on an all-empty row is exactly the artefact the fix was meant to remove.
Separator escaping differs by context. A literal "\n" may render as stacked lines in one context and as whitespace in another; "<br>" may be escaped in one and interpreted in the other. Screenshot the same row in index and detail and compare. Also check whether the separator setter takes a raw string or a rendered fragment.
Check date formatting degradation on non-dates. The 0.11 date formatting presumably assumes a Carbon instance. Point it at a string column and a null, and see whether you get a formatted date, the raw value, an exception, or an empty string. Visual bugs, not exceptions, are the expected failure mode here.
Debugging: inspect the resolved value, not the render. When a group renders wrong, dump what the field actually resolved — the attribute values from the model and whatever the field's meta() / resolved payload contains — before touching the Blade/JS side. Almost every wrong-looking group is empty data or a null-handling path, not a rendering bug. Cross-check with dd($model->only(['email', 'phone'])) first.
Fork contingency is cheap — price it now. The code surface is small enough to vendor a stable copy into app/Nova/Fields/Grouped.php and drop the Composer requirement. Do this if the package fails on your Nova major rather than waiting on a maintainer who has not shipped since January 2020. Keep the local factory wrapper (above) so the swap touches one file.
Document the pinned version and separator conventions in your admin-panel notes. With no declared support matrix and no active upstream, the conventions live in your repo or they live nowhere — and the next developer will re-derive them by trial and error.
How can I help you explore Laravel packages today?