Skip to content

Instantly share code, notes, and snippets.

@larshei
Created September 19, 2023 19:58
Show Gist options
  • Select an option

  • Save larshei/249ebc49b7469c41d8ea4260fe9ad93a to your computer and use it in GitHub Desktop.

Select an option

Save larshei/249ebc49b7469c41d8ea4260fe9ad93a to your computer and use it in GitHub Desktop.
Leaflet Map as Elixir Phoenix LiveView Component

Note

This was just a quick copy/paste of snippets from a project.

I dod not get to test this version yet.

// Add this to your app.js phoenix application
let Hooks = {}
Hooks.Map = {
mounted(){
const markers = {}
const map = L.map('mapid').setView([51.505, -0.09], 14)
const paths = {}
let geojsonLayer = L.geoJSON().addTo(map).setStyle({color: "#6435c9"})
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1,
accessToken: YOUR_MAPBOX_ACCESS_TOKEN
}).addTo(map)
this.handleEvent("update_marker_position", ({reference, lat, lon, center_view}) => {
markers[reference].setLatLng(L.latLng(lat, lon))
if (center_view) {
map.flyTo(L.latLng(lat, lon))
}
})
this.handleEvent("draw_path", ({reference, coordinates, color}) => {
data = {
"type": "LineString",
"coordinates": coordinates
}
geojsonLayer.addData(data)
})
this.handleEvent("view_init", ({reference, lat, lon, zoom_level = 20}) => {
geojsonLayer.remove()
geojsonLayer = L.geoJSON().addTo(map).setStyle({color: "#6435c9"})
map.setView(L.latLng(lat, lon), zoom_level)
})
this.handleEvent("set_zoom_level", ({zoom_level}) => {
map.setZoom(zoom_level)
})
this.handleEvent("add_marker", ({reference, lat, lon}) => {
// lets not add duplicates for the same marker!
if (markers[reference] == null) {
const marker = L.marker(L.latLng(lat, lon))
marker.addTo(map)
markers[reference] = marker
}
})
this.handleEvent("add_marker_with_popup", ({reference, lat, lon, link}) => {
// lets not add duplicates for the same marker!
if (markers[reference] == null) {
const marker = L.marker(L.latLng(lat, lon))
marker.bindPopup(`<a href=\"${link}\">${reference}</a>`)
marker.addTo(map)
markers[reference] = marker
}
})
this.handleEvent("clear", () => {
geojsonLayer.remove()
geojsonLayer = L.geoJSON().addTo(map)
for (const [reference, value] of Object.entries(markers)) {
marker = markers[reference]
marker.remove()
markers.delete(reference)
}
})
this.handleEvent("remove_marker", ({reference}) => {
if (markers[reference] != null) {
marker = markers[reference]
marker.remove()
markers.delete(reference)
}
geojsonLayer.remove()
})
}
}
defmodule Components.LeafletMap do
use Phoenix.Component
attr :class, :string, default: nil
def map(assigns) do
~H"""
<div style="overflow: hidden" phx-update="ignore" id="mapcontainer">
<div class={@class} phx-hook="Map" id="mapid"></div>
</div>
"""
end
# might be off if the maps shape is not close to a square.
def calculate_initial_map_zoom_level(n, e, s, w) do
lat_to_radiant = fn lat ->
sin = :math.sin(lat * :math.pi() / 180)
radX2 = :math.log((1 + sin) / (1 - sin)) / 2
max(min(radX2, :math.pi()), -:math.pi()) / 2
end
lat_difference = abs(lat_to_radiant.(n) - lat_to_radiant.(s))
lon_difference = abs(e - w)
lat_fraction = lat_difference / :math.pi()
lon_fraction = lon_difference / 360
# Ensure we never get 0 in division. 1.0e-5 was chosen arbitrarily after trying different values.
lat_zoom = :math.log(1 / max(lat_fraction, 1.0e-5)) / :math.log(2)
lon_zoom = :math.log(1 / max(lon_fraction, 1.0e-5)) / :math.log(2)
# Slight zoom out for vertical dimension, because our map view is very wide and not square.
min(lat_zoom - 0.5, lon_zoom)
|> min(20) # Lets not zoom in to infinity
end
def liveview_setup_map(socket, opts \\ []) do
socket
|> assign(selected_address: address)
|> Phoenix.LiveView.push_event("view_init", %{
reference: opts[:reference],
lat: opts[:latitude],
lon: opts[:longitude],
zoom_level: opts[:zoom_level] || 15
})
|> Phoenix.LiveView.push_event("add_marker", %{
reference: opts[:reference],
lat: opts[:latitude],
lon: opts[:longitude]
})
|> Phoenix.LiveView.push_event("update_marker_position", %{
reference: opts[:reference],
lat: address[:latitude],
lon: address[:longitude],
center_view: true
})
end
end
defmodule MyAppWeb.MapLive do
use MyAppWeb, :live_view
@impl true
def mount(%{"id" => id}, _session, socket) do
opts = [latitude: 51.123456, longitude: 7.123456, reference: "main"]
LeafletMap.liveview_set_map_to_address(socket, opts)
end
@impl true
def render(assigns) do
~H"""
<div class="h-screen bg-gray-100">
<div class="flex h-screen justify-center items-center rounded-md shadow-lg">
<LeafletMap.map class="h-80" />
</div>
</div>
"""
end
end
@nomtrosk
Copy link

nomtrosk commented Sep 20, 2023

Great approach to get leaflet working in Elixir!

I spent some time getting this to work: The biggest time consumer was to discover that the flex class in line 15 of map_live would render the map gray. Anyways, here is what else i changed to make it run

  • Removed address variable in liveview_setup_map (looked like some left overs)
  • Called liveview_setup_map instead of liveview_set_map_to_address in map_live.ex
  • Added leaflet CSS and JS imports to <head> in root.html.heex from hosted source https://leafletjs.com/download.html
  • Added alias Components.LeafletMap, as: LeafletMap to map_live.ex
  • Added the hooks to livesocket in app.js by altering this line:
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})

@larshei
Copy link
Author

larshei commented Sep 20, 2023

@nomtrosk right, sorry for rushing it out like this, but glad you got it to work in the end!

@daviaws
Copy link

daviaws commented Dec 12, 2025

If someone else is struggling with this, I’ve outlined a straightforward approach that works without extra customization. However, since the hook can be highly dependent on individual project needs, I don’t think creating a Hex package is justified.
Simbolismo-Digital/prever@aae7c2d
Tip: remember running mix assets.setup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment