Packages
Declarative MapLibre GL JS map components for the Hologram Elixir framework.
Current section
Files
Jump to
Current section
Files
README.md
# HoloMap
Declarative [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) map components for the
[Hologram](https://hologram.page) 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.
```elixir
<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>
```
```elixir
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
| Group | Components |
| ----- | ---------- |
| Container | `HoloMap.Map` |
| Sources | `GeoJSON`, `Vector`, `Raster`, `RasterDEM`, `Image` |
| Layers | `Fill`, `Line`, `Circle`, `Symbol`, `Raster`, `Heatmap`, `Hillshade`, `FillExtrusion`, plus a generic `Layer` |
| Overlays | `Marker`, `Popup` |
| Images | `Image`, for registering your own icons and patterns |
| Controls | `Navigation`, `Scale`, `Geolocate`, `Fullscreen`, `Attribution`, `Globe`, `Terrain` |
| 3D | `Terrain`, `Sky` |
| Imperative | `HoloMap.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
```elixir
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](guides/installation.md) 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](guides/architecture.md) 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
- [Installation](guides/installation.md): the three steps `HoloMap` cannot do for you
- [Getting started](guides/getting-started.md): from an empty map to data, state and events
- [Architecture](guides/architecture.md): how a prop change becomes a MapLibre call, and what it costs
- [Events](guides/events.md): handler syntax, targets, and every payload
- [Imperative API](guides/imperative-api.md): the escape hatch, and when to reach for it
- [Limitations](guides/limitations.md): what does not work, and why
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.
```console
$ 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.