Go back
  • Elixir
  • Phoenix
  • LiveView
  • Mapbox
  • JS Hooks
Publish date:

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:

  1. 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.

  2. 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 default access token, I'll refer to this token as the MAPBOX_ACCESS_TOKEN. Please store it securely and never share it with others.

Mapbox console to get access key

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!

Video of rendered Mapbox 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:

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!

Map showing markers

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!

Share this post