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

feat: limit view fields exposed to render function #138

Merged
merged 6 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/unlucky-moose-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"anywidget": minor
"@anywidget/types": minor
---

feat!: Limit view fields exposed to render function

BREAKING: The render function's argument has been refactored from a full `AnyView` to a simple object. This object only exposes the `model` and `el` fields to the user-provided `render` function. This change aims to simplify the API and reduce potential misuse. Please ensure your render function only depends on these fields.
8 changes: 4 additions & 4 deletions docs/src/pages/blog/introducing-anywidget.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,8 @@ ExampleWidget()
updates for the widget.

```javascript
/** @param {import("@jupyter-widgets/base").DOMWidgetView} */
export function render(view) {
/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
export function render({ model, el }) {
// Render model contents and setup dynamic updates
}
```
Expand Down Expand Up @@ -556,8 +556,8 @@ pip install anywidget
```

**anywidget** is new and still under active development. **It should _not_
yet be used in production since the API can change and some critical features are missing**
(e.g., APIs to extend the `DOMWidgetModel` with custom serializers and `DOMWidgetView` with lifecycle hooks). With that said, it is already [in use](https://github.com/vitessce/vitessce-python) and ready for testing.
yet be used in production since the API can change and some critical features are missing**.
With that said, it is already [in use](https://github.com/vitessce/vitessce-python) and ready for testing.

I hope using **anywidget** is simple and enjoyable. I have personally found
it valuable in my work as a visualization researcher to quickly iterate on new
Expand Down
14 changes: 7 additions & 7 deletions docs/src/pages/en/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ What's going on here:
It must _must_ export a `render` custom rendering logic and initializes dynamic updates for the custom widget.

```javascript
/** @param view {import("@jupyter-widgets/base").DOMWidgetView} */
export function render(view) {
/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
export function render({ model, el }) {
// Render model contents and setup dynamic updates
// See Jupyter widgets docs for more information: https://ipywidgets.readthedocs.io/en/8.0.2/examples/Widget%20Custom.html#Rendering-model-contents
}
Expand All @@ -92,9 +92,9 @@ Therefore, dependencies can be imported directly via a fully qualified URL.
```javascript
import * as d3 from "https://esm.sh/d3@7";

/** @param view {import("@jupyter-widgets/base").DOMWidgetView} */
export function render(view) {
let selection = d3.select(view.el);
/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
export function render({ model, el }) {
let selection = d3.select(el);
/* ... */
}
```
Expand All @@ -104,8 +104,8 @@ removed from the DOM. This feature is useful when dealing with complex event lis
or third-party libraries that require proper teardwon.

```javascript
/** @param view {import("@jupyter-widgets/base").DOMWidgetView} */
export function render(view) {
/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
export function render({ model, el }) {
// Create DOM elements and set up subscribers
return () => {
// Optionally cleanup
Expand Down
30 changes: 14 additions & 16 deletions docs/src/pages/en/jupyter-widgets-the-good-parts.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ that removes boilerplate and packaging details.
### Comparison with traditional Jupyter Widgets

**anywidget** simplies creating your widget's front-end code. Its only requirement
is that your widget front-end code is a valid [JavaScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and exports a function
called `render`. This `render` function just an alias for the traditional
[`DOMWidgetView.render`](https://ipywidgets.readthedocs.io/en/8.0.2/examples/Widget%20Custom.html#Render-method) method,
except that your Widget's view is passed as the first argument.
is that your widget front-end code is a valid [JavaScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
and exports a function called `render`. This `render` function is similar to the traditional
[`DOMWidgetView.render`](https://ipywidgets.readthedocs.io/en/8.0.2/examples/Widget%20Custom.html#Render-method).

Concretely, custom widgets are traditionally defined like:

Expand All @@ -49,29 +48,28 @@ export { CustomModel, CustomView };

... which must be transformed, bundled, and installed in multiple notebook environments.

In **anywidget**, the above code simplies to just:
In **anywidget**, the above code simplies to:

```javascript
/** @param view {DOMWidgetView} view */
export function render(view) {
let el = view.el;
let model = view.model;
/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
export function render(context) {
let el = context.el;
let model = context.model;
/* ... */
}
```

... which explicity defines the widget view (i.e., `CustomView`) via the `render`
... which explicity defines the widget view via the `render`
function, and (implicitly) **anywidget** defines the associated widget
model (i.e., `CustomModel`). **anywidget** front-end code is often so
minimal that it can easily be inlined as a Python string:

```python
class CustomWidget(anywidget.AnyWidget):
_esm = """
/** @param {DOMWidgetView} view */
export function render(view) {
let el = view.el;
let model = view.model;
export function render(context) {
let el = context.el;
let model = context.model;
/* ... */
}
"""
Expand All @@ -83,9 +81,9 @@ Just like `DOMWidgetView.render`, your widget's `render` function
is executed exactly **one per output cell** that displays the widget instance.
Therefore, `render` primarily serves two purposes:

1. Initializing content to display (i.e., create and append element(s) to `view.el`)
1. Initializing content to display (i.e., create and append element(s) to `context.el`)
2. Registering event handlers to update or display model state any time it changes
(i.e., passing callbacks to `view.model.on`)
(i.e., passing callbacks to `context.model.on`)

## Connecting JavaScript with Python

Expand Down
4 changes: 2 additions & 2 deletions packages/anywidget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"build": "node build.mjs"
},
"dependencies": {
"@jupyter-widgets/base": "^2 || ^3 || ^4 || ^5 || ^6",
"@anywidget/vite": "workspace:*",
"@anywidget/types": "workspace:*",
"@anywidget/vite": "workspace:*"
"@jupyter-widgets/base": "^2 || ^3 || ^4 || ^5 || ^6"
},
"devDependencies": {
"@jupyterlab/builder": "^3.6.2"
Expand Down
21 changes: 10 additions & 11 deletions packages/anywidget/src/widget.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// @ts-check
import { name, version } from "../package.json";

/** @typedef {import("@jupyter-widgets/base").WidgetView} WidgetView */

/**
* @typedef AnyWidgetModule
* @prop render {(view: WidgetView) => Promise<undefined | (() => Promise<void>)>}
* @prop render {import("@anywidget/types").Render}
*/

/**
Expand Down Expand Up @@ -101,8 +99,8 @@ async function load_esm(esm) {
}

/** @param {typeof import("@jupyter-widgets/base")} base */
export default function (base) {
class AnyModel extends base.DOMWidgetModel {
export default function ({ DOMWidgetModel, DOMWidgetView }) {
class AnyModel extends DOMWidgetModel {
static model_name = "AnyModel";
static model_module = name;
static model_module_version = version;
Expand All @@ -111,7 +109,7 @@ export default function (base) {
static view_module = name;
static view_module_version = version;

/** @param {Parameters<InstanceType<base["DOMWidgetModel"]>["initialize"]>} args */
/** @param {Parameters<InstanceType<DOMWidgetModel>["initialize"]>} args */
initialize(...args) {
super.initialize(...args);

Expand All @@ -133,10 +131,11 @@ export default function (base) {
if (!id) return;
console.debug(`[anywidget] esm hot updated: ${id}`);

let views = (/** @type {Promise<AnyView>[]} */ (Object.values(this.views ?? {})));
let views =
/** @type {Promise<AnyView>[]} */ (/** @type {unknown} */ (Object
.values(this.views ?? {})));

for await (let view of views) {

// load updated esm
let widget = await load_esm(this.get("_esm"));

Expand All @@ -160,18 +159,18 @@ export default function (base) {
view.stopListening(this);

// render the view with the updated render
let cleanup = await widget.render(view);
let cleanup = await widget.render({ model: this, el: view.el });
view._anywidget_cached_cleanup = cleanup ?? (() => {});
}
});
}
}

class AnyView extends base.DOMWidgetView {
class AnyView extends DOMWidgetView {
async render() {
await load_css(this.model.get("_css"), this.model.get("_anywidget_id"));
let widget = await load_esm(this.model.get("_esm"));
let cleanup = await widget.render(this);
let cleanup = await widget.render({ model: this.model, el: this.el });
this._anywidget_cached_cleanup = cleanup ?? (() => {});
}

Expand Down
6 changes: 3 additions & 3 deletions packages/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export interface AnyModel<T extends ObjectHash = ObjectHash>
extends DOMWidgetModel {
get<K extends keyof T>(key: K): T[K];
set<K extends keyof T>(key: K, value: T[K]): void;
views: Record<string, Promise<AnyView<AnyModel<T>>>>;
}

export interface AnyView<Model extends DOMWidgetModel> extends DOMWidgetView {
export interface RenderContext<Model> {
model: Model;
el: HTMLElement;
}

export interface Render<T extends ObjectHash = ObjectHash> {
(view: AnyView<AnyModel<T>>): MaybePromise<void | CleanupFn>;
(context: RenderContext<AnyModel<T>>): MaybePromise<void | CleanupFn>;
}