CnMapWidget
Leaflet-backed map primitive for declarative manifest-driven maps. Renders a configurable layer stack (tile / WMS / WFS / inline GeoJSON) plus an optional marker set sourced from inline features OR an HTTP dataSource.url. Marker clustering is opt-in (lazy-loads leaflet.markercluster only when first toggled). Spec: REQ-MMW-* (manifest-map-widget).
Try it
Usage
<!-- Standalone — embed a map inside a custom component -->
<CnMapWidget
:center="[52.1326, 5.2913]"
:zoom="7"
:layers="[
{ type: 'tile',
url: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:3857/{z}/{x}/{y}.png',
options: { attribution: '© Kadaster', maxZoom: 19 } },
{ type: 'wms',
url: 'https://service.pdok.nl/lv/bag/wms/v2_0',
options: { layers: 'pand', transparent: true, opacity: 0.6 } },
]"
:markers="{
dataSource: { url: '/index.php/apps/procest/api/cases/geo' },
latField: 'lat',
lngField: 'lng',
popupField: 'title',
clustering: true,
}"
height="500px"
@marker-click="onMarkerClick" />
The layers[] array dispatches by type:
layer.type | Leaflet factory |
|---|---|
tile | L.tileLayer(url, options) |
wms | L.tileLayer.wms(url, options) |
wfs | fetch(url) → L.geoJSON(features, options) |
geojson | inline data → L.geoJSON; OR fetched URL |
Unknown types log a warning and are skipped — manifests stay forward-compatible.
Base maps and the layer switcher
basemaps declares the switchable background map(s). The first entry is live on load; when more than one is given, a Leaflet layer switcher appears in the top-right corner:
<CnMapWidget
:center="[52.1326, 5.2913]"
:basemaps="[
{ name: 'Standard',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap contributors' },
{ name: 'Terrain',
url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap contributors, SRTM | © OpenTopoMap' },
]" />
Use basemaps instead of a tile entry in layers for the background — layers is then free to carry only overlays (WMS / WFS / GeoJSON). basemaps is empty by default, so consumers that already declare their background through layers are unaffected.
Base map tiles are
<img>loads from a third-party host. A hardened Content-Security-Policy (img-src) may block hosts other than the one already allowed — verify before shipping a new basemap.
Controls
Beyond Leaflet's own zoom and attribution controls, the widget mounts a control bar (top-left). Each button is individually switchable:
| Prop | Default | Button |
|---|---|---|
fitControl | true | Fit all markers — re-centres and re-zooms to the full marker bounds, i.e. back to the frame the map opens at with autoFit. |
locateControl | true | Show my location — browser geolocation; warns (never throws) when denied or on an insecure origin. |
fullscreenControl | true | Toggle fullscreen — a CSS overlay, so no Fullscreen-API permission prompt. |
fitToMarkers() is also callable directly through a template ref:
<CnMapWidget ref="map" ... />
<!-- this.$refs.map.fitToMarkers() -->
Sizing
height accepts any CSS length (default 500px). Pass height="100%" to fill a flex parent — the widget watches its own box with a ResizeObserver and re-flows Leaflet whenever it changes, so a map that grows, or that mounts while hidden behind a view toggle, still lays its tiles and markers out correctly.
Markers
Pick one of:
markers.features— inline GeoJSONFeature[](static maps).markers.dataSource.url— fetched on mount; the response may be a GeoJSON FeatureCollection OR a flat array of rows (in which caselatField,lngField,popupFielddrive the conversion).markers.dataSource.{register, schema}— RESERVED. Round-tripped by validators today, resolver lands in a follow-up change.
markers.clustering: true (or the widget-level clustering prop) opts in to leaflet.markercluster. The cluster bundle lazy-loads only when first enabled — consumers without clustering pay zero extra bytes.
Legend
Render a custom legend overlay via the #legend slot. It is positioned absolute (top-right) by default; consumers may override with their own container. Scoped props: { layers, markers }.
<CnMapWidget :layers="layers" :markers="markers">
<template #legend="{ layers }">
<ul class="my-legend">
<li v-for="layer in layers" :key="layer.type">{{ layer.type }}</li>
</ul>
</template>
</CnMapWidget>
Manifest mounting
Don't mount this component directly from a manifest. Use CnMapPage (the type: "map" page renderer) which wraps this primitive with a header, filter slot, and event pass-through.
Fallback
When Leaflet cannot be loaded (test envs, CSP-blocked CDNs), the #fallback slot renders instead. Default text uses unavailableLabel.
<CnMapWidget :center="[52, 5]">
<template #fallback>
<p>Map view unavailable.</p>
</template>
</CnMapWidget>
Reference
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
center | [number, number] | [52.13, 5.29] | Initial map center as [latitude, longitude]. Optional: on a dashboard the centre arrives via content.center (see the cfg computed), and when neither source supplies a valid pair it defaults to [52.13, 5.29] (the Netherlands). | |
zoom | number | 7 | Initial zoom level. | |
layers | Array<object> | [] | Layer definitions. Each entry: { type: 'tile'|'wms'|'wfs'|'geojson', url, options }. geojson MAY supply inline data (FeatureCollection) instead of url. Unknown types log a console.warn and are skipped. | |
markers | union | null | Marker config. { features?, dataSource?, latField?, lngField?, popupField?, clustering?, iconColor?, iconUrl?, centerMarker? }. features[] is inline; dataSource.url is HTTP-fetched on mount; dataSource.{register, schema} plots the objects of an OpenRegister register/schema via their @self.geo. centerMarker: true adds an extra pin at the map's center, alongside any source markers. | |
clustering | boolean | false | Enable marker clustering. When true, lazy-loads leaflet.markercluster on first mount. markers.clustering overrides this prop when set. | |
height | union | '500px' | Container height. Forwarded to the wrapper div's style.height. | |
autoFit | boolean | true | Auto-fit map bounds to all loaded features after first load. | |
fitControl | boolean | true | Show the "fit all markers" control — re-centres and re-zooms the map so every marker is back in view (the position the map opens at). | |
fullscreenControl | boolean | true | Show the fullscreen toggle. Expands the widget to fill the viewport via a CSS overlay, so it needs no Fullscreen-API permission prompt. | |
locateControl | boolean | true | Show the "locate me" control, which centres the map on the visitor's own position via the browser geolocation API. Warns (does not throw) if denied. | |
basemaps | Array<object> | [] | Switchable base maps: [{ name, url, attribution, options }]. The first entry is active on load and a layer switcher appears when more than one is given. Supply these INSTEAD of a tile entry in layers for the background map. Empty by default, so consumers that declare their background through layers are unaffected. | |
ariaLabel | string | () => t('nextcloud-vue', 'Map') | Aria-label for the map application region. | |
unavailableLabel | string | () => t('nextcloud-vue', 'Map library not available') | Label shown when Leaflet is not available. | |
content | union | null | Dashboard content blob. When this widget is placed on a dashboard, the grid stores its config here and each key (center, zoom, markers, clustering, height, autoFit, layers, basemaps) overrides the matching flat prop. Direct (non-dashboard) consumers omit this and pass the flat props instead. See the cfg computed for the merge rules. | |
placement | union | null | Dashboard placement record. Declared so the grid's :placement binding is consumed as a prop instead of leaking onto the root element; unused here. |
Events
| Name | Payload | Description |
|---|---|---|
map-ready | — | Map ready event. Fired once after Leaflet has loaded and the map is mounted. |
marker-click | — | Marker click event. Fired when a marker is clicked. |
bounds-change | — | Viewport bounds change event. Fired (debounced) after pan / zoom settles. |
click | — | Map background click event. Fired when the user clicks the map outside any marker. |
Slots
| Name | Bindings | Description |
|---|---|---|
fallback | — | fallback |
legend | layers, markers | legend |