Ga naar hoofdinhoud

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​

Loading CnMapWidget playground…

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.typeLeaflet factory
tileL.tileLayer(url, options)
wmsL.tileLayer.wms(url, options)
wfsfetch(url) → L.geoJSON(features, options)
geojsoninline 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:

PropDefaultButton
fitControltrueFit all markers — re-centres and re-zooms to the full marker bounds, i.e. back to the frame the map opens at with autoFit.
locateControltrueShow my location — browser geolocation; warns (never throws) when denied or on an insecure origin.
fullscreenControltrueToggle 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 GeoJSON Feature[] (static maps).
  • markers.dataSource.url — fetched on mount; the response may be a GeoJSON FeatureCollection OR a flat array of rows (in which case latField, lngField, popupField drive 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​

NameTypeRequiredDefaultDescription
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).
zoomnumber7Initial zoom level.
layersArray<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.
markersunionnullMarker 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.
clusteringbooleanfalseEnable marker clustering. When true, lazy-loads leaflet.markercluster on first mount. markers.clustering overrides this prop when set.
heightunion'500px'Container height. Forwarded to the wrapper div's style.height.
autoFitbooleantrueAuto-fit map bounds to all loaded features after first load.
fitControlbooleantrueShow 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).
fullscreenControlbooleantrueShow the fullscreen toggle. Expands the widget to fill the viewport via a CSS overlay, so it needs no Fullscreen-API permission prompt.
locateControlbooleantrueShow 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.
basemapsArray<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.
ariaLabelstring() =&gt; t('nextcloud-vue', 'Map')Aria-label for the map application region.
unavailableLabelstring() =&gt; t('nextcloud-vue', 'Map library not available')Label shown when Leaflet is not available.
contentunionnullDashboard 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.
placementunionnullDashboard placement record. Declared so the grid's :placement binding is consumed as a prop instead of leaking onto the root element; unused here.

Events​

NamePayloadDescription
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​

NameBindingsDescription
fallback—fallback
legendlayers, markerslegend