ostheneo/nova-fields
A collection of custom fields for Laravel Nova, adding enhanced form inputs and display components to help you build richer admin panels faster with less boilerplate.
Verify the version matrix before you touch Composer. This package's last release was 2022-05-06, which places it firmly in the Nova 3 / Vue 2 era. Three constraints decide whether it works at all:
composer.json in the package for the nova constraint.php and laravel/framework versions.export default { mixins: [FormField] } vs Vue 3 component API, Mix nova.mix.js vs Vite.Assume at least one will fail on a current stack. Treat that as the baseline, not a surprise.
Read the source before writing any code. The documentation is a single sentence ("A Laravel Nova field"), so the source is the spec. Budget 20 minutes:
git clone https://github.com/OsTheNeo/NovaFields /tmp/nova-fields
cd /tmp/nova-fields
cat composer.json # nova / laravel / php constraints, autoload
ls src/ resources/js/ dist/ # is there a prebuilt bundle?
cat src/*.php # $component string, meta(), resolveUsing/fillUsing
cat resources/js/components/*.vue # Vue 2 or 3? FormField mixin?
Answer three questions from that read:
$component set to?dist/js/field.js exist, or only source?Resolve the dependency on a scratch branch, not main.
composer require ostheneo/nova-fields --dry-run # will it even resolve?
composer require ostheneo/nova-fields
If Composer fails closed on version constraints (expected on Nova 4+), stop here and jump straight to the vendored-fork pattern below. If it resolves and you are genuinely on Nova 3, publish and register assets:
php artisan vendor:publish --provider="Ostheneo\NovaFields\FieldServiceProvider"
php artisan nova:publish
First use case: one field, one resource, prove the round-trip. Pick a low-risk resource and confirm persistence before widening:
use Ostheneo\NovaFields\Fields\YourField; // confirm the exact namespace from src/
public function fields(NovaRequest $request)
{
return [
YourField::make('Settings')
->rules('nullable', 'array')
->help('Custom field — verify persistence before wider rollout'),
];
}
Then save a record in the UI and inspect the model attribute directly. A custom Nova field that renders correctly but serializes wrongly fails silently — the UI is not the test.
Pattern A — pinned dependency (Nova 3 only). Acceptable as a short-lived interim, never a long-term plan. Pin the exact version and commit the lock file in the same change:
"require": { "ostheneo/nova-fields": "1.x.x" }
Add an upgrade-checklist entry at the same time: blocking item on the next Nova major upgrade. Treat this as an alias for "we will own this" — because you will.
Pattern B — vendored fork (the default recommendation). This is how the package earns its place day-to-day. Copy the field class and Vue component into your app, never into vendor/:
mkdir -p app/Nova/Fields/NovaFields resources/js/nova/fields
cp /tmp/nova-fields/src/SomeField.php app/Nova/Fields/NovaFields/
cp /tmp/nova-fields/resources/js/components/*.vue resources/js/nova/fields/
Then rewrite the namespace and register the component yourself in NovaServiceProvider::boot():
Nova::script('nova-fields', __DIR__.'/../resources/js/nova/fields/dist/field.js');
// or, for local dev, import the .vue from your main Nova js entry point
Two mechanical details that bite:
$component property must match the name registered in JS (Nova.booting((app) => app.component('your-field', Component))).composer.json needs the default "App\\": "app/" PSR-4 mapping so the moved class autoloads — run composer dump-autoload after the move if anything is missing.Pattern C — use it as a reference implementation. Often the highest-value outcome. The package is a small, worked example of Nova's field API: make(), rules(), withMeta() for passing props to Vue, resolveUsing(), fillUsing(), and a FormField-based component. Read it, then author a field that matches your domain instead of inheriting someone else's data contract. This also sidesteps the version-matrix problem entirely.
Everyday configuration lives in Nova's standard levers, not the component.
YourField::make('Meta')
->rules('nullable', 'array')
->nullable()
->help('...')
->onlyOnForms() // if only the form view exists
->hideFromIndex()
->withMeta(['placeholder' => 'Optional']); // props your Vue reads
onlyOnForms() is the cheapest guard against the most common Nova custom-field failure: a component that renders the form fine but blows up or renders blank in index/detail views, because those modes were never implemented.
Normalize at the field boundary. Don't let a forked component dictate your storage format. Keep the model's cast as the source of truth and normalize in the field:
YourField::make('Meta')
->resolveUsing(fn ($value) => is_string($value) ? json_decode($value, true) : $value)
->fillUsing(function ($request, $model, $attribute) {
$model->{$attribute} = json_encode($request->input($attribute) ?? []);
})
->rules('nullable', 'array');
Assert the serialization contract, not the pixels. One Pest/PHPUnit test on jsonSerialize() covers more regressions than a browser test:
it('serializes to the expected shape', function () {
$field = YourField::make('Meta')->resolveForDisplay(['meta' => ['a' => 1]]);
expect($field->jsonSerialize())->toHaveKeys(['component', 'attribute', 'meta']);
});
Pair it with one feature test that posts to the Nova update endpoint and asserts the DB value. That pair is what keeps a forked field honest across Nova upgrades.
Roll out behind a flag on a single resource. Verify data round-trip and any CSV/export path, then widen. Keep exactly one internal copy — per-resource variants of a forked admin utility are how small admin utilities become unmaintainable.
Silent frontend breakage is the dominant failure mode. A 2022-era component can compile cleanly and still fail to hydrate or serialize after a Nova upgrade — you get bad data written with no error surfaced. The tell is a Vue warn: Unknown custom element: <your-field> in the browser console: the script never registered, usually because dist/js/field.js wasn't built or Nova::script() wasn't called. Cache-bust with a query hash on Nova::script() when a rebuilt bundle refuses to reload.
Source-only packages ship no dist/. If dist/ is absent, the field renders nothing until you build it yourself with the package's own Mix/Vite config and republish. This is the most common "installed it, nothing appears" cause — budget for it.
Composer failures surface mid-sprint. Version constraints on old major lines block adoption outright on current Nova. If --dry-run fails, don't fight it with minimum-stability or --ignore-platform-reqs; move to the vendored fork. You're not going to win an argument with a May 2022 constraint file.
Namespace and autoload drift when vendoring. Move the class, update the namespace, update use statements in resources, and confirm $component matches the Vue registration name. A mismatch gives you a blank field with no PHP error and no log entry.
Validation is vacuously absent. Seven stars, zero dependents, no CI means nothing has stress-tested nulls, empty arrays, or repeated saves. Add explicit ->rules() and ->nullable() on every usage; never rely on the field to validate itself.
Don't use it on security-adjacent surfaces. Money, PII, permissions, audit paths — no upstream means no patch path, and there's no visible audit trail on the component. Review what the Vue component emits before trusting it: a field that round-trips user HTML into your database without escaping is a stored-XSS vector you inherit wholesale.
No upstream to file against. If you keep it as a dependency, pin by commit hash. If you fork it, keep the fork in-repo under app/Nova/Fields and write the README and upgrade-checklist entry at fork time — not after the engineer who ported it has moved teams.
Debugging checklist, in order:
Unknown custom element).php artisan nova:publish and rebuild assets.dd($field->jsonSerialize()) inside the resource.$component string vs the JS registration name.That sequence resolves most issues in under ten minutes.
Extension points, in order of preference. withMeta() for passing props → resolveUsing() / fillUsing() for the data contract → overriding jsonSerialize() for a view-specific shape → and only then editing the Vue component. Every line you change in that component is a line you re-port at the next Nova major. Keep that surface tiny, or you'll be maintaining a 2022 field forever.
How can I help you explore Laravel packages today?