From 101aaf9f2916db8c8ee76abc41a6814526a6dce7 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 17 Dec 2024 21:13:51 -0800 Subject: [PATCH 01/21] docs(widgets) Widget Developer Guide --- docs/api-reference/core/widget.md | 3 +- docs/developer-guide/custom-widgets/README.md | 70 ++++ .../custom-widgets/react-widgets.md | 131 +++++++ .../custom-widgets/universal-widgets.md | 339 ++++++++++++++++++ docs/table-of-contents.json | 9 + 5 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 docs/developer-guide/custom-widgets/README.md create mode 100644 docs/developer-guide/custom-widgets/react-widgets.md create mode 100644 docs/developer-guide/custom-widgets/universal-widgets.md diff --git a/docs/api-reference/core/widget.md b/docs/api-reference/core/widget.md index 6545c6fedc2..f75577bbcab 100644 --- a/docs/api-reference/core/widget.md +++ b/docs/api-reference/core/widget.md @@ -21,6 +21,7 @@ class LoadingIndicator implements Widget { constructor(options: { size: number; }) { + this.id = 'loading-indicator' this.size = options.size; } @@ -48,7 +49,7 @@ deckgl.addWidget(new LoadingIndicator({size: 48})); ## Widget Interface -When a widget instance is added to Deck, the user can optionally specify a `viewId` that it is attached to (default `null`). If assigned, this widget will only respond to events occured inside the specific view that matches this id. +When a widget instance is added to Deck, the user can optionally specify a `viewId` that it is attached to (default `null`). If assigned, this widget will only respond to events occurred inside the specific view that matches this id. ### Members diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md new file mode 100644 index 00000000000..db01d75f5dc --- /dev/null +++ b/docs/developer-guide/custom-widgets/README.md @@ -0,0 +1,70 @@ +# Writing Your Own Widget + +## Preparations + +There are a many ways to build a widget in deck.gl, and it is helpful to consider what approach will serve you best before starting. We've provided guides for commonly used approaches: + +* **[Implement a universal widget](./universal-widgets.md)** - A universal widget is compatible with any deck.gl application and is UI framework agnostic. This option is best for developing widgets to be used throughout the deck.gl ecosystem. +* **[Create a react widget](./react-widgets.md)** - A react widget utilizes the convenience of react to develop the UI for your widget. It is tightly coupled to your react application, being mounted in the same root as the rest of your UI. This option is best for developing widgets custom to your react application. + + +## Creating The Widget class + +Your widget class must implement the [Widget](../../api-reference/core/widget.md) interface. + +```ts +import type {Widget} from '@deck.gl/core'; + +class AwesomeWidget implements Widget { + constructor(props) { + this.id = props.id || 'awesome-widget'; + this.props = { ...props }; + } + onAdd() {...} + onRemove() {...} +} +``` + +It's most convenient to use TypeScript, but it can also be implemented in JavaScript. + +### Defining Widget Properties + +The list of properties is the main API your new widget will provide to +applications. So it makes sense to carefully consider what properties +your widget should offer. + +You also need to define the default values of the widget's properties. + +```ts +import type {WidgetPlacement} from '@deck.gl/core' + +interface AwesomeWidgetProps { + id?: string; + /** + * Widget positioning within the view. Default: 'top-left'. + */ + placement?: WidgetPlacement; + /** + * View to attach to and interact with. Required when using multiple views. Default: null + */ + viewId?: string | null; + ... +} + +class AwesomeWidget implements Widget { + constructor(props: AwesomeWidgetProps) { + this.id = props.id || 'awesome-widget'; + this.placement = props.placement || 'top-left'; + this.viewId = props.viewId || null; + + this.props = { ...props } + } +} +``` + +## Best Practices + +- **Plan Your API:** Clearly define the properties and events your widget will expose so that its easy for developers to integrate into their applications. +- **Handle Lifecycle Events:** Implement lifecycle methods like `onAdd`, `onRemove`, and `setProps` to manage the widget's updates effectively. +- **Optimize for Performance:** Minimize unnecessary DOM re-renders and resource usage by carefully managing state updates. +- **Ensure Accessibility:** Provide options for styling and interactions that respect user preferences, such as keyboard navigation and screen reader support. \ No newline at end of file diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md new file mode 100644 index 00000000000..4f8e7bbf2cf --- /dev/null +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -0,0 +1,131 @@ +# React Widgets + +React widgets are a powerful way to integrate custom UI elements into deck.gl applications using the React framework. This guide will walk you through the process of building React-based widgets and best practices. + +We recommend users writing their own react widgets be familiar with implementing the `Widget` interface, consider reviewing the [Universal Widgets](./universal-widgets.md) guide. + +## Why Use React Widgets? + +React widgets leverage the strengths of React’s component model, allowing: + - **Easy Composition:** Reuse and combine components within the React ecosystem. + - **React Lifecycle Integration:** Utilize React’s lifecycle hooks to manage state and updates. + - **Declarative UI:** Define your UI in a predictable and straightforward manner using JSX. + +React widgets are most suitable when you are working on React applications and do not intend to distribute your widget outside of your application. + +## Writing a React Widget + +### Prerequisites + +Ensure your deck.gl project includes the `@deck.gl/react` package to utilize React-specific utilities, such as the [`useWidget`](../../api-reference/react/use-widget.md) hook. + +Install the package if it’s not already included: + +```sh +npm install @deck.gl/react +``` + +### Example: Creating a React Widget + +Below is a step-by-step example of implementing a simple React widget. + +#### Define Your Widget Class + +Start by creating the core widget class, which must implement the [Widget](../../api-reference/core/widget.md) interface. + +```ts +import type { Widget, WidgetPlacement } from '@deck.gl/react';= +import { useWidget } from '@deck.gl/react'; +import React, { useRef, RefObject } from 'react'; + +interface RotateWidgetProps { + id?: string + placement?: WidgetPlacement + ref: RefObject +} + +class RotateWidget { + constructor(props: BearingWidgetProps) { + this.id = props.id || 'bearing-widget'; + this.placement = props.placement || 'top-right'; + this.props = props; + this.viewports = {}; + } + + onAdd({ deck }) { + this.deck = deck; + return this.props.ref.current; + } + + onViewportChange(viewport) { + this.viewports[viewport.id] = viewport; + } + + handleRotate(viewport, bearingDelta) { + const nextBearing = viewport.bearing + bearingDelta; + const viewState = { + ...viewport, + bearing: nextBearing + }; + this.deck.setProps({ viewState }); + } + + handleCWRotate() { + Object.values(this.viewports).forEach(viewport => this.handleRotate(viewport, 90)); + } + + handleCCWRotate() { + Object.values(this.viewports).forEach(viewport => this.handleRotate(viewport, -90)); + } +} +``` + +#### Create a React Component + +Wrap the widget class in a React component using the [`useWidget`](../../api-reference/react/use-widget.md) hook. + +```tsx +export const RotateReactWidget = (props) => { + const ref = useRef(); + const widget = useWidget(RotateWidget, { ref, ...props }); + + return ( +
+ + +
+ ); +}; +``` + +This widget controls the bearing of the view its attached to. + +### Styling Your React Widget + +#### Inline Styles + +React widgets can be written to accept `style` props for inline styling. + +```tsx + +``` + +#### CSS Classes + +React widgets can use `className` and add styles to their stylesheet. + +```css +.custom-rotate-widget { + padding: 10px; + background-color: #333; + color: white; +} +``` diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md new file mode 100644 index 00000000000..8bb149704c2 --- /dev/null +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -0,0 +1,339 @@ +# Universal Widgets + +Widgets in deck.gl allow developers to create custom UI elements that are deeply integrated into the deck.gl rendering system. This guide covers the steps to implement widgets that are framework-agnostic, ensuring compatibility across various applications. + +## Implementing the Widget Lifecycle Functions + +The widget lifecycle functions define how a widget initializes, updates, and cleans up its integration with deck.gl. + +### Adding a Widget + +[`onAdd({deck, viewId}): HTMLElement?`](../../api-reference/core/widget.md#onadd) - This method provides deck.gl with the root DOM element of your widget. This element is positioned based on `placement` and `viewId` members. + +```ts +import { type Widget } from '@deck.gl/core' + +class AwesomeWidget implements Widget { + onAdd({ deck, viewId }) { + const element = document.createElement('div'); + // Initialize and style your element + return element + } +} +``` + +### Updating Properties + +[`setProps(props)`](../../api-reference/core/widget.md#setprops) - This method is called whenever widget properties are updated. Use this to apply changes dynamically. + +```ts +class AwesomeWidget implements Widget { + setProps(props: Partial) { + Object.assign(this.props, props); + } +} +``` + +### Handling Viewport Changes + +[`onViewportChange(viewport: Viewport)`](../../api-reference/core/widget.md#onviewportchange) - Widgets can listen to viewport updates via the `onViewportChange` method. You can target specific viewports or listen to all. + +Specific viewport: + +```ts +class AwesomeWidget implements Widget { + viewId = 'minimap' + + onViewportChange(viewport: Viewport) { + // Handle updates for the "minimap" viewport + } +} +``` + +All viewports: + +```ts +class AwesomeWidget implements Widget { + viewId = null + + onViewportChange(viewport: Viewport) { + // Handle updates for all viewports + } +} +``` + +### Redrawing Widget + +[`onRedraw({viewports, layers})`] - This method is invoked during rendering. This is useful for responding to changes in layers or viewports, such as a `HeatmapLayer`'s `colorRange` prop. + +```ts +class AwesomeWidget implements Widget { + onRedraw({ viewports, layers }) { + // Update widget visuals + } +} +``` + +### Removing Widget + +[`onRemove()`](../../api-reference/core/widget.md#onremove) - If implemented, this method is called when your widget is removed. This is a good time to clean up resources. + +```ts +class AwesomeWidget implements Widget { + onRemove() { + // Cleanup code + } +} +``` + +----- + +## Handling View Interaction Events + +A widget can update in response to a user interacting with the deck.gl view the widget is attached to. + +```ts +class AwesomeWidget implements Widget { + viewId = 'minimap' + + onClick(info, event) { + // Called when a click event occurs in the minimap view. + } +} +``` + +See [`Methods`](../../api-reference/core/widget.md#methods) for a complete list. + +## Customizing Styles + +A good universal widget provides users with ways to customize the styles of any elements it creates. Three common ways to do with are: + +- Inline styles prop +- CSS class prop +- CSS variables + +### Inline Styles Prop + +Define a prop for overriding styles inline + +```ts +interface AwesomeWidgetProps { + style?: Partial; +} +``` + +Apply the styles to your widget + +```ts +import { + type Widget, + _deepEqual as deepEqual, + _applyStyles as applyStyles, + _removeStyles as removeStyles +} from '@deck.gl/core' + +class AwesomeWidget implements Widget { + + constructor(props: AwesomeWidgetProps) { + ... + this.props = { + ...props, + style: props.style || {} + } + } + + onAdd() { + const {style} = this.props; + const element = document.createElement('div'); + applyStyles(element, style); + ... + } + + setProps(props: Partial) { + const el = this.element; + if (el) { + if (!deepEqual(oldProps.style, props.style, 1)) { + removeStyles(el, oldProps.style); + applyStyles(el, props.style); + } + } + ... + } + +} +``` + +### CSS Class Prop + +Define a prop for adding a CSS class + +```ts +interface AwesomeWidgetProps { + /** + * Additional CSS class. + */ + className?: string; +} +``` + +Apply the CSS class to your widget + +```ts +import { type Widget } from '@deck.gl/core' + +class AwesomeWidget implements Widget { + + onAdd() { + const {className} = this.props; + const element = document.createElement('div'); + if (className) element.classList.add(className); + ... + } + + setProps(props: Partial) { + const oldProps = this.props; + const el = this.element; + if (el) { + if (oldProps.className !== props.className) { + if (oldProps.className) el.classList.remove(oldProps.className); + if (props.className) el.classList.add(props.className); + } + } + ... + } +} +``` + +### CSS Variables + +Define variables in the widget's CSS stylesheet + +```css +.my-awesome-widget { + background-color: var(--primary-color, rebeccapurple); +} +``` + +Override in user's application + +```css +.my-awesome-widget { + --primary-color: chartreuse; +} +``` + +## Example: Layer List Widget with Preact + +Below is a comprehensive example demonstrating a layer list widget implemented using Preact for dynamic UI rendering: + +```tsx +import { + _deepEqual as deepEqual, + _applyStyles as applyStyles, + _removeStyles as removeStyles +} from '@deck.gl/core' +import type { + Deck, Viewport, Widget, WidgetPlacement, Layer +} from '@deck.gl/core' +import {render} from 'preact'; + +interface LayerListWidgetProps { + id?: string; + /** + * Widget positioning within the view. Default: 'top-left'. + */ + placement?: WidgetPlacement; + /** + * View to attach to and interact with. Required when using multiple views. Default: null + */ + viewId?: string | null; + /** + * CSS inline style overrides. + */ + style?: Partial; + /** + * Additional CSS class. + */ + className?: string; +} + +class LayerListWidget implements Widget { + id = 'layer-list-widget'; + props: ZoomWidgetProps; + placement: WidgetPlacement = 'top-left'; + viewId?: string | null = null; + viewports: {[id: string]: Viewport} = {}; + layers: Layer[] = []; + deck?: Deck; + element?: HTMLDivElement; + + constructor(props: AwesomeWidgetProps) { + this.id = props.id || 'layer-list-widget'; + this.placement = props.placement || 'top-left'; + this.viewId = props.viewId || null; + + this.props = { + ...props, + style: props.style || {} + } + } + + onAdd({deck}: {deck: Deck}): HTMLDivElement { + const {style, className} = this.props; + const element = document.createElement('div'); + element.classList.add('deck-widget', 'deck-widget-zoom'); + if (className) element.classList.add(className); + applyStyles(element, style); + this.deck = deck; + this.element = element; + this.update(); + return element; + } + + onRedraw({layers}: {layers: Layer[]}) { + this.layers = layers; + this.update(); + } + + onViewportChange(viewport) { + this.viewports[viewport.id] = viewport + } + + update() { + const element = this.element; + if (!element) { + return; + } + let layers = this.layers + if (this.deck?.props.layerFilter) { + const ui = ( + {viewports.values().map(viewport => ( +
+ {viewport.id} +
    + {layers.filter(layer => ( + this.deck?.props.layerFilter({layer, viewport}) + )).map((layer) => { +
  • {layer.id}
  • + })} +
+
+ ))} + ); + render(ui, element); + } else { + const ui = ( +
    + {this.layers.map((layer) => ( +
  • {layer.id}
  • + ))} +
+ ) + render(ui, element); + } + } +} + +``` + +This widget dynamically renders a list of layers and updates as the deck.gl state changes. diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json index b10a77e2c7d..fc05836d318 100644 --- a/docs/table-of-contents.json +++ b/docs/table-of-contents.json @@ -69,6 +69,15 @@ "developer-guide/custom-layers/attribute-management", "developer-guide/custom-layers/writing-shaders" ] + }, + { + "type": "category", + "label": "Writing Custom Widgets", + "items": [ + "developer-guide/custom-widgets/README", + "developer-guide/custom-widgets/universal-widgets", + "developer-guide/custom-widgets/react-widgets" + ] } ] }, From ebd2b5097d369a0a37373f1cfee17d40d134b1a1 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 08:15:34 -0800 Subject: [PATCH 02/21] Update docs/developer-guide/custom-widgets/README.md Co-authored-by: Ib Green <7025232+ibgreen@users.noreply.github.com> --- docs/developer-guide/custom-widgets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index db01d75f5dc..688be0f3a0d 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -25,7 +25,7 @@ class AwesomeWidget implements Widget { } ``` -It's most convenient to use TypeScript, but it can also be implemented in JavaScript. +It's most convenient to use TypeScript, but widgets can also be implemented in JavaScript. ### Defining Widget Properties From 70248ce5db694d5b7516ab42a6bf5fee13bd279c Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 08:15:59 -0800 Subject: [PATCH 03/21] Update docs/developer-guide/custom-widgets/universal-widgets.md Co-authored-by: Ib Green <7025232+ibgreen@users.noreply.github.com> --- docs/developer-guide/custom-widgets/universal-widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index 8bb149704c2..593b3c209e2 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -64,7 +64,7 @@ class AwesomeWidget implements Widget { ### Redrawing Widget -[`onRedraw({viewports, layers})`] - This method is invoked during rendering. This is useful for responding to changes in layers or viewports, such as a `HeatmapLayer`'s `colorRange` prop. +[`onRedraw({viewports, layers})`] - This method is invoked when deck.gl performs rendering. This is useful for updating the widget UI to correspond to changes in layers or viewports, such as a `HeatmapLayer`'s `colorRange` prop. ```ts class AwesomeWidget implements Widget { From 026fd274fed032098a360ae20c776e136bceb1e5 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 08:19:05 -0800 Subject: [PATCH 04/21] Update docs/developer-guide/custom-widgets/README.md Co-authored-by: felixpalmer --- docs/developer-guide/custom-widgets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index 688be0f3a0d..9e180bc7da1 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -5,7 +5,7 @@ There are a many ways to build a widget in deck.gl, and it is helpful to consider what approach will serve you best before starting. We've provided guides for commonly used approaches: * **[Implement a universal widget](./universal-widgets.md)** - A universal widget is compatible with any deck.gl application and is UI framework agnostic. This option is best for developing widgets to be used throughout the deck.gl ecosystem. -* **[Create a react widget](./react-widgets.md)** - A react widget utilizes the convenience of react to develop the UI for your widget. It is tightly coupled to your react application, being mounted in the same root as the rest of your UI. This option is best for developing widgets custom to your react application. +* **[Create a react widget](./react-widgets.md)** - A React widget utilizes the convenience of react to develop the UI for your widget. It is tightly coupled to your react application, being mounted in the same root as the rest of your UI. This option is best for developing widgets custom to your React application. ## Creating The Widget class From 41cdfb39f7a866e34085c14ad1ca576ef7a7690a Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 10:45:24 -0800 Subject: [PATCH 05/21] adding preact page and vanilla example --- docs/developer-guide/custom-widgets/README.md | 3 +- .../custom-widgets/preact-widgets.md | 153 ++++++++++++++++++ .../custom-widgets/universal-widgets.md | 78 ++++----- 3 files changed, 188 insertions(+), 46 deletions(-) create mode 100644 docs/developer-guide/custom-widgets/preact-widgets.md diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index 9e180bc7da1..3fc0d5c9bbe 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -5,7 +5,8 @@ There are a many ways to build a widget in deck.gl, and it is helpful to consider what approach will serve you best before starting. We've provided guides for commonly used approaches: * **[Implement a universal widget](./universal-widgets.md)** - A universal widget is compatible with any deck.gl application and is UI framework agnostic. This option is best for developing widgets to be used throughout the deck.gl ecosystem. -* **[Create a react widget](./react-widgets.md)** - A React widget utilizes the convenience of react to develop the UI for your widget. It is tightly coupled to your react application, being mounted in the same root as the rest of your UI. This option is best for developing widgets custom to your React application. +* **[Use Preact in a universal widget](./preact-widgets.md)** - Preact is a lightweight virtual DOM commonly used to implement dynamic widgets without tightly coupling widget internals to an application's UI framework. +* **[Create a React widget](./react-widgets.md)** - A React widget utilizes the convenience of React to develop the UI for your widget. It is tightly coupled to your React application, being mounted in the same root as the rest of your UI. This option is best for developing widgets custom to your React application. ## Creating The Widget class diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md new file mode 100644 index 00000000000..fbdad56b0e7 --- /dev/null +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -0,0 +1,153 @@ +# Preact Widgets + +Preact widgets are an easy way to add dynamic UI elements into universal deck.gl widgets using the [Preact](https://preactjs.com/) UI library. This guide will walk you through the process of building Preact-based widgets and best practices. + +## Why Use Preact Widgets? + +Preact widgets leverage the strengths of React’s component model in a lighter weight library, allowing: + + - **Easy Composition:** Reuse and combine components. + - **Declarative UI:** Define your UI in a predictable and straightforward manner using JSX. + - **Small Size:** Preact is small enough that your code is the largest part of your application. + +Preact widgets are suitable when you are working with any UI framework and is lightweight enough to distribute with your widget in a library. + +> Tip: Read more about the differences between Preact and React [here](https://preactjs.com/guide/v10/differences-to-react/). + +## Writing a React Widget + +### Prerequisites + +Ensure your project includes the `preact` package. + +```sh +npm install preact +``` + +When using the TypeScript compiler, add the following configuration to your `tsconfig.json` to transpile JSX to Preact-compatible JavaScript: + +```json +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + } +} +``` + +> Note: Developer environments vary. Refer to the [Preact Typescript](https://preactjs.com/guide/v10/typescript) documentation for additional environments. + +## Example: Layer List Widget with Preact + +Below is a comprehensive example demonstrating a layer list widget implemented using Preact for dynamic UI rendering: + +```tsx +import { + _deepEqual as deepEqual, + _applyStyles as applyStyles, + _removeStyles as removeStyles +} from '@deck.gl/core' +import type { + Deck, Viewport, Widget, WidgetPlacement, Layer +} from '@deck.gl/core' +import {render} from 'preact'; + +interface LayerListWidgetProps { + id?: string; + /** + * Widget positioning within the view. Default: 'top-left'. + */ + placement?: WidgetPlacement; + /** + * View to attach to and interact with. Required when using multiple views. Default: null + */ + viewId?: string | null; + /** + * CSS inline style overrides. + */ + style?: Partial; + /** + * Additional CSS class. + */ + className?: string; +} + +class LayerListWidget implements Widget { + id = 'layer-list-widget'; + props: LayerListWidgetProps; + placement: WidgetPlacement = 'top-left'; + viewId?: string | null = null; + viewports: {[id: string]: Viewport} = {}; + layers: Layer[] = []; + deck?: Deck; + element?: HTMLDivElement; + + constructor(props: LayerListWidgetProps) { + this.id = props.id || 'layer-list-widget'; + this.placement = props.placement || 'top-left'; + this.viewId = props.viewId || null; + + this.props = { + ...props, + style: props.style || {} + } + } + + onAdd({deck}: {deck: Deck}): HTMLDivElement { + const {style, className} = this.props; + const element = document.createElement('div'); + element.classList.add('deck-widget', 'deck-widget-layer-list'); + if (className) element.classList.add(className); + applyStyles(element, style); + this.deck = deck; + this.element = element; + this.update(); + return element; + } + + onRedraw({layers}: {layers: Layer[]}) { + this.layers = layers; + this.update(); + } + + onViewportChange(viewport: Viewport) { + this.viewports[viewport.id] = viewport + } + + update() { + const element = this.element; + if (!element) { + return; + } + let layers = this.layers + if (this.deck?.props.layerFilter) { + const ui = ( + {viewports.values().map(viewport => ( +
+ {viewport.id} +
    + {layers.filter(layer => ( + this.deck?.props.layerFilter({layer, viewport}) + )).map((layer) => { +
  • {layer.id}
  • + })} +
