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: Add AFM section #672

Merged
merged 7 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions docs/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export const SIDEBAR: Sidebar = {
{ text: "Community", link: "en/community" },
],
Learn: [
{
text: "Anywidget Front-End Module",
link: "en/afm",
},
{
text: "Jupyter Widgets: The Good Parts",
link: "en/jupyter-widgets-the-good-parts",
Expand Down
228 changes: 228 additions & 0 deletions docs/src/pages/afm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
---
title: "Anywidget Front-End Module (AFM)"
description: "A specification for portable widgets based on ECMAScript modules."
layout: ../../layouts/MainLayout.astro
---

## What is AFM?

The **Anywidget Front-End Module (AFM)** specification defines a standard for
creating portable widget front-end code. Our vision is to enable widget reuse
**beyond Jupyter**, including other computational notebooks and standalone web
manzt marked this conversation as resolved.
Show resolved Hide resolved
applications. AFM is oriented around a minimal set of APIs we identified as
essential for integration with _host platforms_, boiling down to:
manzt marked this conversation as resolved.
Show resolved Hide resolved

- Bidirectional communication with a host (e.g., Jupyter)
- Modifying output areas (DOM manipulation) (e.g., a notebook output cell)

## Core Concepts

### Front-End Module

The Anywidget Front-end Module is widget front-end code authored by a widget
developer. It contains the front-end logic of a widget, which is defined by
implementing various [lifecycle methods](#lifecycle-methods) to control the
widget's behavior. AFM is a web-standard ECMAScript module (ESM) can be
authored simply as a text file or generated from a more complex front-end
toolchain.

### Host platform

The web-based environment in which a widget is embedded. It is responsible for
loading AFM and calling [lifecycle methods](#lifecycle-methods) with required
the platform APIs.

The `anywidget` Python library provides the necessary glue code to make all
Jupyter-like environments (e.g., Jupyter Notebook, JupyterLab, Google Colab, VS
Code) an AFM-compatible host platform. The
[marimo](https://github.com/marimo-team/marimo) project is a _native_ host
manzt marked this conversation as resolved.
Show resolved Hide resolved
platform.

## Anywidget Front-end Module (AFM)

An Anywidget Front-End Module (AFM) is an [ECMAScript
module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
that defines a widget's behavior through [lifecycle
methods](#lifecycle-methods).

```js
export default {
initialize({ model }) {
// Set up shared state or event handlers.
return () => {
// Optional: Called when the widget is destroyed.
}
},
render({ model, el }) {
// Render the widget's view into the el HTMLElement.
return () => {
// Optional: Called when the view is destroyed.
}
}
}
```

A host platform is expected to:

- Load this module.
- Call the lifecycle methods, passing in dependencies (`model` & `el`).

All browsers support ESM, so loading the module is supported natively across
manzt marked this conversation as resolved.
Show resolved Hide resolved
web-based environments. It is then the host platform's responsibility to
implement the required [`model` interface](#model-interface) and provide an
output DOM element (`el`). This simple mechanism allows new host platforms to
be implemented as long as they adhere to these requirements.

### Lifecycle Methods

The widget lifecycle in AFM follows a Model-View pattern, consisting of two
main phases:

- **Model Initialization**: Occurs when a widget is first created, setting up
the model and any shared state.
- **View Rendering**: Happens each time a widget needs to be displayed,
potentially multiple times for a single widget instance.

AFM exports methods that correspond to these lifecycle phases. The default
export object specifies one or more widget lifecycle hooks:

- `initialize({ model })`: Executed once per widget instance during model
initialization. It has access to `model` to setup non-view event handlers or
state to share across views.

- `render({ model, el })`: Executed once per view during view rendering. It has
access to both the `model` and an `el` DOM element. Used to setup event
handlers or access state specific to that view.

Each method MAY return a **cleanup function** which will be called when the
nvictus marked this conversation as resolved.
Show resolved Hide resolved
widget is destroyed or the view is removed.

#### Additional Setup Logic

The default export may also be a function that returns this interface. This
option can be useful to setup some front-end specific state for the lifecycle
of the widget:

```js
export default async () => {
let extraState = {};
return {
initialize({ model }) { /* ... */ },
render({ model, el }) { /* ... */ },
}
}
```

Here, `initialize` and `render` both will have access to `extraState` during the
lifetime of the widget.

### Model interface

The `model` interface in AFM is loosely based on traditional Jupyter Widgets but
defines a _narrower_ subset of APIs. This approach maintains familiarity for
widget developers while requiring host platforms to implement a small subset of
APIs to be a proper host.

The simplified interface is:

```typescript
/**
* The model interface for an Anywidget Front-End Module
* @see {https://github.com/manzt/anywidget/tree/main/packages/types} for complete types
*/
interface AnyModel {
/** Get a property value from the model
* @param key The key of the property to get
* @returns The value of the property
*/
get(key: string): any;
/**
* Set a property value in the model
* @param key The key of the property to set
* @param value The value to set
*/
set(key: string, value: any): void;
/**
* Remove an event listener
* @param eventName The name of the event to stop listening to
* @param callback The callback function to remove
*/
off(eventName?: string | null, callback?: Function | null): void;
/**
* Add an event listener for custom messages
* @param eventName Must be "msg:custom"
* @param callback The function to call when a custom message is received
*/
on(eventName: "msg:custom", callback: (msg: any, buffers: DataView[]) => void): void;
/**
* Add an event listener for property changes
* @param eventName The name of the event, in the format "change:propertyName"
* @param callback The function to call when the property changes
*/
on(eventName: `change: ${string}`, callback: Function): void;
/**
* Commit changes to sync with the backend
*/
save_changes(): void;
/**
* Send a custom message to the backend
* @param content The content of the message
* @param callbacks Optional callbacks for the message
* @param buffers Optional binary buffers to send with the message
*/
send(content: any, callbacks?: any, buffers?: ArrayBuffer[] | ArrayBufferView[]): void;
}
```

This interface can be implemented without any dependencies and does not need to
extend from [Jupyter Widget's patch of
BackboneJS](https://github.com/jupyter-widgets/ipywidgets/blob/main/packages/base/src/backbone-patch.ts).
For instance, marimo's `model` implementation uses [no third-party
dependencies](https://github.com/marimo-team/marimo/blob/7f3023ff0caef22b2bf4c1b5a18ad1899bd40fa3/frontend/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx#L161-L267).

## Framework Bridges

AFM intentionally does not prescribe specific models for state management or UI
rendering. While many front-end tools exist to help with authoring UIs (e.g.,
React, Svelte, Vue) we strongly believe that incorporating these
non-web-standard pieces at the specification level would be a mistake. Our goal
is to create a solution for reusable widgets that aligns with the web's strong
backwards compatibility garantuees.
manzt marked this conversation as resolved.
Show resolved Hide resolved

Instead of baking framework support into the specification, we envision support
for UI frameworks through:

- **Framework bridges**: Libraries that provide idiomatic APIs for popular
frameworks while adhering to the AFM specification.
- **Developer tooling**: Simple build processes that can compile
framework-specific code into standard AFM.

This approach gives anywidget developers the option to use their preferred
tools and frameworks while ensuring that the final output is a web-standard
JavaScript.

For example, using the `@anywidget/react` bridge looks like this:

```jsx
// index.jsx
import * as React from "react";
import { useModelState, createRender } from "@anywidget/react";

function Counter() {
let [count, setCount] = useModelState("count");
return <button onClick={() => setCount(count + 1)}>Count is {count}</button>;
}

export default {
render: createRender(Counter)
};
```

The bridge provides an idiomatic
[_hook_](https://react.dev/reference/react/hooks) for model state
(`useModelState`) and `createRender` function wraps a React component to
adhere to the AFM specification.

By maintaining this separation between frameworks and the core specification,
we ensure that AFM remains flexible, future-proof, and aligned with the
long-term evolution of web standards.
Loading