Declarative MapLibre GL JS map components for the Hologram Elixir framework.

A map is a component tree. Sources, layers, markers and controls are components; what they look like is component state; what happens on them comes back as Hologram actions.

<HoloMap.Map cid="explorer" style={@style_url} center={{-70.66, 19.45}} zoom={11}>
  <HoloMap.Source.GeoJSON id="parcels" data={@parcels} />

  <HoloMap.Layer.Fill
    id="parcels-fill"
    source="parcels"
    paint={%{fill_color: @colour, fill_opacity: 0.4}}
    on_click={:parcel_clicked}
  />

  <HoloMap.Control.Navigation position="top-right" />
</HoloMap.Map>
def action(:parcel_clicked, params, component) do
  case params.features do
    [%{properties: %{parcel_id: id}} | _rest] -> put_state(component, :selected, id)
    [] -> component
  end
end

Changing @colour calls setPaintProperty on the live layer. Nothing is rebuilt, nothing flickers, and there is no JavaScript in your application.

What is in the box

GroupComponents
ContainerHoloMap.Map
SourcesGeoJSON, Vector, Raster, RasterDEM, Image
LayersFill, Line, Circle, Symbol, Raster, Heatmap, Hillshade, FillExtrusion, plus a generic Layer
OverlaysMarker, Popup
ImagesImage, for registering your own icons and patterns
ControlsNavigation, Scale, Geolocate, Fullscreen, Attribution, Globe, Terrain
3DTerrain, Sky
ImperativeHoloMap.API: fly_to/2, fit_bounds/3, set_feature_state/3, …

Requirements

  • Elixir 1.19+, OTP 28.1+ (Hologram's own floor)
  • Hologram 0.11+
  • MapLibre GL JS 6.x, installed in the host application

Installation

def deps do
  [{:holo_map, "~> 0.1"}]
end

HoloMap cannot ship its npm dependency, because Hologram resolves bare import specifiers against the host application's assets/package.json. Three steps, once per application, and the Installation guide walks through them:

  1. npm install --prefix assets maplibre-gl@^6.5.0
  2. Serve maplibre-gl-worker.mjs and maplibre-gl-shared.mjs from /assets/. MapLibre's own resolution of its worker breaks once the library is bundled, and the failure is silent
  3. Load maplibre-gl.css

How it works

Hologram gives components an init hook but no "props changed" and no "about to unmount" hook, and its JavaScript interop only runs inside action handlers. A map library needs all three.

So HoloMap does not drive MapLibre from Elixir call sites. Each child component renders one hidden element carrying its MapLibre specification as JSON, and a reconciler in the browser watches those elements with a MutationObserver:

<HoloMap.Layer.Fill paint={%{fill_color: @colour}} />
        
          Hologram renders and re-renders this
<div data-hm="layer" data-hm-key="parcels-fill" data-hm-spec='{"type":"fill",…}'></div>
        
          MutationObserver sees the attribute change
map.setPaintProperty("parcels-fill", "fill-color", "#ef4444")

Mount, update and unmount all fall out of that, without a framework hook for any of them, and it works with Hologram's virtual DOM, which already preserves JavaScript-managed children. The Architecture guide goes into the consequences.

One practical upshot: almost all of HoloMap is pure Elixir that builds JSON, so almost all of it is unit tested. Only HoloMap.Runtime touches JavaScript.

Guides

Every module also carries a full @moduledoc with prop tables and the reasoning behind the awkward parts. Run mix docs, or start from HoloMap.

Demo

demo/ is a Phoenix + Hologram application exercising every component against MapLibre's public demo tiles, so no API key is needed.

$ mix demo.setup
$ mix server        # http://localhost:4000

Those run from the repository root. The demo is its own Mix project in demo/, so cd demo && mix setup && mix server works too. The root aliases just save the cd.

Its browser test suite lives in demo/test/browser/ and drives a real MapLibre instance through Playwright. See the README there for what it covers and why.

Licence

MIT.