+
+ ))} + ); + render(ui, element); + } else { + const ui = ( +
    + {this.layers.map((layer) => ( +
  • {layer.id}
  • + ))} +
+ ) + render(ui, element); + } + } +} +``` + +This widget dynamically renders a list of layers and updates as the deck.gl state changes. diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index 593b3c209e2..de13b4101f3 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -222,11 +222,11 @@ Override in user's application } ``` -## Example: Layer List Widget with Preact +## Example: Layer Loading Widget in Vanilla JS -Below is a comprehensive example demonstrating a layer list widget implemented using Preact for dynamic UI rendering: +Below is a comprehensive example demonstrating a widget indicating whether or not all asynchronous layers are loading implemented without any UI framework. -```tsx +```ts import { _deepEqual as deepEqual, _applyStyles as applyStyles, @@ -235,9 +235,8 @@ import { import type { Deck, Viewport, Widget, WidgetPlacement, Layer } from '@deck.gl/core' -import {render} from 'preact'; -interface LayerListWidgetProps { +interface LayerLoadingWidgetProps { id?: string; /** * Widget positioning within the view. Default: 'top-left'. @@ -258,19 +257,16 @@ interface LayerListWidgetProps { } class LayerListWidget implements Widget { - id = 'layer-list-widget'; - props: ZoomWidgetProps; + id = 'layer-loading-widget'; + props: LayerLoadingWidgetProps; placement: WidgetPlacement = 'top-left'; - viewId?: string | null = null; - viewports: {[id: string]: Viewport} = {}; layers: Layer[] = []; deck?: Deck; element?: HTMLDivElement; - constructor(props: AwesomeWidgetProps) { - this.id = props.id || 'layer-list-widget'; + constructor(props: LayerLoadingWidgetProps) { + this.id = props.id || 'layer-loading-widget'; this.placement = props.placement || 'top-left'; - this.viewId = props.viewId || null; this.props = { ...props, @@ -281,7 +277,7 @@ class LayerListWidget implements Widget { onAdd({deck}: {deck: Deck}): HTMLDivElement { const {style, className} = this.props; const element = document.createElement('div'); - element.classList.add('deck-widget', 'deck-widget-zoom'); + element.classList.add('deck-widget', 'deck-widget-layer-loading'); if (className) element.classList.add(className); applyStyles(element, style); this.deck = deck; @@ -295,45 +291,37 @@ class LayerListWidget implements Widget { this.update(); } - onViewportChange(viewport) { - this.viewports[viewport.id] = viewport - } - update() { const element = this.element; if (!element) { return; } - let layers = this.layers - if (this.deck?.props.layerFilter) { - const ui = ( - {viewports.values().map(viewport => ( -
- {viewport.id} -
    - {layers.filter(layer => ( - this.deck?.props.layerFilter({layer, viewport}) - )).map((layer) => { -
  • {layer.id}
  • - })} -
