State and lifecycle of maps in Angular with OpenLayers
The useful part
Create the map in the browser, give resources an owner and translate events once. Printing needs explicit pixels, scale and failure handling.
A map can work on its first load and still fail when someone switches tabs, changes a layer or returns to the screen. The integration needs an owner for the map, a boundary for application state and a cleanup path. The introductory article covers the product workflow; here we look at the implementation decisions.

One owner, created in the browser
Give each map instance one owning component. Create the OpenLayers Map inside afterNextRender, where the DOM target exists and Angular is running in the browser. A provider factory that calls new Map() while the component is constructed can still run during SSR: deferring only setTarget is not enough.
This minimal component uses Angular render callbacks and signal queries. Install ol and include ol/ol.css in the application’s global styles. The fixed height is intentional: a map needs a non-zero container. The default controls preserve the OpenStreetMap attribution.
import {
afterNextRender, ChangeDetectionStrategy, Component,
DestroyRef, ElementRef, inject, viewChild,
} from '@angular/core';
import OlMap from 'ol/Map.js';
import View from 'ol/View.js';
import TileLayer from 'ol/layer/Tile.js';
import OSM from 'ol/source/OSM.js';
import { fromLonLat } from 'ol/proj.js';
@Component({
selector: 'app-map',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<div #target class="map" aria-label="Map" tabindex="0"></div>',
styles: '.map { height: 24rem; }',
})
export class MapComponent {
private readonly target =
viewChild.required<ElementRef<HTMLDivElement>>('target');
private map?: OlMap;
constructor() {
afterNextRender(() => {
this.map = new OlMap({
target: this.target().nativeElement,
layers: [new TileLayer({ source: new OSM() })],
view: new View({ center: fromLonLat([19.94, 50.06]), zoom: 12 }),
});
});
inject(DestroyRef).onDestroy(() => this.map?.dispose());
}
}The example demonstrates ownership and browser initialization; it does not reproduce the full GeoAtlas library. For composable child layers, expose a map host through the component’s injector, including an explicit readiness signal. Children attach when the instance becomes available and detach when their owner is destroyed. A local injector defines a lifetime; it cannot stop a consumer from retaining a reference.
Layers need explicit order and cleanup
A layer component can translate inputs into changes on an OpenLayers layer. Adding a layer appends it to the map’s collection. That is not a guarantee of template order after a conditional layer is removed and re-created, or after a repeated list is reordered. Use a declared zIndex or reconcile the collection order explicitly.
A small regression scenario
Start with a basemap, a conditional planning layer and a selection highlight. Hide the planning layer, then show it again. The selection must still render above it. Explicit ordering — for example 0, 10 and 20 — keeps the intended stack independent of creation time.
Removing a layer from the map does not dispose every listener or shared source associated with it. The code that creates a subscription, source or request owns its cleanup. Dispose an owned source only when no other layer needs it.
Translate events at one boundary
OpenLayers reports changes through events. In a zoneless Angular view, update a signal consumed by the template or use another supported notification such as markForCheck(). A plain field assignment in an external callback does not, by itself, schedule rendering.
For a map you already own, this helper observes the centre. It accepts the owner’s DestroyRef explicitly, so it can be called from a render callback without trying to inject there.
import { DestroyRef, signal } from '@angular/core';
import type OlMap from 'ol/Map.js';
export function observeCenter(map: OlMap, owner: DestroyRef) {
const read = () => [...(map.getView().getCenter() ?? [])];
const center = signal(read());
const update = () => center.set(read());
map.on('moveend', update);
owner.onDestroy(() => map.un('moveend', update));
return center.asReadonly();
} Keep requested state separate from observed state: “centre on this record” is an instruction; “the user panned here” is an observation. Compare coordinates before writing back to the map and choose a tolerance that fits the projection and interaction. Do not turn every moveend into a new animated navigation request.
Selection and projections belong to the data boundary
Use one store of stable feature IDs for the map, attribute table and detail panel. OpenLayers feature objects can be replaced on reload; an ID from the underlying dataset is the link between views. Decide what happens when a selected record disappears or the user loses access to it.
Make coordinate systems explicit in your data contract. A Polish service may return EPSG:2180, a basemap may use EPSG:3857, and GeoJSON commonly carries longitude and latitude in EPSG:4326. Transform once at the boundary. Register additional projections before using them; do not apply a guessed transform in each component.
Destroying and testing the view
- Map lifetime: call
dispose()when the owner is finished. Detaching withsetTarget(undefined)is useful when retaining a map for reuse, but it is a different lifecycle decision. - Subscriptions: remove listeners registered on the map, view, sources and application services. A framework callback cleans up only what you put in it.
- Requests: abort owned fetches and ignore late responses after destruction. Removing the DOM target is not a general request-cancellation mechanism.
- Shared resources: retain or dispose caches and sources according to their owner, not according to whichever component happens to disappear first.
Test navigation away and back, a hidden container becoming visible, and a panel resizing the map. Use updateSize() when a layout transition requires a fresh size calculation. Repeated navigation is a useful regression flow; heap or resource measurements are needed to substantiate a memory-leak claim.
Pure selection and projection rules can run without a browser. A jsdom test has no browser layout or native WebGL rendering. Use a real browser for sizing, canvas output, keyboard interaction and print flows; a map constructed without a target does not prove that rendering works.
Printing: pixels, scale and failure states
Choose the physical paper size and the output DPI, then calculate the raster dimensions. For A4 portrait at 150 DPI:
const dpi = 150;
const widthPx = Math.round(210 / 25.4 * dpi); // 1240
const heightPx = Math.round(297 / 25.4 * dpi); // 1754 OpenLayers pixelRatio is a ratio of physical pixels to CSS pixels, not a DPI setting. One approach is to size a dedicated export map to the calculated pixel dimensions and use pixelRatio: 1. If you choose a different ratio, account for it once; multiplying both the dimensions and the ratio by the DPI factor produces an oversized image. Derive view resolution separately for a requested map scale.
Build export layers from shared configuration, with their own layer instances; do not attach a live display layer to two maps. Wait for fonts and rendercomplete, which describes completion for the current viewport, and add a timeout plus tile/source error handling. A finished render is not proof that every remote service returned the intended data.
Canvas export also needs CORS-compatible image sources. Keep the required attribution in the output and preserve the provider’s usage terms. GeoAtlas’s print editor keeps page settings and preview together so the user can inspect the result before export.
What this buys the product
Clear ownership makes a map behave like the other views in an application: it follows shared selection, responds to layout changes and releases its work when the user leaves. The integration can grow without distributing event listeners and coordinate transforms across every screen.
Sources and scope
Examples illustrate the integration pattern. Check APIs against your installed versions; references were checked on 2 October 2026.