Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(examples): react widgets get started #8889

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/api-reference/core/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,7 +43,9 @@ class LoadingIndicator implements Widget {
}
}

deckgl.addWidget(new LoadingIndicator({size: 48}));
new Deck({
widgets=[new LoadingIndicator({size: 48})]
});
```

## Widget Interface
Expand Down
2 changes: 1 addition & 1 deletion examples/get-started/pure-js/widgets/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "deckgl-example-pure-js-basic",
"name": "deckgl-example-pure-js-widgets",
"version": "0.0.0",
"private": true,
"license": "MIT",
Expand Down
17 changes: 17 additions & 0 deletions examples/get-started/react/widgets/README.md
Original file line number Diff line number Diff line change
@@ -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.
158 changes: 158 additions & 0 deletions examples/get-started/react/widgets/app.jsx
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Pessimistress if this too complex for get-started then maybe I can write a custom widget developer guide. I think it'd be common for a react user to want to implement their own widget, and we could document a basic pattern they could use.

Original file line number Diff line number Diff line change
@@ -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,

Check failure on line 5 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'CompassWidget' is defined but never used
ZoomWidget,

Check failure on line 6 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'ZoomWidget' is defined but never used
FullscreenWidget,

Check failure on line 7 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'FullscreenWidget' is defined but never used
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)');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I wasn't aware that there was a standardized way to query for dark mode. Seems like something that could be built into the widget theming system as a default? PreferredTheme or something like that? Though maybe it is better for exposition to show it here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the exposition here since apps implement their themes in so many different ways. Maybe they don't want the "Glass" version, maybe they let the user override theme in a setting, etc..

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) {

Check failure on line 36 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'props' is already declared in the upper scope on line 32 column 20
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));

Check failure on line 46 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'requestAnimationFrame' is not defined
return el;
}

onRemove() {
requestAnimationFrame(() => setContainer(null));

Check failure on line 51 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'requestAnimationFrame' is not defined
}

setProps(props) {

Check failure on line 54 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'props' is already declared in the upper scope on line 32 column 20
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)

Check warning on line 83 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

Unexpected console statement

Check failure on line 83 in examples/get-started/react/widgets/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'console' is not defined

const onClick = () => {
// eslint-disable-next-line
// alert('React widget!');
setPlacement('top-right');
};

const infoWidgetEl = (
<div className="deck-widget" style={widgetTheme}>
<div className="deck-widget-button">
<button className="deck-widget-icon-button" onClick={onClick}>
<div style={{color: 'var(--button-icon-idle)'}}>i</div>
</button>
</div>
</div>
);

return (
<>
{infoWidget.container && createPortal(infoWidgetEl, infoWidget.container)}
<DeckWidget ref={buttonWidget} id="component" placement={placement}>
{infoWidgetEl}
</DeckWidget>
<DeckGL
controller={true}
initialViewState={INITIAL_VIEW_STATE}
widgets={[
// new ZoomWidget({style: widgetTheme}),
// new CompassWidget({style: widgetTheme}),
// new FullscreenWidget({style: widgetTheme}),
infoWidget.widget,
buttonWidget.current
]}
layers={[
new GeoJsonLayer({
id: 'base-map',
data: COUNTRIES,
// Styles
stroked: true,
filled: true,
lineWidthMinPixels: 2,
opacity: 0.4,
getLineColor: [60, 60, 60],
getFillColor: [200, 200, 200]
}),
new GeoJsonLayer({
id: 'airports',
data: AIR_PORTS,
// Styles
filled: true,
pointRadiusMinPixels: 2,
pointRadiusScale: 2000,
getPointRadius: f => 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(<Root />);
12 changes: 12 additions & 0 deletions examples/get-started/react/widgets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>deck.gl Example</title>
<style>
body {margin: 0; width: 100vw; height: 100vh; overflow: hidden;}
</style>
</head>
<body></body>
<script type="module" src="app.jsx"></script>
</html>
20 changes: 20 additions & 0 deletions examples/get-started/react/widgets/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
2 changes: 2 additions & 0 deletions modules/core/src/lib/widget-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions modules/widgets/src/compass-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions modules/widgets/src/fullscreen-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading