| Install | |
|---|---|
composer require sanderdewijs/lara-livewire-maps |
|
| Latest Version: | v2.1.1 |
| PHP: | ^8.2 |
A lightweight Livewire v3 map component for Google Maps. It renders a map, places markers (optionally clustered), and lets users draw a selection (circle or polygon). When a selection is completed, events are dispatched with the markers inside the shape. If no markers are inside, useful selection metadata is returned.
Works out-of-the-box with Laravel 12 and Livewire 3.
Install via Composer:
composer require sanderdewijs/lara-livewire-maps
This package ships a Blade include directive that loads the required JavaScript for the map and (optionally) the Google Maps API. Place the directive once per page, ideally immediately after the opening
tag in your main layout.Example layout:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My App</title>
@vite(['resources/js/app.js'])
@livewireStyles
</head>
<body>
{{ $slot ?? '' }}
@livewireScripts
@LwMapsScripts
</body>
</html>
Notes:
The @LwMapsScripts directive loads the Google Maps JS API (drawing and geometry libraries) and the MarkerClusterer library. You can control this behavior via config (see below).
You can configure package-wide defaults via config/livewire-maps.php. To publish the config file into your app, run:
php artisan vendor:publish --provider="Sdw\\LivewireMaps\\LivewireMapServiceProvider" --tag="livewire-maps-config"
You can also use the generic config tag:
php artisan vendor:publish --tag=config --provider="Sdw\\LivewireMaps\\LivewireMapServiceProvider"
After publishing, edit config/livewire-maps.php. Supported keys:
GOOGLE_MAPS_API_KEY in your .env)LW_MAPS_GOOGLE_KEY in your .env)drawing,geometry)nl, en)LW_MAPS_AUTO_FIT_BOUNDS=false to disable)vite | mix | cdn | file | none (default file)cdn)vite, default resources/js/livewire-maps.js)mix, default /vendor/livewire-maps/livewire-maps.js)By default, asset_driver is file, which expects the package JS to be published to public/vendor. The @LwMapsScripts directive will then include it automatically.
File (default, no bundler):
php artisan vendor:publish --provider="Sdw\\LivewireMaps\\LivewireMapServiceProvider" --tag=livewire-maps-assets
# or
php artisan vendor:publish --provider="Sdw\\LivewireMaps\\LivewireMapServiceProvider" --tag=public
asset_driver as file (default). The directive will include /vendor/livewire-maps/livewire-maps.js.# publish (creates the directory if missing)
php artisan livewire-maps:publish-assets
# force overwrite if the file already exists
php artisan livewire-maps:publish-assets --force
This command copies the package asset from resources/js/livewire-maps.js to public/vendor/livewire-maps/livewire-maps.js.
Vite:
.env or config, set LW_MAPS_ASSET_DRIVER=vite (or asset_driver => 'vite').resources/js/livewire-maps.js in your app with:// resources/js/livewire-maps.js
import '../../vendor/sanderdewijs/lara-livewire-maps/resources/js/livewire-maps.js';
laravel({
input: ['resources/js/app.js', 'resources/js/livewire-maps.js'],
refresh: true,
})
vite_entry in config/livewire-maps.php if you use a different path.Laravel Mix:
.env or config, set LW_MAPS_ASSET_DRIVER=mix (or asset_driver => 'mix').resources/js/livewire-maps.js that imports the package script (same import as Vite example).webpack.mix.js:mix.js('resources/js/livewire-maps.js', 'public/vendor/livewire-maps').version();
mix_path in config matches /vendor/livewire-maps/livewire-maps.js.CDN:
asset_driver to cdn.cdn_url to the full URL.None (advanced):
asset_driver to none if you want to fully control script loading yourself. In this mode, @LwMapsScripts will not include any JS; you must load both the package JS and (optionally) the Google Maps API on your own.Google Maps loading:
load_google_maps is true and a key is present (google_maps_key or api_key).load_google_maps=false if you prefer to include the Google script tag elsewhere.Render a map with a couple of markers:
@php
$markers = [
['id' => 1, 'lat' => 52.0907, 'lng' => 5.1214, 'label_content' => '<strong>Utrecht</strong>', 'title' => 'Utrecht'],
['id' => 2, 'lat' => 52.3676, 'lng' => 4.9041, 'title' => 'Amsterdam'],
];
@endphp
<livewire:livewire-map
:zoom="7"
:center-lat="52.0907"
:center-lng="5.1214"
height="360px"
:markers="$markers"
/>
Start drawing immediately (circle or polygon) by passing the drawType property:
<livewire:livewire-map
:zoom="7"
:center-lat="52.0907"
:center-lng="5.1214"
:markers="$markers"
:draw-type="'circle'"
/>
All properties are optional unless noted. Use as Livewire props on the component tag.
init_event config if both are set.maps_placeholder_img), shows a background image covering the container until the map initializes.Sometimes you want the map to initialize only after other backend work has completed. You can configure a custom event name globally or per component and dispatch it when you're ready.
init_event in config/livewire-maps.php or .env LW_MAPS_INIT_EVENT=my-app:maps:init<livewire:livewire-map :init-event="'my-app:maps:init'" />
When you're ready to initialize (e.g., after backend work completes), dispatch a Livewire/Alpine event with the same name as init_event.
Frontend (Alpine/Livewire in your Blade):
<!-- Ensure your map component has initEvent or config init_event set to 'my-app:maps:init' -->
<button type="button" x-on:click="$dispatch('my-app:maps:init')">Init map</button>
You can include overrides in the payload (all keys optional). These will be shallow-merged into the initial config:
<button type="button"
x-on:click="$dispatch('my-app:maps:init', {
lat: 52.09,
lng: 5.12,
zoom: 8,
markers: [ { id: 1, lat: 52.09, lng: 5.12 } ],
useClusters: true,
clusterOptions: { maxZoom: 14 },
mapOptions: { disableDefaultUI: true },
drawType: 'circle',
autoFitBounds: false,
})">
Init with overrides
</button>
Backend (Livewire PHP component):
// From your Livewire component when data is ready
$this->dispatch('my-app:maps:init',
lat: 52.09,
lng: 5.12,
zoom: 8,
markers: [ [ 'id' => 1, 'lat' => 52.09, 'lng' => 5.12 ] ],
useClusters: true,
clusterOptions: [ 'maxZoom' => 14 ],
mapOptions: [ 'disableDefaultUI' => true ],
drawType: 'circle',
autoFitBounds: false,
);
Notes:
init_event is null (default), the map initializes immediately on render (current behavior).You can provide any of the following forms:
Notes:
The map supports starting draw mode in three ways:
lw-map:update: include drawType: 'circle'|'polygon' to enable drawing immediately in the same update round-trip.lw-map:draw).When the user completes the shape, the component computes which markers fall inside and dispatches a selection-complete event with results.
This package initially had support for custom browser events, but this will change to only Livewire event support for simplicity.
Example:
window.addEventListener('lw-map:ready', (e) => {
const { id, map } = e.detail;
console.log('Map ready', id, map);
});
lw-map-internal-update which the component emits after normalizing data. You should not dispatch this internal event yourself.Examples (from a Livewire PHP component):
// Update only markers (no clustering)
$this->dispatch('lw-map:update', markers: [
['lat' => 52.0907, 'lng' => 5.1214, 'title' => 'Utrecht'],
['lat_lng' => '52.3676,4.9041', 'title' => 'Amsterdam'],
]);
// Update markers and enable clustering
$this->dispatch('lw-map:update', markers: [
['lat' => 52.0907, 'lng' => 5.1214],
['lat' => 52.3676, 'lng' => 4.9041],
], useClusters: true);
// Update markers, enable clustering, and pass cluster options
$this->dispatch('lw-map:update', markers: [
['lat' => 52.0907, 'lng' => 5.1214],
['lat' => 52.3676, 'lng' => 4.9041],
], useClusters: true, clusterOptions: ['maxZoom' => 14]);
Notes:
lat/lng, lat_lng array, or lat_lng string).lw-map:update from the browser; use your Livewire PHP component.Runtime drawing via lw-map:update (single round-trip):
drawType: 'circle'|'polygon' in the same lw-map:update call to immediately enable the drawing tools. No separate lw-map:draw dispatch is needed.drawType to leave the current drawing state unchanged.// Update center/zoom and start drawing a circle immediately
$this->dispatch('lw-map:update',
markers: [
['lat' => 52.0907, 'lng' => 5.1214, 'label_content' => 'Utrecht'],
],
useClusters: true,
clusterOptions: ['maxZoom' => 14],
center: ['lat' => 52.1, 'lng' => 5.1],
zoom: 14,
drawType: 'circle', // NEW: runtime draw activation
);
Examples (backend PHP and frontend $dispatch):
// Backend (Livewire component): start a circle drawing session
$this->dispatch('lw-map:draw', type: 'circle');
// Switch to polygon
$this->dispatch('lw-map:draw', type: 'polygon');
// Exit draw mode
$this->dispatch('lw-map:draw', type: null);
<!-- Frontend (inside your Livewire/Alpine scope): start a circle -->
<button type="button" x-on:click="$dispatch('lw-map:draw', { type: 'circle' })">Circle</button>
<!-- Switch to polygon -->
<button type="button" x-on:click="$dispatch('lw-map:draw', { type: 'polygon' })">Polygon</button>
<!-- Exit draw mode -->
<button type="button" x-on:click="$dispatch('lw-map:draw', { type: null })">Exit</button>
Dispatched after the user completes a shape. Emitted on the Livewire client bus.
Example listener (Livewire v3 client bus):
Livewire.on('lw-map:draw-complete', ({ payload }) => {
console.log('Draw complete:', payload);
if (payload.type === 'circle' && payload.circle) {
const { center, radius } = payload.circle;
console.log('Circle center:', center, 'radius(m):', radius);
}
if (payload.type === 'polygon' && payload.polygon) {
console.log('Polygon path:', payload.polygon.path);
}
});
When a user finishes drawing a circle or polygon, the map emits a lw-map:selection-complete event. The intended follow-up action is inferred from whether the selection contains any of your markers.
type: circle or polygonmarkers: [] (empty)center (lat/lng), bounds (north/east/south/west), and radius (meters)polygonPath (stringified path) and boundscenter + radius (circle) or polygonPath (polygon) to run a geoquery in your database.Example payload (circle, no markers):
{
"id": "lw-map-123",
"type": "circle",
"markers": [],
"center": { "lat": 52.0907, "lng": 5.1214 },
"bounds": { "north": 52.2, "east": 5.3, "south": 52.0, "west": 5.0 },
"radius": 1500
}
Example payload (polygon, no markers):
{
"id": "lw-map-123",
"type": "polygon",
"markers": [],
"bounds": { "north": 52.2, "east": 5.3, "south": 52.0, "west": 5.0 },
"polygonPath": "(52.10,5.10),(52.15,5.10),(52.15,5.20),(52.10,5.20)"
}
type: circle or polygonmarkers: An array of your original marker objects that fall inside the shape
id with each marker you provide so you can easily identify selected items on the backend.ids from markers and pass them to your server or trigger UI actions.Example payload (markers selected):
{
"id": "lw-map-123",
"type": "polygon",
"markers": [
{ "id": 1, "lat": 52.0907, "lng": 5.1214, "title": "Utrecht" },
{ "id": 2, "lat": 52.3676, "lng": 4.9041, "title": "Amsterdam" }
]
}
id in your marker definitions if you plan to use marker selection.markers.length is zero:
0 → treat as an area/geoquery> 0 → treat as a marker selectionFrom a Livewire component, you can update markers or toggle clustering by dispatching the 'lw-map:update' event using named arguments (property: value):
// app/Livewire/Example.php
namespace App\Livewire;
use Livewire\Component;
class Example extends Component
{
public function addMarkers(): void
{
$markers = [
['id' => 1, 'lat' => 52.0907, 'lng' => 5.1214, 'title' => 'Utrecht'],
['id' => 2, 'lat' => 52.3676, 'lng' => 4.9041, 'title' => 'Amsterdam'],
];
// Update markers (no clustering)
$this->dispatch('lw-map:update', markers: $markers);
// Or, update markers and enable clustering with options
$this->dispatch('lw-map:update', markers: $markers, useClusters: true, clusterOptions: []);
}
public function render()
{
return view('livewire.example');
}
}
<!-- resources/views/livewire/example.blade.php -->
<div>
<button type="button" wire:click="addMarkers">Add markers</button>
<livewire:livewire-map :zoom="7" :center-lat="52.0907" :center-lng="5.1214" height="360px" />
</div>
You can recenter the map by including a center in your Livewire dispatch for 'lw-map:update'. Pass the center as an associative array: ['lat' => ..., 'lng' => ...].
Examples:
// Update center (and keep your markers): re-send your current markers to avoid clearing them
$this->dispatch('lw-map:update',
markers: $markers, // your current markers
center: ['lat' => 52.0907, 'lng' => 5.1214],
);
// Or using separate values
$this->dispatch('lw-map:update',
markers: $markers, // your current markers
centerLat: 52.0907,
centerLng: 5.1214,
);
// Combine with clustering options if desired
$this->dispatch('lw-map:update',
markers: $markers,
useClusters: true,
clusterOptions: ['maxZoom' => 14],
center: ['lat' => 52.0907, 'lng' => 5.1214],
);
Note: The update event replaces the marker list with what you send. If you only want to change the center, re-send your current markers as shown above.
You can also control draw mode from your Livewire component using the same dispatch API:
// Start drawing a circle
$this->dispatch('lw-map:draw', type: 'circle');
// Switch to polygon
$this->dispatch('lw-map:draw', type: 'polygon');
// Exit draw mode
$this->dispatch('lw-map:draw', type: null);
When useClusters is true (at render time or via an update event), markers will be grouped using @googlemaps/markerclusterer loaded from a CDN. You can pass clusterOptions both at render and in update events.
Every map instance gets a unique DOM id (exposed in events as id).
lw-map:draw from the browser, include the id to target a specific map; otherwise, all instances may react.lw-map:update from PHP, each Livewire component updates its own instance; no browser id is needed.lw-map:ready event right after initialization so you can capture the map instance if needed.You can update the map center and zoom level in a single update from your Livewire component:
// Update markers (if needed), set center and zoom together
$this->dispatch('lw-map:update',
markers: $markers, // your current markers
useClusters: true,
clusterOptions: ['maxZoom' => 14],
center: ['lat' => 52.0907, 'lng' => 5.1214],
zoom: 12,
);
Note: Always pass center as an associative array with 'lat' and 'lng' keys. The frontend will apply the zoom when provided.