# useDocyrusMapView URL: /docs/web/hooks/use-docyrus-map-view Build Docyrus-backed map pages from a locationSelect field with saved views, marker models, and ready-to-render Leaflet or Google Maps canvases. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-map-view ``` **Dependencies:** - [@docyrus/app-utils](https://www.npmjs.com/package/@docyrus/app-utils) - [@docyrus/api-client](https://www.npmjs.com/package/@docyrus/api-client) - [@tanstack/react-query](https://tanstack.com/query/latest) - [react-leaflet](https://react-leaflet.js.org) - [leaflet](https://leafletjs.com) - [@vis.gl/react-google-maps](https://visgl.github.io/react-google-maps/) This hook is distributed as source. It expects an authenticated Docyrus client, a `QueryClientProvider`, and a data source that includes at least one `field-locationSelect` field. ## Overview `useDocyrusMapView` is the map-first companion to [`useDocyrusDataGrid`](/docs/web/hooks/use-docyrus-data-grid): - loads the active Docyrus data source metadata - auto-detects the first `field-locationSelect` field, or uses the slug you provide - keeps saved-view filtering, sorting, and search wiring from `useDocyrusDataGrid` - appends required map columns (`id`, location, title, popup fields) even when a saved view hides them - transforms records into provider-agnostic marker objects - returns ready-to-render helpers for both **Leaflet** and **Google Maps** The hook is designed for pages where records stay in Docyrus, but the primary surface is a map instead of a table. ## Supported map providers ### 1. Leaflet Use [`DocyrusLeafletMapView`](#docyrusleafletmapview) when you want the built-in Docyrus UI map stack: - no API key requirement - theme-aware tile layers - clustering - custom Docyrus map controls (zoom, fullscreen, locate) ### 2. Google Maps Use [`DocyrusGoogleMapView`](#docyrusgooglemapview) when you want Google basemaps. Provide the key either with the hook/component `googleMapsApiKey` prop or through `VITE_GOOGLE_MAPS_API_KEY`. ## Usage ### Leaflet page ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { DocyrusLeafletMapView, useDocyrusMapView } from '@docyrus/ui/library/hooks/use-docyrus-map-view'; import { useBaseOrganizationCollection } from '@/db/collections/base-organization.collection'; export function OrganizationLocationsPage() { const { client } = useDocyrusAuth(); if (!client) return null; const collection = useBaseOrganizationCollection(); const mapView = useDocyrusMapView({ client, appSlug: 'base', dataSourceSlug: 'organization', collection, locationFieldSlug: 'map_location', titleFieldSlug: 'name', descriptionFieldSlugs: ['address', 'city'], searchPlaceholder: 'Search organizations', enableGroupMenu: false, enableDisplayMenu: false, enableRowHeightMenu: false }); return (
{mapView.toolbar}
); } ``` ### Google Maps page ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { DocyrusGoogleMapView, useDocyrusMapView } from '@docyrus/ui/library/hooks/use-docyrus-map-view'; export function WarehouseMapPage() { const { client } = useDocyrusAuth(); if (!client) return null; const mapView = useDocyrusMapView({ client, appSlug: 'base', dataSourceSlug: 'warehouse', locationFieldSlug: 'location', titleFieldSlug: 'name', descriptionFieldSlugs: ['city', 'status'], googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY }); return ( ); } ``` ## How the query is built Internally the hook reuses the saved-view state from `useDocyrusDataGrid`, then creates its own items query with a stronger `columns` list. That means a user can save a view that hides `map_location`, but the hook will still request: - `id` - the location field - the resolved title field - any popup/description fields you configured So markers keep rendering correctly while the rest of the data-view behavior still follows the active Docyrus view. ## Field resolution ### Location field Resolution order: 1. `locationFieldSlug` 2. first field with `type === 'field-locationSelect'` ### Title field Resolution order: 1. `titleFieldSlug` 2. first matching slug from: `name`, `title`, `label`, `subject`, `display_name`, `displayName`, `commercial_title`, `short_name` 3. first text-like field that is not the location field ### Description fields Resolution order: 1. `descriptionFieldSlugs` 2. first matching slugs from: `description`, `details`, `address`, `city`, `district`, `country`, `state`, `province`, `status`, `type` ### Marker visual (image + icon) Each marker can render as either a circular avatar (from an image field) or a Docyrus icon, with a configurable priority. **Image field** (`markerImageFieldSlug`): - Reads `field-image` / `field-file` payloads (array of `{ signed_url, url, src }`) - Renders as a 36×36 circular avatar with a white ring **Icon field** (`markerIconFieldSlug`): - Reads a Docyrus icon identifier string such as `"huge building-06"` or `"fal star"` - Auto-detected from `field-icon` type or the `icon` slug when omitted - Renders the same SVG asset used by `DocyrusIcon` inside a 32×32 white badge **Priority** (`markerPriority`): - `'image'` (default): image when present → icon when present → default `MapPin` - `'icon'`: icon when present → default `MapPin` (image is ignored) **Icon color** (`markerIconColorFieldSlug` + `markerIconColor`): - Per-record color via the field, with a static fallback through `markerIconColor` - Accepts hex, `rgb()`, CSS variables, or Tailwind tokens like `"emerald-500"` (resolved via `resolveColorHex`) - Leaflet recolors the icon itself via CSS `mask-image` - Google Maps renders the icon URL natively and does not support recoloring; the color value is ignored on that provider The hook exposes the resolved values on each marker as `marker.imageUrl`, `marker.iconUrl`, and `marker.iconColor`. Custom `renderMarkerIcon` / `getMarkerIcon` callbacks should return `null` (Leaflet) or fall through (Google) when either is set to let the built-in renderer take over. ### Viewport filter When `enableViewportFilter` is `true`, `markers` / `mappedItems` are narrowed to records whose `position` falls inside the active map bounds. `allMarkers` keeps the unfiltered set. Two modes: - **`auto`** (default) — every pan/zoom updates the filter. Side lists and counts feel "live". - **`manual`** — the filter only applies when `searchThisArea()` is called. `isViewportStale` becomes `true` when the user moves the map without confirming, perfect for an Airbnb-style button. ```tsx // Manual mode "Search this area" button {mapView.isViewportStale && ( )} ``` Wire the map → hook connection by passing `mapView.setViewportBounds` to the `onViewportChange` prop of `DocyrusLeafletMapView` / `DocyrusGoogleMapView`. ## API Reference ### `useDocyrusMapView(options)` The hook accepts all `useDocyrusDataGrid` options except `enableItemsQuery`, `showSelectColumn`, and `enableRowMarkers`, plus the options below. | Option | Type | Default | Description | |--------|------|---------|-------------| | `locationFieldSlug` | `string` | auto | Explicit location field slug. Use when the data source has multiple `field-locationSelect` fields. | | `titleFieldSlug` | `string` | auto | Field slug used for marker titles and side-list labels. | | `descriptionFieldSlugs` | `string[]` | auto | Extra fields appended into popup text. | | `markerImageFieldSlug` | `string` | — | Field whose value resolves to an image URL (e.g. `field-image` / `field-file` payload). Used as the marker's circular avatar. | | `markerIconFieldSlug` | `string` | auto | Field whose value is a Docyrus icon identifier (e.g. `huge building-06`). Auto-detects `field-icon` type or the `icon` slug. | | `markerIconColorFieldSlug` | `string` | — | Field whose value is a CSS color (hex, `rgb()`, Tailwind token like `emerald-500`). Tints the icon in Leaflet via CSS `mask-image`. | | `markerIconColor` | `string` | — | Static fallback color used when the per-record color field is empty. | | `markerPriority` | `'image' \| 'icon'` | `'image'` | Which source wins when both fields are populated. `'image'` falls back to icon when image is empty; `'icon'` always renders the icon when present. | | `enableViewportFilter` | `boolean` | `false` | Narrow `markers` / `mappedItems` to records inside the map's current visible bounds. `allMarkers` keeps the unfiltered set. | | `viewportFilterMode` | `'auto' \| 'manual'` | `'auto'` | `'auto'` re-filters on every pan/zoom. `'manual'` only filters when `searchThisArea()` is called — pair with an Airbnb-style "Search this area" button via `isViewportStale`. | | `enableMarkerDrag` | `boolean` | `false` | Make every marker draggable. On drop, the hook calls `onMarkerMove` (if provided) or PATCHes the resolved location field of the record. | | `onMarkerMove` | `(marker, newPosition) => void \| Promise` | — | Custom drop handler. Default behavior: reverse-geocodes the new coordinates, PATCHes the location field with `{ latitude, longitude, address }`, then `reload()`s. | | `basemaps` | `Array \| false` | — | Basemaps to surface in the in-map picker. Built-in ids: `'street'`, `'satellite'`, `'hybrid'`, `'terrain'`. Pass custom `DocyrusBasemapDefinition` for non-default tile servers. | | `defaultBasemap` | `string` | first entry | Initially selected basemap id. | | `markerTemplate` | `string` | — | Handlebars-style template for popup body text (e.g. `"{{name}} — {{city}}"`). | | `getMarker` | `(item, context) => DocyrusMapMarker \| null` | — | Override marker generation completely. Return `null` to skip a record. | | `defaultCenter` | `{ lat: number; lng: number }` | Ankara | Fallback center used when there are no mapped records. | | `defaultZoom` | `number` | `6` | Base zoom level when the map starts without fitted bounds. | | `focusedZoom` | `number` | `14` | Zoom level used when the result set has a single marker. | | `fitBoundsPadding` | `number` | `48` | Padding used when fitting multiple markers into view. | | `autoSelectSingleMarker` | `boolean` | `true` | Opens the only marker automatically when the query returns exactly one mapped record. | | `googleMapsApiKey` | `string \| null` | `VITE_GOOGLE_MAPS_API_KEY` | API key forwarded to `DocyrusGoogleMapView`. | | `onMapClick` | `(position: DocyrusMapPoint) => void` | — | Called when the user clicks an empty area of the map (not a marker). Used to power click-to-locate flows for unmapped records. | ### Return value | Property | Type | Description | |----------|------|-------------| | `toolbar` | `ReactNode` | Prebuilt Docyrus data-view toolbar from `useDocyrusDataGrid`. | | `table` | `Table` | Underlying TanStack table used by the toolbar and saved-view editor. | | `items` | `TData[]` | All fetched records for the active view. | | `markers` | `DocyrusMapMarker[]` | Mapped records transformed into provider-agnostic marker models. Filtered by the active viewport when `enableViewportFilter` is on. | | `allMarkers` | `DocyrusMapMarker[]` | All resolved markers before viewport filtering. Use this for the underlying dataset (e.g. counts or "show all" links). | | `mappedItems` | `TData[]` | Records that produced markers successfully. Also viewport-filtered. | | `unmappedItems` | `TData[]` | Records without usable coordinates. | | `viewportBounds` | `DocyrusMapBounds \| null` | Bounds reported by the active map provider on its last `moveend` / `idle` event. | | `setViewportBounds` | `(bounds \| null) => void` | Wired internally — pass to `DocyrusLeafletMapView` / `DocyrusGoogleMapView` via `onViewportChange`. | | `isViewportStale` | `boolean` | `true` when the map has moved since the last applied viewport filter (manual mode). | | `searchThisArea` | `() => void` | Apply the current viewport as the active filter (manual mode). | | `clearViewportFilter` | `() => void` | Remove the viewport filter and restore the full marker set. | | `enableMarkerDrag` | `boolean` | Mirrors the input option — pass to a map view's `draggableMarkers` prop. | | `handleMarkerMove` | `(marker, newPosition) => Promise` | Default drop handler wired to the Docyrus client — pass to a map view's `onMarkerMove` prop. | | `availableBasemaps` | `DocyrusBasemapDefinition[]` | Resolved basemap list after expanding ids. Pass to a map view's `basemaps` prop. | | `activeBasemap` | `DocyrusBasemapDefinition \| null` | Currently selected basemap. Pass to a map view's `activeBasemap` prop. | | `setActiveBasemap` | `(id: string) => void` | Switch the active basemap. Pass to a map view's `onBasemapChange` prop. | | `locationField` | `DataSourceField \| null` | Resolved `field-locationSelect` definition. | | `titleField` | `DataSourceField \| null` | Resolved title field definition. | | `descriptionFields` | `DataSourceField[]` | Resolved popup/description fields. | | `hasLocationField` | `boolean` | `true` when a usable location field was resolved on the data source. | | `defaultZoom` | `number` | Mirrors the resolved option — pass to a map view's `defaultZoom` prop. | | `focusedZoom` | `number` | Mirrors the resolved option — pass to a map view's `focusedZoom` prop. | | `fitBoundsPadding` | `number` | Mirrors the resolved option — pass to a map view's `fitBoundsPadding` prop. | | `views` | `SavedDataGridView[]` | Saved views from `useDocyrusDataGrid`. | | `fields` | `DataSourceField[]` | Full field list from `useDocyrusDataGrid`. | | `dataSource` | `DataSourceMetadata \| null` | Resolved Docyrus data source metadata. | | `activeViewId` | `string \| null` | Currently active saved view id. | | `setActiveViewId` | `(id) => void` | Switch the active saved view. | | `onMapClick` | `((position) => void) \| undefined` | Mirrors the option — pass to a map view's `onMapClick` prop. | | `requestedColumns` | `string` | Final comma-separated columns string sent to the items endpoint. | | `resolvedListParams` | `DocyrusDataGridListParams` | Full query payload passed to the Docyrus items endpoint. | | `center` | `{ lat: number; lng: number }` | Computed center for the current marker set. | | `bounds` | `{ north; south; east; west } \| null` | Bounding box for the current marker set. | | `selectedMarkerId` | `string \| null` | Currently selected marker id. Updated by popup open/close events. | | `selectedMarker` | `DocyrusMapMarker \| null` | Full selected marker object. | | `selectMarker` | `(id: string) => void` | Programmatically select a marker (e.g. from a side list). Pass to `onSelectMarker`. | | `setSelectedMarkerId` | `(id: string \| null) => void` | Raw selection setter — prefer `selectMarker` / `clearSelectedMarker`. | | `clearSelectedMarker` | `() => void` | Clears the active selection. | | `counts` | `{ total; mapped; unmapped }` | Summary metrics for the active result set. | | `googleMapsApiKey` | `string \| null` | Resolved Google Maps API key. | | `reload` | `() => void` | Refetches the saved-view metadata and the map items query. | | `isLoading` | `boolean` | Combined loading state for metadata + items. | | `error` | `Error \| null` | First query error, if any. | ## `DocyrusLeafletMapView` Renders the hook's marker model on top of the Docyrus UI Leaflet map. | Prop | Type | Default | Description | |------|------|---------|-------------| | `markers` | `DocyrusMapMarker[]` | — | Marker models from `useDocyrusMapView`. | | `center` | `{ lat; lng }` | — | Center point used when bounds are not available. | | `bounds` | `DocyrusMapBounds \| null` | — | Bounding box used for `fitBounds`. | | `defaultZoom` | `number` | `6` | Base zoom level. | | `focusedZoom` | `number` | `14` | Zoom level for a single marker. | | `fitBoundsPadding` | `number` | `48` | Padding used in `fitBounds`. | | `selectedMarkerId` | `string \| null` | — | Tracks the currently selected marker for list ↔ map synchronization. Does not control popup visibility directly — popups are managed by Leaflet natively. | | `onSelectMarker` | `(id: string \| null) => void` | — | Called when a marker popup opens or closes, and when the map background is clicked. | | `onMapClick` | `(position) => void` | — | Called on a click on empty map area (not a marker). Reverse-geocodes the position and re-fires with `address`. | | `onEditMarker` | `(marker) => void` | — | Edit button handler used by the built-in popup. | | `onViewportChange` | `(bounds \| null) => void` | — | Fires on `moveend` with the new visible bounds. Pass `mapView.setViewportBounds` here to power viewport filtering. | | `draggableMarkers` | `boolean` | `false` | Make each marker draggable. | | `onMarkerMove` | `(marker, newPosition) => void \| Promise` | — | Fired after a marker is dropped. Pass `mapView.handleMarkerMove` for default PATCH+reload behavior. | | `allMarkersCount` | `number` | — | Underlying marker count before viewport filtering. When provided and non-zero, the map keeps rendering even if the filtered `markers` array is empty (so panning to an area without records doesn't unmount the map). Pass `mapView.allMarkers.length`. | | `basemaps` | `DocyrusBasemapDefinition[]` | — | Basemap options to surface in the in-map picker. Pass `mapView.availableBasemaps`. Picker shows when at least 2 entries are provided. | | `activeBasemap` | `DocyrusBasemapDefinition \| null` | — | Currently selected basemap. Pass `mapView.activeBasemap`. | | `onBasemapChange` | `(id: string) => void` | — | Called when the user picks a different basemap. Pass `mapView.setActiveBasemap`. | | `renderPopup` | `(marker) => ReactNode` | default popup | Custom popup renderer. | | `renderMarkerIcon` | `(marker, isSelected) => ReactNode` | default pin | Custom marker icon renderer. Called with `isSelected=false` on creation and `isSelected=true/false` imperatively when the popup opens/closes. | | `emptyState` | `ReactNode` | built-in | Custom empty state when no markers exist. | | `className` | `string` | — | Wrapper class name. | ## `DocyrusGoogleMapView` Renders the same marker model on Google Maps. | Prop | Type | Default | Description | |------|------|---------|-------------| | `apiKey` | `string \| null` | env | Google Maps API key. When absent, the component renders a graceful fallback. | | `markers` | `DocyrusMapMarker[]` | — | Marker models from `useDocyrusMapView`. | | `center` | `{ lat; lng }` | — | Center point used when bounds are not available. | | `bounds` | `DocyrusMapBounds \| null` | — | Bounding box used for `fitBounds`. | | `defaultZoom` | `number` | `6` | Base zoom level. | | `focusedZoom` | `number` | `14` | Zoom level for a single marker. | | `fitBoundsPadding` | `number` | `48` | Padding used in `fitBounds`. | | `defaultMapType` | `DocyrusGoogleMapType` | `'roadmap'` | Initial Google `mapTypeId`. Ignored when `activeBasemap` is provided. | | `selectedMarkerId` | `string \| null` | — | Opens the selected marker popup. | | `onSelectMarker` | `(id: string \| null) => void` | — | Called when a marker or the map background is clicked. | | `onMapClick` | `(position) => void` | — | Called on a click on empty map area (not a marker). | | `onEditMarker` | `(marker) => void` | — | Edit button handler used by the built-in popup. | | `onViewportChange` | `(bounds \| null) => void` | — | Fires on Google's `idle` event with the new visible bounds. Pass `mapView.setViewportBounds` here to power viewport filtering. | | `draggableMarkers` | `boolean` | `false` | Make each marker draggable. | | `onMarkerMove` | `(marker, newPosition) => void \| Promise` | — | Fired after a marker is dropped. Pass `mapView.handleMarkerMove` for default PATCH+reload behavior. | | `allMarkersCount` | `number` | — | Underlying marker count before viewport filtering. Keeps the map mounted when the filtered `markers` array is empty. Pass `mapView.allMarkers.length`. | | `basemaps` | `DocyrusBasemapDefinition[]` | — | Basemap options to surface in the in-map picker. Pass `mapView.availableBasemaps`. Picker shows when at least 2 entries are provided. | | `activeBasemap` | `DocyrusBasemapDefinition \| null` | — | Currently selected basemap. Pass `mapView.activeBasemap`. | | `onBasemapChange` | `(id: string) => void` | — | Called when the user picks a different basemap. Pass `mapView.setActiveBasemap`. | | `getMarkerIcon` | `(marker, isSelected) => string \| DocyrusMapMarkerIcon \| null` | default | Custom Google icon resolver. Return `null` to fall through to the built-in image/icon logic. | | `renderPopup` | `(marker) => ReactNode` | default popup | Custom info-window renderer. | | `emptyState` | `ReactNode` | built-in | Custom empty state when no markers exist. | | `className` | `string` | — | Wrapper class name. | ## Type Exports | Type | Description | |------|-------------| | `DocyrusMapMarker` | Provider-agnostic marker model with `id`, `title`, `position`, optional `imageUrl` / `iconUrl` / `iconColor`, and the underlying `item`. | | `DocyrusMapMarkerBuilderContext` | Context passed to a custom `getMarker` builder. | | `DocyrusMapPoint` | `{ lat: number; lng: number }`. | | `DocyrusMapBounds` | `{ north; south; east; west }` rectangle. | | `DocyrusNormalizedLocation` | Shape used internally for location-field values (`lat`, `lng`, plus address/description/details/placeId). | | `DocyrusLocationValue` | Loose location-field payload that the hook normalizes (supports both `latitude`/`longitude` and `lat`/`lng`). | | `DocyrusMapProvider` | `'leaflet' \| 'google'`. | | `DocyrusGoogleMapType` | `'roadmap' \| 'satellite' \| 'hybrid' \| 'terrain'`. | | `DocyrusBasemapId` | Built-in basemap id: `'street' \| 'satellite' \| 'hybrid' \| 'terrain'`. | | `DocyrusBasemapDefinition` | Custom basemap shape: `{ id, label, leafletUrl, leafletDarkUrl?, leafletAttribution?, googleMapType }`. | | `DocyrusMapMarkerIcon` | `{ url, size? }` icon descriptor returned by Google's `getMarkerIcon`. | | `DEFAULT_DOCYRUS_BASEMAPS` | Built-in basemap registry, keyed by `DocyrusBasemapId`. Useful for cloning/extending presets. | | `UseDocyrusMapViewOptions` | Hook option shape (extends `UseDocyrusDataGridOptions`). | | `UseDocyrusMapViewResult` | Hook return shape. |