We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
- Elixir
- Phoenix
- LiveView
- Mapbox
- JS Hooks
8 min read
Integrating Mapbox in your Phoenix LiveView application
Mapbox is a powerful mapping platform that provides developers with libraries, SDKs and APIs to work with high-performance maps, geospatial data and visualization tools.
For the past 7 years I've been working with Mapbox in React and more recently with React Native applications to create really interesting and challenging features from interactive maps, drawing tools, geolocation tracking, route optimization and advanced geospatial visualizations. Now, I started a new project using Phoenix LiveView and want to show you how you can integrate Mapbox in your application.
You can find the full source code of this example here.
I want to remember you that you can click on the magic wand
icon to get a deeper explanation of each code snippet, with that being said, let's start!
1. Creating our foundation
Let's create the boilerplate code, this LiveView will hold the map we'll be using throughout the example.
Create the following LiveView:
defmodule PhoenixRecipesWeb.MapLive do
use PhoenixRecipesWeb, :live_view
@impl true
def render(assigns) do
~H"""
<div class="h-screen w-screen"></div>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
end
I updated the app.html.heex
file to remove the default padding so that our map spans the whole screen, is not required but recommended.
<main>
<.flash_group flash={@flash} />
{@inner_content}
</main>
Then add the MapLive
to the router:
scope "/", PhoenixRecipesWeb do
pipe_through :browser
live "/", MapLive
end
Let's start with the basic:
-
In order to use Mapbox we need to install the MapboxGL JS library, this is the official package from Mapbox for web. Just so you know, there are libraries for React, React Native, iOS, Android and other platforms to work with Mapbox.
-
We need to sign-up to get an Access Token here. At the time of writing Mapbox asks for a credit card even if they offer a free tier just so you know. You'll need to fill a survey right after sign-up, fill it however you like, then you'll be presented with a dashboard and on the sidebar there'll be a
Tokens
link, click it and you'll see your defaultaccess token
, I'll refer to this token as theMAPBOX_ACCESS_TOKEN
. Please store it securely and never share it with others.
2. Installing Mapbox
To get started with Mapbox for the web, we need to install it, cd
into your assets
directory and run the following command:
cd assets
yarn add mapbox-gl
Next, we need to provide our app with the MAPBOX_API_KEY
, for simplicity I'll export the environment variable in the same terminal session that will be used to run the server.
export MAPBOX_API_KEY=your-long-string-mapbox-key
That's it! Now we're ready to render a map!
3. Rendering our first map
Because MapboxGL JS is a JavaScript library that means we have to write some JavaScript code to work with Mapbox and all of its features. To keep things organized I'll do it using a JS Hook.
Add the following to your LiveView, this code will try to call a hook named MapHook
that we'll be writing next.
def render(assigns) do
~H"""
<div class="h-screen w-screen">
<div id="map" class="h-screen w-screen" phx-hook="MapHook" />
</div>
"""
end
Under the assets/js
directory, create a hooks
directory and also a file named map-hook.js
and index.js
.
This directory stricture is just a matter of preference, you can organize your hooks however you prefer.
mkdir assets/js/hooks
touch assets/js/hooks/map-hook.js
touch assets/js/hooks/index.js
In the map-hook.js
write the following:
const MapHook = {
mounted() {},
};
export default MapHook;
In the index.js
write the following code to export the hook, this will serve as an entry point to get all of the hooks next.
import MapHook from "./map-hook";
export default {
MapHook,
};
In app.js
import the hooks and declare them in the liveSocket.
import hooks from "./hooks"; // <- import the hooks
let csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
hooks, // <- tell our LiveView about them
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
});
Now onto the interesting stuff, you can access most of the Mapbox features through the mapboxgl
instance that comes with the library. Write the following code and refresh your LiveView.
import mapboxgl from "mapbox-gl";
const MapHook = {
mounted() {
mapboxgl.accessToken = this.el.dataset.accessToken;
const map = new mapboxgl.Map({
container: this.el.id,
zoom: 15,
center: [-117.91897, 33.81196], // [lng, lat]
});
},
};
export default MapHook;
About Latitude/Longitude format
Notice the comment [lng, lat]
, Mapbox expects all of the coordinates to be provided in the form of an array to be in the order of [latitude, longitude] otherwise it won't work properly or it will throw an error.
Then if you go to your browser you won't see anything yet, but if you open the console you'll see the following error:
An API access token is required to use Mapbox GL. See https://docs.mapbox.com/api/overview/#access-tokens-and-token-scopes
I mention this because a missing access token configuration can be the root of hours of debugging, I've been there. The JS library is explicit about this but the React Native one is so cryptic and you only see a dark screen without any errors.
Let's go back to our Phoenix code, we need to include Mapbox's style sheet that comes with the library. Open up the assets/css/app.css
file and add the following import:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "../node_modules/mapbox-gl/dist/mapbox-gl.css"; // <- add this import
We need a way for our application to get the value of the access token, to do it in the more organized way, we'll add it to the config/runtime.exs
file. This is where Phoenix setup important configurations that will be processed at runtime and not at compile time.
"In a Phoenix application, configuration values can be set at compile time or runtime, depending on where they are defined. The main difference between config/dev.exs and config/runtime.exs lies in when the configuration is evaluated and whether changes require recompilation."
Inside the runtime.exs
file just above the line if config_env() == :prod do
, add the following:
config :my_app, :mapbox, access_token: System.get_env("MAPBOX_ACCESS_TOKEN")
Don't forget to replace :my_app
with the name of your app as an atom, in my case is :phoenix_recipes
.
Now we are ready for a second round with the map.
Update your LiveView code to provide the access token to the MapHook:
defmodule PhoenixRecipesWeb.MapLive do
use PhoenixRecipesWeb, :live_view
@impl true
def render(assigns) do
~H"""
<div class="h-screen w-screen">
<div id="map" class="h-screen w-screen" phx-hook="MapHook" data-access-token={@access_token} />
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
access_token = Application.get_env(:phoenix_recipes, :mapbox) |> Keyword.get(:access_token)
{:ok, assign(socket, access_token: access_token)}
end
end
You can go to your LiveView and you'll see a map!
At this point you maybe you're thinking, ok that's cool but what else?
You can do pretty much anything with the Mapbox JavaScript library, I recommend you to check out the guides. Also there's the examples page where you can see what Mapbox is capable of and to find inspiration, they have really cool examples.
I've seen Mapbox mainly being used just to show some data as markers but there's a lot you can do, just to name a few:
- Display information in Popups after clicking a marker
- Create dragable markers
- Use a Geocoder to move the map to a specific address
- Render heatmaps
- Working with GeoJSON data
- Add 3D buildings
- Work with vector tile sources
I suggest that you take a look at the documentation and learn by practicing! Also, if you have access to georeferenced data it can be a lot of fun to explore it yourself using geospatial visualizations like a heatmap or hundreds of clustered markers instead of raw data from a database.
4. Add markers dynamically
As always, I like to keep things interesting by expanding on the topic. So let's implement a way to add some markers dynamically to the map.
First, copy the following data, they are just a list of attraction locations at Disneyland in Anaheim :)
defmodule PhoenixRecipesWeb.MapLive do
use PhoenixRecipesWeb, :live_view
@attractions [
[-117.92053664465006, 33.81115995986345],
[-117.91757819187785, 33.81100097317143],
[-117.92247054082354, 33.81215983080281],
[-117.92030331607128, 33.81240942885339],
[-117.91903731349323, 33.813452384256856],
[-117.92110797870988, 33.814762746118774],
[-117.91875836377658, 33.81534215144098],
[-117.91839358337273, 33.81591263897725]
]
...
Then, add a button that will trigger an event to let our LiveView know that we want to add a new marker to the map, this event will be called show-random-attraction
:
def render(assigns) do
~H"""
<div class="h-screen w-screen">
<div class="absolute z-10 top-10 left-10">
<button
type="button"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
phx-click="show-random-attraction"
>
Show me a random attraction!
</button>
</div>
<div id="map" class="h-screen w-screen" phx-hook="MapHook" data-access-token={@access_token} />
</div>
"""
end
There are a few different ways to instruct our map to add markers dynamically, for simplicity I'll be pushing an event from our LiveView to the MapHook
with the location of an attraction we want to add. By doing it this way the hook will be responsible to append the marker to the map.
Add the following handle_event
bellow the mount
function inside MapLive
:
@impl true
def handle_event("show-random-attraction", _params, socket) do
location = Enum.take_random(@attractions, 1) |> hd()
{:noreply, socket |> push_event("add-marker", %{location: location})}
end
Next, let's move to the MapHook
and add a handleEvent
handler to take in the random attraction location and rendering over the map as a marker.
this.handleEvent("add-marker", ({ location }) => {
new mapboxgl.Marker().setLngLat(location).addTo(map);
});
The full code should look like this:
import mapboxgl from "mapbox-gl";
const MapHook = {
mounted() {
mapboxgl.accessToken = this.el.dataset.accessToken;
const map = new mapboxgl.Map({
container: this.el.id,
zoom: 15,
center: [-117.91897, 33.81196],
});
this.handleEvent("add-marker", ({ location }) => {
new mapboxgl.Marker().setLngLat(location).addTo(map);
});
},
};
export default MapHook;
That's it, just reload your browser and try clicking the button a few times and you'll see some markers appear out of thin air!
Conclusion
Integrating Mapbox with Phoenix LiveView opens up a world of possibilities for building dynamic, geospatially-rich applications. By combining Mapbox's powerful mapping features with LiveView's real-time interactivity, you can create engaging experiences that go beyond static maps.
Now it’s your turn to explore Mapbox’s capabilities and experiment with more advanced features. Dive into the documentation, try out different examples, and let your creativity guide you. If you have any questions or want to share your own experiments, feel free to reach out—I’d love to hear what you build!