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

docs(widgets) Custom Widget Developer Guide #9304

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
101aaf9
docs(widgets) Widget Developer Guide
chrisgervang Dec 18, 2024
ebd2b50
Update docs/developer-guide/custom-widgets/README.md
chrisgervang Dec 18, 2024
70248ce
Update docs/developer-guide/custom-widgets/universal-widgets.md
chrisgervang Dec 18, 2024
026fd27
Update docs/developer-guide/custom-widgets/README.md
chrisgervang Dec 18, 2024
41cdfb3
adding preact page and vanilla example
chrisgervang Dec 18, 2024
077ddd9
onViewportChange
chrisgervang Dec 18, 2024
775c7b8
misc react docs
chrisgervang Dec 18, 2024
e68bd41
Styling Your React Widget
chrisgervang Dec 18, 2024
5291a26
TOC
chrisgervang Dec 18, 2024
dcb07be
type cleanup
chrisgervang Dec 18, 2024
8daa0c6
Merge branch 'master' into chr/widget-dev-guide
chrisgervang Dec 18, 2024
e48c1c9
Adding required class members
chrisgervang Dec 19, 2024
98a82a4
Add reactivity to examples
chrisgervang Dec 21, 2024
5366ae2
[docs] rewrite React widget dev guide
chrisgervang Jan 2, 2025
f17de11
Merge branch 'master' into chr/widget-dev-guide
chrisgervang Jan 4, 2025
a45ba17
use type instead of interface
chrisgervang Jan 5, 2025
225cd33
Update preact-widgets.md
chrisgervang Jan 6, 2025
2548071
Fix example in preact-widgets.md
chrisgervang Jan 7, 2025
9a1ffe5
Merge branch 'master' into chr/widget-dev-guide
chrisgervang Jan 7, 2025
90e62a8
After testing react-widgets.md
chrisgervang Jan 8, 2025
afba3b8
Update react-widgets.md
chrisgervang Jan 8, 2025
5152e49
Use a portal instead of a ref
chrisgervang Jan 8, 2025
1392345
Update preact-widgets.md
chrisgervang Jan 10, 2025
06ef666
Update README.md
chrisgervang Jan 16, 2025
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
1 change: 1 addition & 0 deletions docs/api-reference/core/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class LoadingIndicator implements Widget {
constructor(options: {
size: number;
}) {
this.id = 'loading-indicator'
this.size = options.size;
}

Expand Down
77 changes: 77 additions & 0 deletions docs/developer-guide/custom-widgets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Writing Your Own Widget

## Preparations
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
## Preparations
## Preliminaries


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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

These bullets do not fully explain the situation the way I see it.

It seems to me that there are two types of widgets - "universal" and "react".

Suggested change
* **[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.
- universal widgets can be 1) used in React apps 2) wrapped into React widgets, but the reverse is not true.
For React specifically, you have several options:
- You can use the universal widgets by supplying them to the `<DeckGL widgets={[new Widget()]} />` prop.
- You can wrap universal components into React components using the `useWidget()` hook. This way you can specify your components using JSX syntax..
- If you are creating new components, can also create your own React components that implement the DeckGL widget interface. The advantage is that you write your code in the same style as the rest of your React application, however, these components would not be reusable in non-React applications.```

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point. Even calling it "react" doesn't really say what this is.. it's fine, but people could use other frameworks, in theory.

Really, they're making the choice between implementing UI within the widget class, or outside of it.

Do any of these terms pair better? "universal", "internal", or "portable" for UI within the widget.

"react", "external", or "BYOF" (bring your own framework) for UI injected into the widget class.


## Creating a new Widget

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 {
id = 'awesome-widget';
props;
constructor(props) {
this.id = props.id ?? this.id;
this.props = { ...props };
}
onAdd() {...}
onRemove() {...}
}
```

It's most convenient to use TypeScript, but widgets 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'