-
- ))} - ); - render(ui, element); - } else { - const ui = ( -
    - {this.layers.map((layer) => ( -
  • {layer.id}
  • - ))} -
- ) - render(ui, element); + + // Clear the element content + element.innerHTML = ''; + + // Check if all layers are loaded + let loaded = this.layers?.every(layer => layer.isLoaded); + + // Add a status indicator + const statusIndicator = document.createElement('div'); + statusIndicator.textContent = loaded ? 'All layers loaded' : 'Loading layers...'; + statusIndicator.style.fontWeight = 'bold'; + statusIndicator.style.color = loaded ? 'green' : 'red'; + element.appendChild(statusIndicator); + + // Add a list of layers with their load status + const layerList = document.createElement('ul'); + for (const layer of this.layers) { + const listItem = document.createElement('li'); + listItem.textContent = `${layer.id}: ${layer.isLoaded ? 'Loaded' : 'Loading'}`; + listItem.style.color = layer.isLoaded ? 'green' : 'red'; + layerList.appendChild(listItem); } + + element.appendChild(layerList); } } - ``` -This widget dynamically renders a list of layers and updates as the deck.gl state changes. +This widget provides a visual representation of layer load statuses and updates as the deck.gl state changes. From 077ddd915cd04a62ad6200c3ecc59ebf4cfdaa5b Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 10:46:11 -0800 Subject: [PATCH 06/21] onViewportChange --- docs/developer-guide/custom-widgets/universal-widgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index de13b4101f3..305cf5488a4 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -36,7 +36,7 @@ class AwesomeWidget implements Widget { ### Handling Viewport Changes -[`onViewportChange(viewport: Viewport)`](../../api-reference/core/widget.md#onviewportchange) - Widgets can listen to viewport updates via the `onViewportChange` method. You can target specific viewports or listen to all. +[`onViewportChange(viewport: Viewport)`](../../api-reference/core/widget.md#onviewportchange) - Widgets can listen to viewport updates via the `onViewportChange` method. A widget writer can target specific viewports by setting `viewId` or listen to all. Using props, an author could also choose to allow users to configure this targeting. Specific viewport: From 775c7b8e71a533d49ca82ce9290f7dc732f578ad Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 10:47:46 -0800 Subject: [PATCH 07/21] misc react docs --- docs/developer-guide/custom-widgets/react-widgets.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index 4f8e7bbf2cf..8c44539a413 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -1,8 +1,8 @@ # React Widgets -React widgets are a powerful way to integrate custom UI elements into deck.gl applications using the React framework. This guide will walk you through the process of building React-based widgets and best practices. +React widgets are a powerful way to integrate custom UI elements into deck.gl applications using the [React](https://react.dev/) framework. This guide will walk you through the process of building React-based widgets and best practices. -We recommend users writing their own react widgets be familiar with implementing the `Widget` interface, consider reviewing the [Universal Widgets](./universal-widgets.md) guide. +We recommend users writing their own React widgets be familiar with implementing the `Widget` interface, consider reviewing the [Universal Widgets](./universal-widgets.md) guide. ## Why Use React Widgets? @@ -34,7 +34,7 @@ Below is a step-by-step example of implementing a simple React widget. Start by creating the core widget class, which must implement the [Widget](../../api-reference/core/widget.md) interface. ```ts -import type { Widget, WidgetPlacement } from '@deck.gl/react';= +import type { Widget, WidgetPlacement } from '@deck.gl/core'; import { useWidget } from '@deck.gl/react'; import React, { useRef, RefObject } from 'react'; @@ -85,7 +85,7 @@ class RotateWidget { Wrap the widget class in a React component using the [`useWidget`](../../api-reference/react/use-widget.md) hook. ```tsx -export const RotateReactWidget = (props) => { +export const RotateReactWidget = (props: RotateWidgetProps) => { const ref = useRef(); const widget = useWidget(RotateWidget, { ref, ...props }); From e68bd41781ef5a0736a4f7d57fbb9ef1770f40dc Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 10:49:27 -0800 Subject: [PATCH 08/21] Styling Your React Widget --- .../custom-widgets/react-widgets.md | 63 ++++++++++++++++--- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index 8c44539a413..ac201d1c8fc 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -90,11 +90,7 @@ export const RotateReactWidget = (props: RotateWidgetProps) => { const widget = useWidget(RotateWidget, { ref, ...props }); return ( -
+
@@ -108,24 +104,71 @@ export const RotateReactWidget = (props: RotateWidgetProps) => { This widget controls the bearing of the view its attached to. -### Styling Your React Widget +#### Styling Your React Widget -#### Inline Styles +##### Adding Inline Styles -React widgets can be written to accept `style` props for inline styling. +Add the `style` prop in your React component for inline styling overrides. + +```tsx +export const RotateReactWidget = (props: RotateWidgetProps) => { + ... + return
...
+} +``` ```tsx ``` -#### CSS Classes +##### Adding CSS Classes + +Add `className` to your React component and styles to your stylesheet. + +```tsx +import 'style.css'; -React widgets can use `className` and add styles to their stylesheet. +export const RotateReactWidget = (props: RotateWidgetProps) => { + ... + return
...
+} +``` ```css +/* style.css */ .custom-rotate-widget { padding: 10px; background-color: #333; color: white; } ``` + +##### Applying the deck.gl widget design system + +Reuse the built-in deck.gl widget [stylesheet](https://unpkg.com/deck.gl@latest/dist/stylesheet.css) by importing them into your application. This can be useful if you're already theming deck.gl widgets and want to reuse CSS styles and variables. See [Widget Overview](../../api-reference/widgets/overview.md#custom-class-theming) for a full list of built-in variables. + +```tsx +import '@deck.gl/widgets/stylesheet.css'; +import 'style.css'; + +export const RotateReactWidget = (props: RotateWidgetProps) => { + const ref = useRef(); + const widget = useWidget(RotateWidget, { ref, ...props }); + return ( +
+
+ +
+
+ ) +} +``` + +```css +/* style.css */ +.deck-widget { + --button-corner-radius: 4px; +} +``` From 5291a26bb58ed422f99854f92424fdc2728e2ee5 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 10:53:04 -0800 Subject: [PATCH 09/21] TOC --- docs/table-of-contents.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json index fc05836d318..dc612d236d2 100644 --- a/docs/table-of-contents.json +++ b/docs/table-of-contents.json @@ -76,6 +76,7 @@ "items": [ "developer-guide/custom-widgets/README", "developer-guide/custom-widgets/universal-widgets", + "developer-guide/custom-widgets/preact-widgets", "developer-guide/custom-widgets/react-widgets" ] } From dcb07be1df9156707db77c0c87325ad70bddb8ab Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 11:14:43 -0800 Subject: [PATCH 10/21] type cleanup --- .../custom-widgets/preact-widgets.md | 6 ++---- .../custom-widgets/react-widgets.md | 4 ++-- .../custom-widgets/universal-widgets.md | 21 +++++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index fbdad56b0e7..56ecde22074 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -72,11 +72,9 @@ interface LayerListWidgetProps { className?: string; } -class LayerListWidget implements Widget { +class LayerListWidget implements Widget { id = 'layer-list-widget'; - props: LayerListWidgetProps; placement: WidgetPlacement = 'top-left'; - viewId?: string | null = null; viewports: {[id: string]: Viewport} = {}; layers: Layer[] = []; deck?: Deck; @@ -110,7 +108,7 @@ class LayerListWidget implements Widget { this.update(); } - onViewportChange(viewport: Viewport) { + onViewportChange(viewport) { this.viewports[viewport.id] = viewport } diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index ac201d1c8fc..de16a0ea893 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -44,8 +44,8 @@ interface RotateWidgetProps { ref: RefObject } -class RotateWidget { - constructor(props: BearingWidgetProps) { +class RotateWidget implements Widget { + constructor(props: RotateWidgetProps) { this.id = props.id || 'bearing-widget'; this.placement = props.placement || 'top-right'; this.props = props; diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index 305cf5488a4..75fcbdc096f 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -24,11 +24,11 @@ class AwesomeWidget implements Widget { ### Updating Properties -[`setProps(props)`](../../api-reference/core/widget.md#setprops) - This method is called whenever widget properties are updated. Use this to apply changes dynamically. +[`setProps(props: Partial)`](../../api-reference/core/widget.md#setprops) - This method is called whenever widget properties are updated. Use this to apply changes dynamically. ```ts -class AwesomeWidget implements Widget { - setProps(props: Partial) { +class AwesomeWidget implements Widget { + setProps(props) { Object.assign(this.props, props); } } @@ -44,7 +44,7 @@ Specific viewport: class AwesomeWidget implements Widget { viewId = 'minimap' - onViewportChange(viewport: Viewport) { + onViewportChange(viewport) { // Handle updates for the "minimap" viewport } } @@ -56,7 +56,7 @@ All viewports: class AwesomeWidget implements Widget { viewId = null - onViewportChange(viewport: Viewport) { + onViewportChange(viewport) { // Handle updates for all viewports } } @@ -132,7 +132,7 @@ import { _removeStyles as removeStyles } from '@deck.gl/core' -class AwesomeWidget implements Widget { +class AwesomeWidget implements Widget { constructor(props: AwesomeWidgetProps) { ... @@ -149,7 +149,7 @@ class AwesomeWidget implements Widget { ... } - setProps(props: Partial) { + setProps(props) { const el = this.element; if (el) { if (!deepEqual(oldProps.style, props.style, 1)) { @@ -181,7 +181,7 @@ Apply the CSS class to your widget ```ts import { type Widget } from '@deck.gl/core' -class AwesomeWidget implements Widget { +class AwesomeWidget implements Widget { onAdd() { const {className} = this.props; @@ -190,7 +190,7 @@ class AwesomeWidget implements Widget { ... } - setProps(props: Partial) { + setProps(props) { const oldProps = this.props; const el = this.element; if (el) { @@ -256,9 +256,8 @@ interface LayerLoadingWidgetProps { className?: string; } -class LayerListWidget implements Widget { +class LayerListWidget implements Widget { id = 'layer-loading-widget'; - props: LayerLoadingWidgetProps; placement: WidgetPlacement = 'top-left'; layers: Layer[] = []; deck?: Deck; From e48c1c940c354a9a4776ce27e2d7097033aabcf7 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 18 Dec 2024 17:55:42 -0800 Subject: [PATCH 11/21] Adding required class members --- docs/developer-guide/custom-widgets/README.md | 27 ++++++----- .../custom-widgets/preact-widgets.md | 9 ++-- .../custom-widgets/react-widgets.md | 13 ++++-- .../custom-widgets/universal-widgets.md | 45 +++++++++++++++++-- 4 files changed, 72 insertions(+), 22 deletions(-) diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index 3fc0d5c9bbe..d7b2e78ac57 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -17,12 +17,14 @@ Your widget class must implement the [Widget](../../api-reference/core/widget.md import type {Widget} from '@deck.gl/core'; class AwesomeWidget implements Widget { - constructor(props) { - this.id = props.id || 'awesome-widget'; - this.props = { ...props }; - } - onAdd() {...} - onRemove() {...} + id = 'awesome-widget'; + props; + constructor(props) { + this.id = props.id ?? this.id; + this.props = { ...props }; + } + onAdd() {...} + onRemove() {...} } ``` @@ -52,11 +54,16 @@ interface AwesomeWidgetProps { ... } -class AwesomeWidget implements Widget { +class AwesomeWidget implements Widget { + id = 'awesome-widget'; + props: AwesomeWidgetProps; + placement: WidgetPlacement = 'top-left'; + viewId?: string | null = null; + constructor(props: AwesomeWidgetProps) { - this.id = props.id || 'awesome-widget'; - this.placement = props.placement || 'top-left'; - this.viewId = props.viewId || null; + this.id = props.id ?? this.id; + this.placement = props.placement ?? this.placement; + this.viewId = props.viewId ?? this.viewId; this.props = { ...props } } diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index 56ecde22074..972d2b4dd18 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -74,6 +74,7 @@ interface LayerListWidgetProps { class LayerListWidget implements Widget { id = 'layer-list-widget'; + props: LayerListWidgetProps; placement: WidgetPlacement = 'top-left'; viewports: {[id: string]: Viewport} = {}; layers: Layer[] = []; @@ -81,13 +82,13 @@ class LayerListWidget implements Widget { element?: HTMLDivElement; constructor(props: LayerListWidgetProps) { - this.id = props.id || 'layer-list-widget'; - this.placement = props.placement || 'top-left'; - this.viewId = props.viewId || null; + this.id = props.id ?? this.id; + this.placement = props.placement ?? this.placement; + this.viewId = props.viewId ?? this.viewId; this.props = { ...props, - style: props.style || {} + style: props.style ?? {} } } diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index de16a0ea893..10c1b323cd2 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -45,11 +45,16 @@ interface RotateWidgetProps { } class RotateWidget implements Widget { + id = 'bearing-widget'; + props: RotateWidgetProps; + placement: WidgetPlacement = 'top-right'; + viewports: {[id: string]: Viewport} = {}; + deck?: Deck; + constructor(props: RotateWidgetProps) { - this.id = props.id || 'bearing-widget'; - this.placement = props.placement || 'top-right'; - this.props = props; - this.viewports = {}; + this.id = props.id ?? this.id; + this.placement = props.placement || this.placement; + this.props = { ...props }; } onAdd({ deck }) { diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index 75fcbdc096f..e9446111372 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -6,6 +6,37 @@ Widgets in deck.gl allow developers to create custom UI elements that are deeply The widget lifecycle functions define how a widget initializes, updates, and cleans up its integration with deck.gl. +### Constructing a Widget + +[`constructor(props: PropsT`)] - Initialize the widget's members with a constructor. + +```ts +import type { Widget, WidgetPlacement } from '@deck.gl/core' + +class AwesomeWidget implements Widget { + id = 'awesome-widget'; + props: AwesomeWidgetProps; + placement: WidgetPlacement = 'top-left'; + viewId?: string | null = null; + + constructor(props: AwesomeWidgetProps) { + // Required members + this.id = props.id ?? this.id; + this.props = { + ...props, + // Apply additional defaults + style: props.style ?? {} + } + + // Optional members + this.viewId = props.viewId ?? this.viewId; + this.placement = props.placement ?? this.placement; + } +} +``` + +> Warning: Avoid directly mutating the `props` object. Instead, apply modifications to a copy, e.g. `this.props = {...props, props.style || {}}` + ### Adding a Widget [`onAdd({deck, viewId}): HTMLElement?`](../../api-reference/core/widget.md#onadd) - This method provides deck.gl with the root DOM element of your widget. This element is positioned based on `placement` and `viewId` members. @@ -13,7 +44,7 @@ The widget lifecycle functions define how a widget initializes, updates, and cle ```ts import { type Widget } from '@deck.gl/core' -class AwesomeWidget implements Widget { +class AwesomeWidget implements Widget { onAdd({ deck, viewId }) { const element = document.createElement('div'); // Initialize and style your element @@ -258,18 +289,19 @@ interface LayerLoadingWidgetProps { class LayerListWidget implements Widget { id = 'layer-loading-widget'; + props: LayerLoadingWidgetProps; placement: WidgetPlacement = 'top-left'; layers: Layer[] = []; deck?: Deck; element?: HTMLDivElement; constructor(props: LayerLoadingWidgetProps) { - this.id = props.id || 'layer-loading-widget'; - this.placement = props.placement || 'top-left'; + this.id = props.id ?? this.id; + this.placement = props.placement ?? this.placement; this.props = { ...props, - style: props.style || {} + style: props.style ?? {} } } @@ -285,6 +317,11 @@ class LayerListWidget implements Widget { return element; } + setProps(props: Partial) { + this.placement = props.placement ?? this.placement; + this.props = {...props}; + } + onRedraw({layers}: {layers: Layer[]}) { this.layers = layers; this.update(); From 98a82a49c6b224c17f313079a888b4d4a496dd8d Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 20 Dec 2024 16:25:15 -0800 Subject: [PATCH 12/21] Add reactivity to examples --- .../developer-guide/custom-widgets/preact-widgets.md | 12 ++++++++++-- docs/developer-guide/custom-widgets/react-widgets.md | 9 ++++++++- .../custom-widgets/universal-widgets.md | 7 +++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index 972d2b4dd18..65020f5f2b7 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -104,6 +104,14 @@ class LayerListWidget implements Widget { return element; } + setProps(props: Partial) { + // Handle when props change here. + this.placement = props.placement ?? this.placement; + this.viewId = props.viewId ?? this.viewId; + this.props = {...props}; + this.update(); + } + onRedraw({layers}: {layers: Layer[]}) { this.layers = layers; this.update(); @@ -113,7 +121,7 @@ class LayerListWidget implements Widget { this.viewports[viewport.id] = viewport } - update() { + private update() { const element = this.element; if (!element) { return; @@ -121,7 +129,7 @@ class LayerListWidget implements Widget { let layers = this.layers if (this.deck?.props.layerFilter) { const ui = ( - {viewports.values().map(viewport => ( + {this.viewports.values().map(viewport => (
{viewport.id}
    diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index 10c1b323cd2..53a12576415 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -53,7 +53,7 @@ class RotateWidget implements Widget { constructor(props: RotateWidgetProps) { this.id = props.id ?? this.id; - this.placement = props.placement || this.placement; + this.placement = props.placement ?? this.placement; this.props = { ...props }; } @@ -62,6 +62,13 @@ class RotateWidget implements Widget { return this.props.ref.current; } + setProps(props: Partial) { + // Handle when props change here. + this.placement = props.placement ?? this.placement; + this.viewId = props.viewId ?? this.viewId; + this.props = {...props}; + } + onViewportChange(viewport) { this.viewports[viewport.id] = viewport; } diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index e9446111372..f932fffc4b1 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -287,7 +287,7 @@ interface LayerLoadingWidgetProps { className?: string; } -class LayerListWidget implements Widget { +class LayerLoadingWidget implements Widget { id = 'layer-loading-widget'; props: LayerLoadingWidgetProps; placement: WidgetPlacement = 'top-left'; @@ -318,8 +318,11 @@ class LayerListWidget implements Widget { } setProps(props: Partial) { + // Handle when props change here. this.placement = props.placement ?? this.placement; + this.viewId = props.viewId ?? this.viewId; this.props = {...props}; + this.update(); } onRedraw({layers}: {layers: Layer[]}) { @@ -327,7 +330,7 @@ class LayerListWidget implements Widget { this.update(); } - update() { + private update() { const element = this.element; if (!element) { return; From 5366ae26f67f6291f0788d3cf4f1158a0ceb1d31 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 2 Jan 2025 15:47:04 -0800 Subject: [PATCH 13/21] [docs] rewrite React widget dev guide --- docs/developer-guide/custom-widgets/README.md | 9 ++- .../custom-widgets/react-widgets.md | 59 +++++++++++++------ .../custom-widgets/universal-widgets.md | 3 +- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index d7b2e78ac57..90fdfd1eb38 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -2,12 +2,11 @@ ## Preparations -There are a many ways to build a widget in deck.gl, and it is helpful to consider what approach will serve you best before starting. We've provided guides for commonly used approaches: - -* **[Implement a universal widget](./universal-widgets.md)** - A universal widget is compatible with any deck.gl application and is UI framework agnostic. This option is best for developing widgets to be used throughout the deck.gl ecosystem. -* **[Use Preact in a universal widget](./preact-widgets.md)** - Preact is a lightweight virtual DOM commonly used to implement dynamic widgets without tightly coupling widget internals to an application's UI framework. -* **[Create a React widget](./react-widgets.md)** - A React widget utilizes the convenience of React to develop the UI for your widget. It is tightly coupled to your React application, being mounted in the same root as the rest of your UI. This option is best for developing widgets custom to your React application. +There are many ways to build a widget in deck.gl, and it’s helpful to consider which approach best suits your needs before getting started. Below are guides for commonly used approaches: +* **[Implement a universal widget](./universal-widgets.md)** - A "universal widget" is a widget compatible with any deck.gl application and is UI framework agnostic. This is the best option for developing widgets intended to work across the deck.gl ecosystem. +* **[Use Preact in a universal widget](./preact-widgets.md)** - Preact is a lightweight virtual DOM library commonly used to implement dynamic widget UI. It enables you to create highly interactive widgets without tightly coupling their internals to an application’s UI framework. +* **[Wrap widgets in a React component](./react-widgets.md)** - If you are developing a custom Widget for a React application, you can use React to build the UI. This approach allows you to use React components and can coexist alongside other widgets. ## Creating The Widget class diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index 53a12576415..ab4efda9cde 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -1,17 +1,17 @@ -# React Widgets +# Wrapping Widgets with React Components -React widgets are a powerful way to integrate custom UI elements into deck.gl applications using the [React](https://react.dev/) framework. This guide will walk you through the process of building React-based widgets and best practices. +Wrapping widgets in React components is a powerful way to render custom UI elements in deck.gl applications using the [React](https://react.dev/) UI framework. This guide will walk you through the process of rendering widget UI with React and best practices. -We recommend users writing their own React widgets be familiar with implementing the `Widget` interface, consider reviewing the [Universal Widgets](./universal-widgets.md) guide. +We recommend users writing their own widgets be familiar with implementing the `Widget` interface, consider reviewing the [Universal Widgets](./universal-widgets.md) guide. -## Why Use React Widgets? +## Why Use React with Widgets? -React widgets leverage the strengths of React’s component model, allowing: +Widget UI implemented with React leverage the strengths of React’s component model, allowing: - **Easy Composition:** Reuse and combine components within the React ecosystem. - **React Lifecycle Integration:** Utilize React’s lifecycle hooks to manage state and updates. - **Declarative UI:** Define your UI in a predictable and straightforward manner using JSX. -React widgets are most suitable when you are working on React applications and do not intend to distribute your widget outside of your application. +Using React to render the UI for widgets is most suitable when you are working on React applications and do not intend to distribute your widget outside of your application. ## Writing a React Widget @@ -25,9 +25,9 @@ Install the package if it’s not already included: npm install @deck.gl/react ``` -### Example: Creating a React Widget +### Example: Creating a Widget UI with React -Below is a step-by-step example of implementing a simple React widget. +Below is a step-by-step example of implementing a simple widget UI with React. #### Define Your Widget Class @@ -45,7 +45,7 @@ interface RotateWidgetProps { } class RotateWidget implements Widget { - id = 'bearing-widget'; + id = 'rotate-widget'; props: RotateWidgetProps; placement: WidgetPlacement = 'top-right'; viewports: {[id: string]: Viewport} = {}; @@ -66,7 +66,7 @@ class RotateWidget implements Widget { // Handle when props change here. this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; - this.props = {...props}; + Object.assign(this.props, props); } onViewportChange(viewport) { @@ -97,7 +97,7 @@ class RotateWidget implements Widget { Wrap the widget class in a React component using the [`useWidget`](../../api-reference/react/use-widget.md) hook. ```tsx -export const RotateReactWidget = (props: RotateWidgetProps) => { +export const Rotate = (props: RotateWidgetProps) => { const ref = useRef(); const widget = useWidget(RotateWidget, { ref, ...props }); @@ -116,31 +116,52 @@ export const RotateReactWidget = (props: RotateWidgetProps) => { This widget controls the bearing of the view its attached to. -#### Styling Your React Widget +#### Mount the Component as a Deck Child + +The `` component needs to be the root component of any react components wrapping a widget in order to provide a context and render the component within deck's view system. + +```tsx + + + +``` + +> Note: Currently, a widget component cannot be nested within a [JSX view](../../get-started/using-with-react.md#using-jsx-layers-and-views). Instead,set a [`viewId`](../../api-reference/core/widget.md#viewid) on the widget. + +```jsx + + + + + + +``` + +#### Styling Your React Component ##### Adding Inline Styles -Add the `style` prop in your React component for inline styling overrides. +A typical `style` prop in your React component could be used for inline styling overrides. ```tsx -export const RotateReactWidget = (props: RotateWidgetProps) => { +export const Rotate = (props: RotateWidgetProps & {style: CSSProperties}) => { ... return
    ...
    } ``` ```tsx - + ``` ##### Adding CSS Classes -Add `className` to your React component and styles to your stylesheet. +Alternatively, you could add a `className` to your React component along with styles to your stylesheet. ```tsx import 'style.css'; -export const RotateReactWidget = (props: RotateWidgetProps) => { +export const Rotate = (props: RotateWidgetProps) => { ... return
    ...
    } @@ -157,13 +178,13 @@ export const RotateReactWidget = (props: RotateWidgetProps) => { ##### Applying the deck.gl widget design system -Reuse the built-in deck.gl widget [stylesheet](https://unpkg.com/deck.gl@latest/dist/stylesheet.css) by importing them into your application. This can be useful if you're already theming deck.gl widgets and want to reuse CSS styles and variables. See [Widget Overview](../../api-reference/widgets/overview.md#custom-class-theming) for a full list of built-in variables. +Deck.gl ships a widget stylesheet as well. You can use this built-in [stylesheet](https://unpkg.com/deck.gl@latest/dist/stylesheet.css) by importing them into your application. This can be useful if you're already theming deck.gl widgets and want to reuse CSS styles and variables. See [Widget Overview](../../api-reference/widgets/overview.md#custom-class-theming) for a full list of built-in variables. ```tsx import '@deck.gl/widgets/stylesheet.css'; import 'style.css'; -export const RotateReactWidget = (props: RotateWidgetProps) => { +export const Rotate = (props: RotateWidgetProps) => { const ref = useRef(); const widget = useWidget(RotateWidget, { ref, ...props }); return ( diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index f932fffc4b1..84972c47415 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -231,6 +231,7 @@ class AwesomeWidget implements Widget { } } ... + Object.assign(this.props, props); } } ``` @@ -321,7 +322,7 @@ class LayerLoadingWidget implements Widget { // Handle when props change here. this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; - this.props = {...props}; + Object.assign(this.props, props); this.update(); } From a45ba17132160b6017bcd5aa04e916075654cf06 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sun, 5 Jan 2025 15:16:33 -0800 Subject: [PATCH 14/21] use type instead of interface --- docs/developer-guide/custom-widgets/README.md | 2 +- .../custom-widgets/preact-widgets.md | 4 ++-- .../developer-guide/custom-widgets/react-widgets.md | 10 +++++----- .../custom-widgets/universal-widgets.md | 13 ++++++++++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index 90fdfd1eb38..ce4d8c6cae5 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -40,7 +40,7 @@ You also need to define the default values of the widget's properties. ```ts import type {WidgetPlacement} from '@deck.gl/core' -interface AwesomeWidgetProps { +type AwesomeWidgetProps = { id?: string; /** * Widget positioning within the view. Default: 'top-left'. diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index 65020f5f2b7..f7db8da4e17 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -14,7 +14,7 @@ Preact widgets are suitable when you are working with any UI framework and is li > Tip: Read more about the differences between Preact and React [here](https://preactjs.com/guide/v10/differences-to-react/). -## Writing a React Widget +## Writing a Preact Widget ### Prerequisites @@ -52,7 +52,7 @@ import type { } from '@deck.gl/core' import {render} from 'preact'; -interface LayerListWidgetProps { +type LayerListWidgetProps = { id?: string; /** * Widget positioning within the view. Default: 'top-left'. diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index ab4efda9cde..b0e362e14f9 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -38,10 +38,10 @@ import type { Widget, WidgetPlacement } from '@deck.gl/core'; import { useWidget } from '@deck.gl/react'; import React, { useRef, RefObject } from 'react'; -interface RotateWidgetProps { - id?: string - placement?: WidgetPlacement - ref: RefObject +type RotateWidgetProps = { + id?: string + placement?: WidgetPlacement + ref: RefObject } class RotateWidget implements Widget { @@ -62,7 +62,7 @@ class RotateWidget implements Widget { return this.props.ref.current; } - setProps(props: Partial) { + setProps(props: Partial) { // Handle when props change here. this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; diff --git a/docs/developer-guide/custom-widgets/universal-widgets.md b/docs/developer-guide/custom-widgets/universal-widgets.md index 84972c47415..9747aac2147 100644 --- a/docs/developer-guide/custom-widgets/universal-widgets.md +++ b/docs/developer-guide/custom-widgets/universal-widgets.md @@ -13,6 +13,13 @@ The widget lifecycle functions define how a widget initializes, updates, and cle ```ts import type { Widget, WidgetPlacement } from '@deck.gl/core' +type AwesomeWidgetProps = { + id?: string; + placement?: WidgetPlacement; + viewId?: string | null; + style?: Partial; +} + class AwesomeWidget implements Widget { id = 'awesome-widget'; props: AwesomeWidgetProps; @@ -148,7 +155,7 @@ A good universal widget provides users with ways to customize the styles of any Define a prop for overriding styles inline ```ts -interface AwesomeWidgetProps { +type AwesomeWidgetProps = { style?: Partial; } ``` @@ -199,7 +206,7 @@ class AwesomeWidget implements Widget { Define a prop for adding a CSS class ```ts -interface AwesomeWidgetProps { +type AwesomeWidgetProps = { /** * Additional CSS class. */ @@ -268,7 +275,7 @@ import type { Deck, Viewport, Widget, WidgetPlacement, Layer } from '@deck.gl/core' -interface LayerLoadingWidgetProps { +type LayerLoadingWidgetProps = { id?: string; /** * Widget positioning within the view. Default: 'top-left'. From 225cd33ef64eb05d2b931e1ef77f5d63ae4b3f8a Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Mon, 6 Jan 2025 12:14:51 -0800 Subject: [PATCH 15/21] Update preact-widgets.md --- .../custom-widgets/preact-widgets.md | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index f7db8da4e17..21b480f384f 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -76,6 +76,7 @@ class LayerListWidget implements Widget { id = 'layer-list-widget'; props: LayerListWidgetProps; placement: WidgetPlacement = 'top-left'; + viewId?: string | null = null; viewports: {[id: string]: Viewport} = {}; layers: Layer[] = []; deck?: Deck; @@ -105,10 +106,23 @@ class LayerListWidget implements Widget { } setProps(props: Partial) { - // Handle when props change here. + const oldProps = this.props; + const el = this.element; + // Handle when CSS changes. + if (el) { + if (oldProps.className !== props.className) { + if (oldProps.className) el.classList.remove(oldProps.className); + if (props.className) el.classList.add(props.className); + } + if (!deepEqual(oldProps.style, props.style, 1)) { + removeStyles(el, oldProps.style); + applyStyles(el, props.style); + } + } + // Handle when props change. this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; - this.props = {...props}; + Object.assign(this.props, props); this.update(); } @@ -129,18 +143,20 @@ class LayerListWidget implements Widget { let layers = this.layers if (this.deck?.props.layerFilter) { const ui = ( - {this.viewports.values().map(viewport => ( -
    - {viewport.id} -
      - {layers.filter(layer => ( - this.deck?.props.layerFilter({layer, viewport}) - )).map((layer) => { -
    • {layer.id}
    • - })} -
    -
    - ))} + <> + {Object.values(this.viewports).map(viewport => ( +
    + {viewport.id} +
      + {layers.filter(layer => ( + this.deck?.props.layerFilter({layer, viewport}) + )).map((layer) => { +
    • {layer.id}
    • + })} +
    +
    + ))} + ); render(ui, element); } else { From 2548071fdf69f8460d124b2cb75e228459bb8a17 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Mon, 6 Jan 2025 21:42:06 -0800 Subject: [PATCH 16/21] Fix example in preact-widgets.md --- .../custom-widgets/preact-widgets.md | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index 21b480f384f..e71edea1d8b 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -70,7 +70,7 @@ type LayerListWidgetProps = { * Additional CSS class. */ className?: string; -} +}; class LayerListWidget implements Widget { id = 'layer-list-widget'; @@ -87,10 +87,10 @@ class LayerListWidget implements Widget { this.placement = props.placement ?? this.placement; this.viewId = props.viewId ?? this.viewId; - this.props = { + this.props = { ...props, style: props.style ?? {} - } + }; } onAdd({deck}: {deck: Deck}): HTMLDivElement { @@ -132,7 +132,7 @@ class LayerListWidget implements Widget { } onViewportChange(viewport) { - this.viewports[viewport.id] = viewport + this.viewports[viewport.id] = viewport; } private update() { @@ -140,33 +140,44 @@ class LayerListWidget implements Widget { if (!element) { return; } - let layers = this.layers + const layers = this.layers; if (this.deck?.props.layerFilter) { const ui = ( <> {Object.values(this.viewports).map(viewport => ( -
    - {viewport.id} + <> +

    Layers in {viewport.id}

      - {layers.filter(layer => ( - this.deck?.props.layerFilter({layer, viewport}) - )).map((layer) => { -
    • {layer.id}
    • - })} + {layers + .filter(layer => + this.deck?.props.layerFilter?.({ + layer, + viewport, + isPicking: false, + renderPass: 'widget' + }) + ) + .map(layer => ( +
    • {layer.id}
    • + ))}
    -
    + ))} ); render(ui, element); } else { + const viewportNames = Object.keys(this.viewports).join(', '); const ui = ( -
      - {this.layers.map((layer) => ( -
    • {layer.id}
    • - ))} -
    - ) + <> +

    Layers in {viewportNames} view

    +
      + {this.layers.map(layer => ( +
    • {layer.id}
    • + ))} +
    + + ); render(ui, element); } } From 90e62a8a15ab8a585248cccb6147afb0ef9caa90 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 7 Jan 2025 20:37:43 -0800 Subject: [PATCH 17/21] After testing react-widgets.md --- .../custom-widgets/react-widgets.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index b0e362e14f9..76726e6c37d 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -39,25 +39,28 @@ import { useWidget } from '@deck.gl/react'; import React, { useRef, RefObject } from 'react'; type RotateWidgetProps = { - id?: string - placement?: WidgetPlacement - ref: RefObject -} + id?: string; + placement?: WidgetPlacement; + viewId?: string | null; + ref: RefObject; +}; class RotateWidget implements Widget { id = 'rotate-widget'; props: RotateWidgetProps; placement: WidgetPlacement = 'top-right'; + viewId?: string | null = null; viewports: {[id: string]: Viewport} = {}; deck?: Deck; constructor(props: RotateWidgetProps) { this.id = props.id ?? this.id; this.placement = props.placement ?? this.placement; - this.props = { ...props }; + this.viewId = props.viewId ?? this.viewId; + this.props = {...props}; } - onAdd({ deck }) { + onAdd({deck}) { this.deck = deck; return this.props.ref.current; } @@ -79,7 +82,7 @@ class RotateWidget implements Widget { ...viewport, bearing: nextBearing }; - this.deck.setProps({ viewState }); + this.deck?.setProps({viewState}); } handleCWRotate() { From afba3b8d9e4eb999a575f5e590133c6d618c258b Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 7 Jan 2025 21:17:58 -0800 Subject: [PATCH 18/21] Update react-widgets.md --- .../custom-widgets/react-widgets.md | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index 76726e6c37d..d453bd29e4d 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -34,8 +34,9 @@ Below is a step-by-step example of implementing a simple widget UI with React. Start by creating the core widget class, which must implement the [Widget](../../api-reference/core/widget.md) interface. ```ts -import type { Widget, WidgetPlacement } from '@deck.gl/core'; -import { useWidget } from '@deck.gl/react'; +import { WebMercatorViewport } from '@deck.gl/core'; +import type { Widget, WidgetPlacement, Viewport } from '@deck.gl/core'; +import DeckGL, { useWidget } from '@deck.gl/react'; import React, { useRef, RefObject } from 'react'; type RotateWidgetProps = { @@ -50,7 +51,7 @@ class RotateWidget implements Widget { props: RotateWidgetProps; placement: WidgetPlacement = 'top-right'; viewId?: string | null = null; - viewports: {[id: string]: Viewport} = {}; + viewports: {[id: string]: WebMercatorViewport} = {}; deck?: Deck; constructor(props: RotateWidgetProps) { @@ -72,17 +73,19 @@ class RotateWidget implements Widget { Object.assign(this.props, props); } - onViewportChange(viewport) { - this.viewports[viewport.id] = viewport; + onViewportChange(viewport: Viewport) { + if (viewport instanceof WebMercatorViewport) { + this.viewports[viewport.id] = viewport; + } } - handleRotate(viewport, bearingDelta) { + handleRotate(viewport: WebMercatorViewport, bearingDelta: number) { const nextBearing = viewport.bearing + bearingDelta; - const viewState = { + const initialViewState = { ...viewport, bearing: nextBearing }; - this.deck?.setProps({viewState}); + this.deck?.setProps({initialViewState}); } handleCWRotate() { @@ -100,12 +103,12 @@ class RotateWidget implements Widget { Wrap the widget class in a React component using the [`useWidget`](../../api-reference/react/use-widget.md) hook. ```tsx -export const Rotate = (props: RotateWidgetProps) => { - const ref = useRef(); - const widget = useWidget(RotateWidget, { ref, ...props }); +const Rotate = (props: RotateWidgetProps) => { + const ref = useRef(null); + const widget = useWidget(RotateWidget, { ...props, ref }); return ( -
    +
    From 5152e496b2afd95fd0f958867b9e69632f6b8cbf Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 7 Jan 2025 21:38:18 -0800 Subject: [PATCH 19/21] Use a portal instead of a ref Mutating the dom via ref is a react anti-pattern --- .../custom-widgets/react-widgets.md | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/developer-guide/custom-widgets/react-widgets.md b/docs/developer-guide/custom-widgets/react-widgets.md index d453bd29e4d..1058bfcacdf 100644 --- a/docs/developer-guide/custom-widgets/react-widgets.md +++ b/docs/developer-guide/custom-widgets/react-widgets.md @@ -34,16 +34,14 @@ Below is a step-by-step example of implementing a simple widget UI with React. Start by creating the core widget class, which must implement the [Widget](../../api-reference/core/widget.md) interface. ```ts -import { WebMercatorViewport } from '@deck.gl/core'; -import type { Widget, WidgetPlacement, Viewport } from '@deck.gl/core'; -import DeckGL, { useWidget } from '@deck.gl/react'; -import React, { useRef, RefObject } from 'react'; +import {WebMercatorViewport} from '@deck.gl/core'; +import type {Widget, WidgetPlacement, Viewport} from '@deck.gl/core'; type RotateWidgetProps = { id?: string; placement?: WidgetPlacement; viewId?: string | null; - ref: RefObject; + element: HTMLDivElement; }; class RotateWidget implements Widget { @@ -63,7 +61,7 @@ class RotateWidget implements Widget { onAdd({deck}) { this.deck = deck; - return this.props.ref.current; + return this.props.element; } setProps(props: Partial) { @@ -103,19 +101,22 @@ class RotateWidget implements Widget { Wrap the widget class in a React component using the [`useWidget`](../../api-reference/react/use-widget.md) hook. ```tsx +import React, {useMemo} from 'react'; +import {createPortal} from 'react-dom'; +import DeckGL, {useWidget} from '@deck.gl/react'; + const Rotate = (props: RotateWidgetProps) => { - const ref = useRef(null); - const widget = useWidget(RotateWidget, { ...props, ref }); + const element = useMemo(() => document.createElement('div'), []); + const widget = useWidget(RotateWidget, {...props, element}); - return ( -
    - - -
    + +
    , + element ); }; ``` @@ -124,7 +125,7 @@ This widget controls the bearing of the view its attached to. #### Mount the Component as a Deck Child -The `` component needs to be the root component of any react components wrapping a widget in order to provide a context and render the component within deck's view system. +The [``](../../api-reference/react/deckgl.md) component needs to be the root component of any react components wrapping a widget in order to provide a context and render the component within deck's view system. ```tsx @@ -150,10 +151,10 @@ The `` component needs to be the root component of any react components A typical `style` prop in your React component could be used for inline styling overrides. ```tsx -export const Rotate = (props: RotateWidgetProps & {style: CSSProperties}) => { +const Rotate = (props: RotateWidgetProps & {style: CSSProperties}) => { ... return
    ...
    -} +}; ``` ```tsx @@ -167,10 +168,10 @@ Alternatively, you could add a `className` to your React component along with st ```tsx import 'style.css'; -export const Rotate = (props: RotateWidgetProps) => { +const Rotate = (props: RotateWidgetProps) => { ... return
    ...
    -} +}; ``` ```css @@ -190,19 +191,20 @@ Deck.gl ships a widget stylesheet as well. You can use this built-in [stylesheet import '@deck.gl/widgets/stylesheet.css'; import 'style.css'; -export const Rotate = (props: RotateWidgetProps) => { - const ref = useRef(); - const widget = useWidget(RotateWidget, { ref, ...props }); - return ( -
    +const Rotate = (props: RotateWidgetProps) => { + const element = useMemo(() => document.createElement('div'), []); + const widget = useWidget(RotateWidget, {...props, element}); + return createPortal( +
    -
    - ) -} +
    , + element + ); +}; ``` ```css From 139234501905f83a305935f24181303ea04d395f Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 10 Jan 2025 13:21:04 -0800 Subject: [PATCH 20/21] Update preact-widgets.md Co-authored-by: Ib Green <7025232+ibgreen@users.noreply.github.com> --- docs/developer-guide/custom-widgets/preact-widgets.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/developer-guide/custom-widgets/preact-widgets.md b/docs/developer-guide/custom-widgets/preact-widgets.md index e71edea1d8b..b04e76fc358 100644 --- a/docs/developer-guide/custom-widgets/preact-widgets.md +++ b/docs/developer-guide/custom-widgets/preact-widgets.md @@ -1,6 +1,8 @@ # Preact Widgets -Preact widgets are an easy way to add dynamic UI elements into universal deck.gl widgets using the [Preact](https://preactjs.com/) UI library. This guide will walk you through the process of building Preact-based widgets and best practices. +Normally we want to to create a reusable, universal widget that can work with any JavaScript UI framework, we would implement it using the "raw" HTML DOM APIs. While this is the canonical approach, these APIs are quite verbose. +Instead, the core widgets provided by deck.gl widgets are internally using the [Preact](https://preactjs.com/) UI library, which lets us develop widgets with the clarity of JSX and react style code, while remaining completely framework agnostic in their external APIs. +This guide will walk you through the process of using Preact to implement universal widgets and best practices. ## Why Use Preact Widgets? From 06ef6666cc07f1f48f0debb384a977ac86d50394 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Wed, 15 Jan 2025 17:12:29 -0800 Subject: [PATCH 21/21] Update README.md Co-authored-by: Ib Green <7025232+ibgreen@users.noreply.github.com> --- docs/developer-guide/custom-widgets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/custom-widgets/README.md b/docs/developer-guide/custom-widgets/README.md index ce4d8c6cae5..4933bfbc1a3 100644 --- a/docs/developer-guide/custom-widgets/README.md +++ b/docs/developer-guide/custom-widgets/README.md @@ -8,7 +8,7 @@ There are many ways to build a widget in deck.gl, and it’s helpful to consider * **[Use Preact in a universal widget](./preact-widgets.md)** - Preact is a lightweight virtual DOM library commonly used to implement dynamic widget UI. It enables you to create highly interactive widgets without tightly coupling their internals to an application’s UI framework. * **[Wrap widgets in a React component](./react-widgets.md)** - If you are developing a custom Widget for a React application, you can use React to build the UI. This approach allows you to use React components and can coexist alongside other widgets. -## Creating The Widget class +## Creating a new Widget Your widget class must implement the [Widget](../../api-reference/core/widget.md) interface.