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 13 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -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

Expand Down
78 changes: 78 additions & 0 deletions docs/developer-guide/custom-widgets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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.
* **[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

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'

interface AwesomeWidgetProps {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Recommend with the arguably more modern approach to use the type keyword here. I personally prefer reserving interface for types that are to be derived from with methods that need to be implemented.

Suggested change
interface AwesomeWidgetProps {
export type AwesomeWidgetProps = {

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'd want to do this in a separate PR and update everything in one sweep.

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.
160 changes: 160 additions & 0 deletions docs/developer-guide/custom-widgets/preact-widgets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# 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<CSSStyleDeclaration>;
/**
* Additional CSS class.
*/
className?: string;
}

class LayerListWidget implements Widget<LayerListWidgetProps> {
id = 'layer-list-widget';
props: LayerListWidgetProps;
placement: WidgetPlacement = 'top-left';
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>) {
// 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();
}

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

private update() {
const element = this.element;
if (!element) {
return;
}
let layers = this.layers
if (this.deck?.props.layerFilter) {
const ui = (
{this.viewports.values().map(viewport => (
<div>
{viewport.id}
<ul>
{layers.filter(layer => (
this.deck?.props.layerFilter({layer, viewport})
)).map((layer) => {
<li key={layer.id}>{layer.id}</li>
})}
</ul>
</div>
))}
);
render(ui, element);
} else {
const ui = (
<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