mostafaznv/nova-map-field
A Laravel Nova field that adds an interactive map to your forms, letting users search locations or click to store latitude and longitude coordinates, with support for multiple map providers, drawing tools, and customizable markers.
This is a Nova field package, not a framework. "Getting started" means proving the compatibility triple, installing it pinned, wiring it into one resource, and confirming a coordinate round-trip.
1. Verify compatibility before installing. Release 4.4.0 confirms Laravel 13 support — a single PR widening framework constraints. It says nothing about Nova majors. Nova's field API evolves independently of Laravel, so check the README matrix for your exact (Nova, Laravel, PHP) combination and treat the changelog as a signal, not a guarantee.
2. Install pinned and commit the lockfile:
composer require mostafaznv/nova-map-field:^4.4
4.4.0 is a non-breaking constraint widening, so staying inside 4.x is low risk. Re-run your staging smoke test after any composer update, because transitive resolution can move under you.
3. Publish config and rebuild Nova assets. Depending on your Nova version this is nova:publish and/or npm run build / vite build. A blank, grey, or unstyled map is almost always this step or a missing provider key — not the field code. Do this before debugging the resource.
4. Create plain decimal columns, never a JSON blob:
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->string('address')->nullable(); // optional, if you capture it
decimal(10,7) gives ~1cm precision and is what you want for lat/lng. Separate columns mean replacing the field later is a UI edit, not a migration — that is the entire containment story for a 46-star, zero-dependent package.
5. Register the field on exactly one resource:
use Mostafaznv\NovaMapField\Fields\NovaMapField;
NovaMapField::make('Location')
->latitude('latitude')
->longitude('longitude')
->height(400)
->zoom(14)
->center(51.5074, -0.1278);
Verify exact method names against the README for your 4.x version — the fluent API has shifted between majors and the package description is thinner than the source.
6. First use case: a single low-risk admin resource where an operator currently types coordinates — Store, Venue, Warehouse, JobSite. Keep the numeric lat/lng inputs visible alongside it until operators confirm the picker is faster and more accurate.
Where to look first when something is wrong: (a) Nova assets built and config published, (b) browser console for 404s on field JS/CSS, (c) provider API key present and referrer-restricted, (d) php artisan tinker on a saved record to confirm the columns actually changed.
Wrap the vendor field in a thin local subclass. With this package's profile — small star count, no dependents, a maintainer roster that only recently widened past one author — this is the highest-value pattern you can adopt. A package swap, a fork, or a hand-rolled Vue component becomes a one-file change across every resource:
namespace App\Nova\Fields;
use Mostafaznv\NovaMapField\Fields\NovaMapField as BaseMapField;
class MapPicker extends BaseMapField
{
public function __construct($name, $attribute = null, $resolveCallback = null)
{
parent::__construct($name, $attribute ?? 'location', $resolveCallback);
$this->latitude('latitude')
->longitude('longitude')
->zoom(config('maps.default_zoom', 14))
->height(config('maps.field_height', 400))
->center(config('maps.center.lat'), config('maps.center.lng'));
}
}
Now MapPicker::make('Location') is the only call site in your app, and defaults live in config rather than being copy-pasted across resources.
Always pair the map with numeric coordinate inputs. The map is a pointer-driven control with no keyboard path. Number::make('Latitude')->step('any') and the longitude equivalent solve three problems simultaneously: fine precision editing, keyboard-only accessibility, and a manual fallback when the map JS or the tile provider fails:
$fields = [
MapPicker::make('Location')
->rules(['nullable', 'required_with:longitude', 'numeric', 'between:-90,90']),
Number::make('Latitude')->step('any')->hideFromIndex()
->rules(['nullable', 'required_with:longitude', 'numeric', 'between:-90,90']),
Number::make('Longitude')->step('any')->hideFromIndex()
->rules(['nullable', 'required_with:latitude', 'numeric', 'between:-180,180']),
];
Note the required_with pairing: coordinates should be both-present or both-null. A half-populated row is the most common data bug from map pickers, because a marker that was dropped and then cleared leaves one side behind.
Validate on the server; the field enforces nothing. ->zoom(14) and a default centre are UI hints. They do not constrain what gets saved. Bounds checks, required-with pairing, and any "must be inside our service region" rule belong in Nova rules or a FormRequest — never assumed from the widget.
Layer address search carefully. If the field exposes address search/geocoding, treat it as UI convenience only. Reverse-resolution, bulk geocoding of existing rows, and any "fill address from pin" behaviour belong in a queued job with rate limiting, caching, and idempotency. Never let an operator's save trigger a synchronous billable geocoding call.
Backfill asynchronously and re-runnably. When migrating existing Store/Venue rows that have addresses but no coordinates (or vice versa), geocode in a queued job behind a rate limiter. Make it idempotent — skip rows already populated — so a partially completed backfill resumes rather than restarting.
Fan out slowly, gated by config. The package is presentation-layer only: no models, no migrations, no API surface. Incremental adoption is free, and so is rollback:
if (config('features.map_picker')) {
$fields[] = MapPicker::make('Location');
} else {
$fields[] = nova_map_fallback_fields();
}
Run it on Store for a sprint, gather operator feedback, then roll to Venue and Warehouse. Because coordinates live in your own columns, pulling the field changes nothing in the database.
Detail and index views. The description claims it works in forms, detail, and index views. Practically, use it as a form input; on detail/index prefer a read-only rendering of your own lat/lng columns and, if useful, a link out to a static map. Do not assume index-view rendering is cheap — a map component per row multiplies tile requests and provider quota.
CSP integration is part of the integration. Add your tile/style/marker hostnames to img-src and, if the field calls a search or geocoding endpoint client-side, to connect-src. This is the classic reason a map that renders in local dev shows as a grey rectangle in production.
Test the round-trip, not the widget. You cannot meaningfully unit-test a JavaScript map. What you can test:
it('persists coordinates from a payload', function () {
$store = Store::factory()->create(['latitude' => null, 'longitude' => null]);
// simulate the Nova update payload
$store->update(['latitude' => '51.5074', 'longitude' => '-0.1278']);
expect($store->fresh())
->latitude->toEqual('51.5074000')
->longitude->toEqual('-0.1278000');
});
Plus a validation test for the both-or-neither rule and an out-of-bounds rejection. That covers every failure mode the field can actually produce.
Nova coverage is under-documented relative to Laravel. Never infer Nova-major support from the Laravel 13 changelog line. Prove it on staging with your Nova version before assuming anything, and re-check on every Nova upgrade — that is the axis most likely to break silently.
Compatibility may be asserted rather than tested. A composer.json constraint widening and a CI matrix run are different things. Ask (or check the repo's CI config) whether 4.4.0 is actually exercised against Laravel 13 with your Nova major. Your own staging build is the authoritative answer.
"It saves nothing" debugging order:
->latitude() / ->longitude() point at the exact column names in the migration? Typos fail silently.php artisan tinker — if the DB changed but the form reverts, it's a hydration/resolve issue.prepareForValidation(), an observer, or a model mutator that recomputes coordinates?Precision and the hemisphere are where hand-entered data goes wrong. decimal(10,7), not float and not string. Sign matters: the classic errors are swapped lat/lng and a dropped minus for southern/western hemispheres. If any data was hand-entered before the picker, run a one-off sanity sweep — pins outside your expected operating region are visible on a map and cheap to fix now.
Provider keys may be exposed client-side. Determine whether search/geocoding runs in the browser (key visible, quota abusable) or server-side. Either way: restrict the key by HTTP referrer or origin at the provider, set billing/quota alerts, and note that map requests can leak user-entered addresses to a third party. Flag that to security/legal before production if you operate under GDPR-style regimes.
Tile and asset licensing is not the package's MIT licence. The package is MIT; map tiles, styles, marker icons, and fonts carry their own terms. Confirm provider ToS separately, especially for offline, air-gapped, or internal-only deployments. Remember this is an admin input — customer-facing map rendering is a wholly separate decision with its own licensing.
The usual first breakage is front-end build and asset publishing. When the map is blank or unstyled, check in this order: config/assets published → npm run build / vite build completed for your Nova instance → browser console 404s on field JS/CSS → CSP blocks → provider API key.
Height, zoom, and centre are cosmetic. They do not constrain or validate anything. Enforce between:-90,90 and between:-180,180 server-side, and pair the nullable columns with required_with so half-populated rows cannot be saved.
Don't store coordinates as JSON. If you are tempted to serialize {lat, lng} into a single column, don't: it forecloses swapping to a custom field or plain numeric inputs, and it makes distance queries and index sorting awkward. Separate decimals are the containment plan.
Accessibility is a hard constraint, not a nice-to-have. Map picking is inherently pointer-driven. If any operator works keyboard-only or on a locked-down browser, the numeric fields are mandatory. Decide at pilot time — retrofitting after rollout is friction with the ops team and a likely accessibility complaint.
Extension points are limited to what the field exposes. Before you need a custom behaviour (map provider change, marker clustering, a second pin), check the README's config and fluent options first. If the option doesn't exist, the intended route is a local subclass or a custom Nova field — not patching vendor/, which will be lost on the next composer update.
Release cadence is a signal, not a guarantee. 4.4.0 landed promptly for Laravel 13 and a first-time external contributor appeared — genuinely positive maintenance evidence, but it's one data point. Keep the pinned version and the thin subclass wrapper so a stalled upstream is an afternoon's work, not a project.
There is no vendor support path. Acceptable for a supporting admin field; not acceptable on the critical path of a revenue feature with an SLA. Confine it to a handful of resources and treat it as a convenience layer over coordinate columns you own and can fall back to.
How can I help you explore Laravel packages today?