github manzt/anywidget anywidget@0.11.0

This release implements the widget composition RFC shared earlier this year. All changes are additive: existing widgets keep working without modification.

Background

For the past year, anywidget has predominantly been in maintenance mode. The library works, and the community has been expanding on both ends (more host platforms and more widgets shipped on top of it).

A stretch of stability also tends to surface what's still missing. One pattern that kept coming up (#28, #193, #855) was a different kind of widget than the AFM had been designed around.

Prior to v0.11, anywidget was a good fit for widgets that own a self-contained piece of the screen (a chart, a map, a control). Two limitations got in the way of anything else:

  • Composition was left to the host. A widget that wanted to lay out other widgets had to be expressed using whatever container the host provides (ipywidgets.HBox / ipywidgets.VBox in Jupyter, mo.hstack / mo.vstack in marimo). Container widgets that ship as a single anywidget primitive were not expressible.
  • Shared interfaces were limited to model state. The only thing one widget could know about another was what was exposed on the synced model. Calling panTo(lat, lng) or highlight(rowId) on another widget on the page had to round-trip through a synced trait.

v0.11 closes both gaps with three additions to the front-end contract.

For the full narrative, see the blog post: anywidgets All the Way Down.

For the formal spec, see the updated AFM specification.

What's new

signal (AbortSignal) on initialize and render (#974)

Both initialize and render now receive an AbortSignal via the signal prop. The host aborts it when the widget is destroyed (or during HMR). This is the preferred way to manage cleanup going forward, since it composes with web platform APIs that already accept an AbortSignal (addEventListener, fetch, child widgets):

// before
export default {
  render({ model, el }) {
    let handler = () => { /* ... */ };
    el.addEventListener("click", handler);
    return () => el.removeEventListener("click", handler);
  },
};

// after
export default {
  render({ model, el, signal }) {
    el.addEventListener("click", () => { /* ... */ }, { signal });
  },
};

Returning a cleanup callback from render (or initialize) still works, so existing widgets need no changes. New code is encouraged to prefer signal.

initialize MAY return an exports object (#974)

initialize runs once per widget instance. In v0.11, it MAY return a plain object: the widget's exports. The host stores it and exposes it to other widgets that resolve this one as a reference (next section).

export default {
  initialize({ model, signal }) {
    return {
      getValue: () => model.get("value"),
      setValue: (v) => {
        model.set("value", v);
        model.save_changes();
      },
      onChange: (cb) => model.on("change:value", cb),
    };
  },
  render({ model, el, signal }) {
    /* ... */
  },
};

The return type is distinguished by typeof: functions are still treated as cleanup callbacks (existing behavior), objects are treated as exports, and void means neither.

There is no schema and no validation. Widget authors define their own interfaces and consumers duck-type at the boundary.

host.getWidget and host.getModel for widget composition (#974)

render now receives a host prop with two methods that resolve a child widget by reference:

  • host.getWidget(ref) awaits the child's initialize and returns { exports, render }.
  • host.getModel(ref) returns the child's underlying AnyModel for direct event subscriptions or get / set / send access without participating in rendering.
export default {
  async render({ model, el, signal, host }) {
    let slider = await host.getWidget(model.get("control"));
    if (typeof slider.exports?.onChange === "function") {
      slider.exports.onChange(() =>
        console.log("value:", slider.exports.getValue()),
      );
    }
    let div = document.createElement("div");
    el.appendChild(div);
    await slider.render({ el: div, signal });
  },
};

Passing the parent's signal through to the child's render ties their lifecycles together: aborting the parent's view tears the child's view down too.

On the Python side, widget references are serialized as "anywidget:<model_id>" strings. A new WidgetTrait traitlet validates anywidget-compatible objects, and a Widget type alias is provided for annotations:

import anywidget

class Dashboard(anywidget.AnyWidget):
    _esm = "dashboard.js"
    control = anywidget.WidgetTrait().tag(sync=True)

Dashboard(control=Slider(value=50))

References work at any depth in synced state (top-level traits, values inside dicts, items inside lists).

Backward compatibility

Nothing is required to migrate. Existing widgets, including those returning cleanup callbacks from render or initialize, continue to work unchanged. New widgets can opt into signal, exports, and host incrementally, hook by hook.

If you maintain a host runtime, the host requirements for composition support are documented in the AFM spec. Hosts that do not (yet) implement composition should expose host on render and have its methods reject with a descriptive error rather than omitting the prop.

Patch Changes

  • Updated dependencies: @anywidget/types@0.4.0

Don't miss a new anywidget release

NewReleases is sending notifications on new releases.