diff --git a/docs/api-reference/core/widget.md b/docs/api-reference/core/widget.md index 6545c6fedc2..1b95db1d98a 100644 --- a/docs/api-reference/core/widget.md +++ b/docs/api-reference/core/widget.md @@ -12,7 +12,7 @@ You may find many ready-to-use widgets in the `@deck.gl/widgets` module. A widget is expected to implement the `Widget` interface. Here is a custom widget that shows a spinner while layers are loading: ```ts -import {Widget} from '@deck.gl/core'; +import {Deck, Widget} from '@deck.gl/core'; class LoadingIndicator implements Widget { element?: HTMLDivElement; @@ -43,7 +43,9 @@ class LoadingIndicator implements Widget { } } -deckgl.addWidget(new LoadingIndicator({size: 48})); +new Deck({ + widgets=[new LoadingIndicator({size: 48})] +}); ``` ## Widget Interface diff --git a/examples/get-started/pure-js/widgets/package.json b/examples/get-started/pure-js/widgets/package.json index f025ce2262b..3c762bf6815 100644 --- a/examples/get-started/pure-js/widgets/package.json +++ b/examples/get-started/pure-js/widgets/package.json @@ -1,5 +1,5 @@ { - "name": "deckgl-example-pure-js-basic", + "name": "deckgl-example-pure-js-widgets", "version": "0.0.0", "private": true, "license": "MIT", diff --git a/examples/get-started/react/widgets/README.md b/examples/get-started/react/widgets/README.md new file mode 100644 index 00000000000..277a76dc22d --- /dev/null +++ b/examples/get-started/react/widgets/README.md @@ -0,0 +1,17 @@ +## Example: Use deck.gl with widgets + +Uses [Vite](https://vitejs.dev/) to bundle and serve files. + +## Usage + +To install dependencies: + +```bash +npm install +# or +yarn +``` + +Commands: +* `npm start` is the development target, to serve the app and hot reload. +* `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/examples/get-started/react/widgets/app.jsx b/examples/get-started/react/widgets/app.jsx new file mode 100644 index 00000000000..e276cdefa67 --- /dev/null +++ b/examples/get-started/react/widgets/app.jsx @@ -0,0 +1,158 @@ +import React, {useState, forwardRef, useImperativeHandle, useMemo, useRef} from 'react'; +import {createRoot} from 'react-dom/client'; +import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; +import { + CompassWidget, + ZoomWidget, + FullscreenWidget, + DarkGlassTheme, + LightGlassTheme +} from '@deck.gl/widgets'; +import '@deck.gl/widgets/stylesheet.css'; +import {createPortal} from 'react-dom'; + +/* global window */ +const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); +const widgetTheme = prefersDarkScheme.matches ? DarkGlassTheme : LightGlassTheme; + +// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz +const COUNTRIES = + 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line +const AIR_PORTS = + 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson'; + +const INITIAL_VIEW_STATE = { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 +}; + +function useWidget(props = {}) { + const [container, setContainer] = useState(null); + + class ReactWidget { + constructor(props) { + this.id = props.id || 'react'; + this.placement = props.placement || 'top-left'; + this.viewId = props.viewId; + this.props = props; + } + + onAdd() { + const el = document.createElement('div'); + // Defer state update to avoid conflicts with rendering + requestAnimationFrame(() => setContainer(el)); + return el; + } + + onRemove() { + requestAnimationFrame(() => setContainer(null)); + } + + setProps(props) { + this.props = props; + this.placement = props.placement || this.placement; + this.viewId = props.viewId || this.viewId; + } + } + + const widget = useMemo(() => new ReactWidget(props), [props]); + + return { + widget, + container + }; +} + +function DeckWidgetWithRef(props, ref) { + const { widget, container } = useWidget(props); + + useImperativeHandle(ref, () => widget, [widget]); + + return container ? createPortal(props.children, container) : null; +} + +const DeckWidget = forwardRef(DeckWidgetWithRef); + +function Root() { + const [placement, setPlacement] = useState('top-left'); + const infoWidget = useWidget({id: 'hook'}); + const buttonWidget = useRef(null); + console.log(infoWidget, buttonWidget) + + const onClick = () => { + // eslint-disable-next-line + // alert('React widget!'); + setPlacement('top-right'); + }; + + const infoWidgetEl = ( +
+
+ +
+
+ ); + + return ( + <> + {infoWidget.container && createPortal(infoWidgetEl, infoWidget.container)} + + {infoWidgetEl} + + 11 - f.properties.scalerank, + getFillColor: [200, 0, 80, 180] + }), + new ArcLayer({ + id: 'arcs', + data: AIR_PORTS, + dataTransform: d => d.features.filter(f => f.properties.scalerank < 4), + // Styles + getSourcePosition: f => [-0.4531566, 51.4709959], // London + getTargetPosition: f => f.geometry.coordinates, + getSourceColor: [0, 128, 200], + getTargetColor: [200, 0, 80], + getWidth: 1 + }) + ]} + /> + + ); +} + +/* global document */ +const container = document.body.appendChild(document.createElement('div')); +createRoot(container).render(); diff --git a/examples/get-started/react/widgets/index.html b/examples/get-started/react/widgets/index.html new file mode 100644 index 00000000000..38a008bc464 --- /dev/null +++ b/examples/get-started/react/widgets/index.html @@ -0,0 +1,12 @@ + + + + + deck.gl Example + + + + + diff --git a/examples/get-started/react/widgets/package.json b/examples/get-started/react/widgets/package.json new file mode 100644 index 00000000000..eb4458f7ce7 --- /dev/null +++ b/examples/get-started/react/widgets/package.json @@ -0,0 +1,20 @@ +{ + "name": "deckgl-example-react-widgets", + "version": "0.0.0", + "private": true, + "license": "MIT", + "scripts": { + "start": "vite --open", + "start-local": "vite --config ../../../vite.config.local.mjs", + "build": "vite build" + }, + "dependencies": { + "deck.gl": "^9.0.0", + "@deck.gl/widgets": "^9.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "vite": "^4.0.0" + } +} diff --git a/modules/core/src/lib/widget-manager.ts b/modules/core/src/lib/widget-manager.ts index 2ae837ee21f..f2fc1a5eb71 100644 --- a/modules/core/src/lib/widget-manager.ts +++ b/modules/core/src/lib/widget-manager.ts @@ -137,6 +137,8 @@ export class WidgetManager { } for (let widget of nextWidgets) { + if (!widget) continue; + console.log(widget) const oldWidget = oldWidgetMap[widget.id]; if (!oldWidget) { // Widget is new diff --git a/modules/widgets/src/compass-widget.tsx b/modules/widgets/src/compass-widget.tsx index a81840c39a4..ae9ed4ed85f 100644 --- a/modules/widgets/src/compass-widget.tsx +++ b/modules/widgets/src/compass-widget.tsx @@ -5,6 +5,9 @@ import {render} from 'preact'; interface CompassWidgetProps { id?: string; + /** + * Widget positioning within the view. Default 'top-left'. + */ placement?: WidgetPlacement; /** * View to attach to and interact with. Required when using multiple views. diff --git a/modules/widgets/src/fullscreen-widget.tsx b/modules/widgets/src/fullscreen-widget.tsx index 2829a7f4ec5..74cf925de63 100644 --- a/modules/widgets/src/fullscreen-widget.tsx +++ b/modules/widgets/src/fullscreen-widget.tsx @@ -6,6 +6,9 @@ import {IconButton} from './components'; interface FullscreenWidgetProps { id?: string; + /** + * Widget positioning within the view. Default 'top-left'. + */ placement?: WidgetPlacement; /** * A [compatible DOM element](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen#Compatible_elements) which should be made full screen.