# 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. |