type AwesomeWidgetProps = {
id?: string;
/**
* Widget positioning within the view. Default: 'top-left'.
*/
placement?: WidgetPlacement;
Copy link
Collaborator

Choose a reason for hiding this comment

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

consider exporting a set of base WidgetProps so that widget writers don't need to retype all of that?

Suggested change
placement?: WidgetPlacement;
export type WidgetProps = {
id?: string;
placement?: WidgetPlacement;
viewId?: string | null;
};
...
export type AwesomeWidgetProps = WidgetProps & {
customText: string;
}

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 like the idea to encourage consistency, though it's still 100% up to the widget authors to decide how they implement this since we're only defining an interface.

/**
* View to attach to and interact with. Required when using multiple views. Default: null
*/
viewId?: string | null;
...
}

class AwesomeWidget implements Widget<AwesomeWidgetProps> {
id = 'awesome-widget';
props: AwesomeWidgetProps;
placement: WidgetPlacement = 'top-left';
viewId?: string | null = null;

constructor(props: AwesomeWidgetProps) {
this.id = props.id ?? this.id;
this.placement = props.placement ?? this.placement;
this.viewId = props.viewId ?? this.viewId;

this.props = { ...props }
}
}
```

## Best Practices
Copy link
Collaborator

Choose a reason for hiding this comment

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

This section looks a little "lost" here at the very end of the page. Maybe lead with something like this before all the examples?. Or maybe the section will grow and it will look more natural.


- **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.
189 changes: 189 additions & 0 deletions docs/developer-guide/custom-widgets/preact-widgets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Preact Widgets

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?

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 Preact 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';

type 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<CSSStyleDeclaration>;
/**
* Additional CSS class.
*/
className?: string;
};

class LayerListWidget implements Widget<LayerListWidgetProps> {
id = 'layer-list-widget';
props: LayerListWidgetProps;
placement: WidgetPlacement = 'top-left';
viewId?: string | null = null;
viewports: {[id: string]: Viewport} = {};
layers: Layer[] = [];
deck?: Deck<any>;
element?: HTMLDivElement;

constructor(props: LayerListWidgetProps) {
this.id = props.id ?? this.id;
this.placement = props.placement ?? this.placement;
this.viewId = props.viewId ?? this.viewId;

this.props = {
...props,
style: props.style ?? {}
};
}

onAdd({deck}: {deck: Deck<any>}): 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;
}

setProps(props: Partial<LayerListWidgetProps>) {
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;
Object.assign(this.props, props);
this.update();
}

onRedraw({layers}: {layers: Layer[]}) {
this.layers = layers;
this.update();
}
Comment on lines +131 to +134
Copy link
Collaborator Author

@chrisgervang chrisgervang Jan 17, 2025

Choose a reason for hiding this comment

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

@Pessimistress one thing I noticed is this layers arg includes all sub-layers in a flattened list. While that could be useful sometime, I imagine most widgets would want to display state from the top-level layers. Is there a way to access this aside from deck.props.layers?


onViewportChange(viewport) {
this.viewports[viewport.id] = viewport;
}

private update() {
const element = this.element;
if (!element) {
return;
}
const layers = this.layers;
if (this.deck?.props.layerFilter) {
const ui = (
<>
{Object.values(this.viewports).map(viewport => (
<>
<h4>Layers in {viewport.id}</h4>
<ul>
{layers
.filter(layer =>
this.deck?.props.layerFilter?.({
layer,
viewport,
isPicking: false,
renderPass: 'widget'
})
)
.map(layer => (
<li key={layer.id}>{layer.id}</li>
))}
</ul>
</>
))}
</>
);
render(ui, element);
} else {
const viewportNames = Object.keys(this.viewports).join(', ');
const ui = (
<>
<h4>Layers in {viewportNames} view</h4>
<ul>
{this.layers.map(layer => (
<li key={layer.id}>{layer.id}</li>
))}
</ul>
</>
);
render(ui, element);
}
}
}
```

This widget dynamically renders a list of layers and updates as the deck.gl state changes.
Loading
Loading