arbermustafa/filament-locationpickr-field
A Filament form field for picking a location on an interactive map. Users drop or drag a marker to capture latitude and longitude, with search and geocoding built in. Drop-in ready for Filament admin panels and forms, with configurable map options.
Install the package
composer require arbermustafa/filament-locationpickr-field
It's a Filament form field, so it assumes a working Filament panel (v2 or v3 — check the composer.json require constraints of the release you pull before upgrading Filament).
Enable the right Google APIs in Google Cloud Console — this trips up more people than anything else:
Provide the API key. Put it in .env and expose it to the frontend (the JS SDK needs it client-side):
GOOGLE_MAPS_API_KEY=AIza...
The package typically reads this via config or you pass ->apiKey(...) on the field. Confirm which the installed version uses by reading its service provider / README.
Publish assets if required and clear caches:
php artisan vendor:publish --tag=filament-locationpickr-field-config
php artisan filament:assets
php artisan optimize:clear
First use case — a "hub location" field on a Locations resource:
use ArberMustafa\FilamentLocationPickrField\Components\LocationPickr; // verify namespace
LocationPickr::make('location')
->label('Pick location')
->defaultLocation([40.7128, -74.0060]);
Render the form, drag the pin, save, and inspect what got written to the DB — the exact state shape (array vs. "lat,lng" string) is the first thing you need to nail down.
Look first at: the README's usage block, the field class (src/), and the Alpine/JS component in resources/js/. Those three tell you the state contract and available fluent options.
1. Decide your persistence shape up front. Two common options:
// Single JSON column
$table->json('location')->nullable();
// Split columns — let the picker drive them
LocationPickr::make('location')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('latitude', is_array($state) ? ($state['lat'] ?? null) : null);
$set('longitude', is_array($state) ? ($state['lng'] ?? null) : null);
}),
If you go with split columns, normalize on hydrate too (->formatStateUsing() or ->afterStateHydrated()) so editing an existing record re-centers the map correctly.
2. Normalize state, don't rely on it. Wrap the raw state in an accessor/cast on the model so the rest of the app gets a consistent {lat, lng} shape regardless of what the JS emits.
3. Use dehydrateStateUsing for display formatting. Anything that needs to store "lat,lng" as a string, or a full address, should be shaped there rather than in a mutator — this keeps the form field the single source of transformation.
4. Chain with a plain address field. The picker handles coordinates; a separate TextInput for a human-readable address is usually cleaner than trying to make the map do everything. If Places autocomplete is enabled, listen via afterStateUpdated to seed the address text.
5. Use it in tables with a link, not a map. There is no bundled table column, so display coordinates in a TextColumn and add a url() to https://www.google.com/maps?q={lat},{lng} for quick verification. A static map thumbnail is fine too, but costs API quota.
6. Wrap it. Given 0 dependents and a low activity signal, create your own app/Forms/Components/LocationPicker.php extending the package field, centralize the API key and default options, and call that everywhere in your resources. Swapping the underlying dependency later becomes a one-file change instead of a grep across every form.
Key restrictions, not key secrecy. The Maps JS key is inherently public. Restrict it by HTTP referrer and by API (Maps JS, Places, Geocoding). If the map renders grey with "This page didn't load Google Maps correctly", it's almost always restriction or billing, not the package.
Places is a separate enable and a separate bill. Autocomplete silently fails if Places API isn't enabled even when Maps JS works fine.
Blank container / no JS errors → check that assets are published and loaded. In Filament v3, php artisan filament:assets and php artisan optimize:clear fix 90% of "the field renders as an empty box" reports. Also confirm your layout includes @filamentScripts.
Livewire re-render kills the map. Any reactive sibling field can trigger a Livewire update that re-runs init() and either duplicates pins or blanks the map. If you see flicker, check the Alpine component for a re-init guard, and prefer ->live(onBlur: true) on neighbors instead of continuous updates.
Multiple instances / repeaters. If the package uses a fixed DOM ID for the map container, two pickers on the same form (or inside a Repeater) will collide. Test this early; if it breaks, you'll need a wrapper that supplies unique IDs.
->required() doesn't mean "user moved the pin." It validates Livewire state, not human interaction. If the default location pre-fills, required passes with zero clicks. Add an explicit "location confirmed" toggle if the choice matters (e.g. delivery address).
Confirm the state shape before writing migrations. Log $state inside afterStateUpdated on your first integration. The difference between ['lat' => ..] and "lat,lng" will otherwise bite you when a record is edited.
Testing: don't simulate the map in browser-less tests. Fill the state directly — livewire(CreateLocation::class)->fillForm(['location' => ['lat' => 40.7, 'lng' => -74.0]])->call('create') — and assert on the saved coordinates.
Dark mode: Google Maps doesn't follow Tailwind's dark: class. If the map is visually jarring, pass a styles array (many packages expose ->mapOptions([...])); otherwise accept the mismatch.
Stability risk. With 15 stars and 0 dependents, treat this as a "use behind an abstraction" dependency: pin the version in composer.json, add a smoke test that fills the field's state, and keep a plain latitude/longitude text-input fallback so a Filament major upgrade can't take your form down.
How can I help you explore Laravel packages today?