Imperative API
For most use cases, the declarative components (<map.source>, <source.layer>, <map.marker>, etc.) are the right choice. But sometimes you need direct access to the MapLibre Map instance — for custom WebGL layers, runtime style queries, or APIs the addon doesn't wrap.
Getting the map instance
Use the @mapLoaded callback to get the map instance. Note that @mapLoaded fires when MapLibre's style and tiles are ready — child components (sources, layers) may not be on the map until the next frame. If you need to interact with sources imperatively, defer with requestAnimationFrame:
ts
onMapLoaded = (map: Map) => {
this.map = map;
// Sources and layers are added during the render that follows.
// Defer imperative work to the next frame.
requestAnimationFrame(() => {
const source = map.getSource('my-source');
source.setData(newData);
});
};See the Animate a Line example for a full working demo.
Example
gts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import type { Map } from 'maplibre-gl';
import MapLibreGL from 'ember-maplibre-gl/components/maplibre-gl';
const mapOptions = {
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [-122.4194, 37.7749] as [number, number],
zoom: 11,
};
export default class ImperativeDemo extends Component {
@tracked bearing = 0;
@tracked pitch = 0;
@tracked zoom = 11;
map: Map | null = null;
onMapLoaded = (map: Map) => {
this.map = map;
// Direct API: query rendered features on click
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point);
const names = features
.map((f) => f.properties?.name)
.filter(Boolean)
.slice(0, 3);
if (names.length) {
console.log('Clicked features:', names.join(', '));
}
});
// Direct API: update tracked state on move
map.on('move', () => {
this.bearing = Math.round(map.getBearing());
this.pitch = Math.round(map.getPitch());
this.zoom = Math.round(map.getZoom() * 10) / 10;
});
};
spin = () => {
this.map?.easeTo({
bearing: this.bearing + 90,
duration: 1000,
});
};
resetNorth = () => {
this.map?.easeTo({
bearing: 0,
pitch: 0,
duration: 500,
});
};
<template>
<div style="position: relative;">
<MapLibreGL
@initOptions={{mapOptions}}
@mapLoaded={{this.onMapLoaded}}
style="height: 500px; width: 100%; border-radius: 8px;"
/>
<div style="position: absolute; top: 12px; left: 12px; z-index: 1; display: flex; gap: 6px;">
<button
type="button"
{{on "click" this.spin}}
style="padding: 6px 14px; background: #fff; border: 1px solid #ddd; border-radius: 6px; font: 13px/1 system-ui; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.15);"
>Spin 90°</button>
<button
type="button"
{{on "click" this.resetNorth}}
style="padding: 6px 14px; background: #fff; border: 1px solid #ddd; border-radius: 6px; font: 13px/1 system-ui; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.15);"
>Reset North</button>
</div>
<div style="position: absolute; bottom: 20px; left: 12px; z-index: 1; background: rgba(0,0,0,0.7); color: white; padding: 8px 12px; border-radius: 6px; font: 13px/1.4 monospace;">
Bearing: {{this.bearing}}° · Pitch: {{this.pitch}}° · Zoom: {{this.zoom}}
</div>
</div>
</template>
}