doode/filament-map-picker
A Filament form field that lets users pick a location on an interactive map, saving latitude and longitude directly to your model. Drop it into any resource form for fast, accurate address or coordinate selection with minimal setup.
This is a small, single-purpose Filament form field: it renders a Leaflet map with Leaflet-Geoman draw controls and stores the picked geometry (marker, polyline, polygon, rectangle, circle) as GeoJSON-ish state. As of v1.6.5 the package is explicitly supported on Laravel 10, 11 and 12 and ships a test suite, so it's a safer bet than its star count suggests — but the source is still effectively the documentation. Budget an hour in src/ and resources/js before committing.
composer require doode/filament-map-picker
php artisan vendor:publish --tag="filament-map-picker-assets" # tag name may differ - check README
Laravel ≠ Filament. v1.6.5 nails down the Laravel constraint, but the thing that will actually break your install is the Filament major version — v3 and v4 field APIs diverge. Read the filament/filament constraint in the package's composer.json before installing, and pin the package version.
If the package ships a plugin class, register it in your panel provider (->plugin(MapPickerPlugin::make())); if it is a pure form component (common for map pickers), skip this and just use the field.
First use case: a Venue resource where an admin drops a pin instead of typing lat/lng.
use Doode\FilamentMapPicker\Forms\Components\MapPicker; // confirm namespace in src/
MapPicker::make('location')
->label('Location')
->setLocation(51.5074, -0.1278) // initial centre / pin
->zoom(13)
->height(400)
->required(),
Store it as JSON:
protected $casts = ['location' => 'array'];
Where to look first: the field's src/Forms/Components/*.php (what public setters exist), the Blade view + Alpine component (how state is written back to Livewire), and the JS bundle (which Geoman modes are actually enabled). That trio answers 90% of questions. The new tests/ directory is a decent fourth stop — it shows the exact payload shape the field hydrates and dehydrates, which is faster than reverse-engineering the Alpine.
Point-only vs. shape. For a simple address pin, leave draw controls off and read $state['lat'] / $state['lng'] (verify the exact key names against the JS). For service areas, delivery zones, or catchments, enable the drawing control, constrain it to one shape mode, and store the resulting GeoJSON feature collection directly in a json column.
Denormalise for queries. Never query against the JSON blob. In mutateFormDataBeforeSave() (or afterStateUpdated) extract what you need into real columns:
->mutateFormDataBeforeSave(function (array $data) {
$geo = $data['location'] ?? null;
$data['lat'] = $geo['lat'] ?? null;
$data['lng'] = $geo['lng'] ?? null;
return $data;
})
Add a MySQL spatial index / POINT column only if you actually need radius queries; otherwise plain decimal(10,7) columns plus a bounding-box whereBetween will carry you a long way.
Validate the shape, not just presence. ->required() passes on an empty array in some Filament versions. Add a closure rule that asserts the geometry type and a sane coordinate range (-90..90, -180..180) before it hits the database.
Repeater / multi-location. Each map instance needs a unique wire key. Test this early — nested map pickers inside a Repeater are the most common place these packages break, because the Leaflet instance isn't torn down on row deletion. The v1.6.5 tests are PHP-side and won't catch this; you have to click it yourself.
Read-only display. There is generally no infolist entry. For view pages, either a TextEntry with the coordinates plus a static-map image, or embed a <div> with Leaflet in a custom Blade view. Don't try to reuse the form component in read-only mode unless the package explicitly supports it.
Geocoding is your job. The picker gives you coordinates from clicks and dragged pins; it does not do address search. Wire a Nominatim/Mapbox/Google lookup into a sibling TextInput with ->live(debounce: 500) and ->afterStateUpdated(), then push the result into the map field with ->setLocation() on the parent form's ->fill(), or use $set('location', [...]) from the search field.
Saving GeoJSON for later rendering. If you need to render the zone back on a public-facing map, store the raw GeoJSON string and feed it straight to Leaflet or Mapbox GL on the front end — don't round-trip it through your own model.
Borrow from the test suite. v1.6.5 added tests and QA passes. Rather than guessing what array shape the field emits, copy a fixture out of tests/ into your own Pest/PHPUnit test and assert your mutateFormDataBeforeSave() handles it. That gives you a regression net for free across future upgrades.
[lng, lat], not [lat, lng]. This is the single most common bug. Leaflet's own LatLng and GeoJSON's coordinate array use opposite orders. Put a test around any conversion.leaflet.css / geoman. Every release — including patch releases like 1.6.5 — is a candidate for stale published assets; re-run vendor:publish --force when the map behaves oddly after composer update.map.invalidateSize() when the parent becomes visible, or set an explicit pixel height via ->height().wire:ignore if you patch the view; most implementations do this already, but verify after any Filament upgrade.->live() is on, you'll round-trip to the server on every frame. Use ->live(debounce: 500) or ->dehydrated(false) with a deferred save.package.json in the published assets rather than trusting the repo's top-level MIT badge (which may only cover the PHP wrapper).null or [] — the two behave differently through ->required() and through your cast. Normalise in mutateFormDataBeforeSave.composer.json ("doode/filament-map-picker": "1.6.5") and add an integration test that asserts the field hydrates and dehydrates a known payload, so a future minor bump can't silently change the state shape on you.img-src and connect-src allowances for the tile host. On HTTPS pages, any http:// tile URL silently fails.How can I help you explore Laravel packages today?