@marimo-team/islands 0.23.15-dev10 → 0.23.15-dev11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{code-visibility-CyTw8jup.js → code-visibility-HO1U02IJ.js} +1 -1
- package/dist/main.js +19 -35
- package/dist/{reveal-component-CgdFrza3.js → reveal-component-BuQ2GKAM.js} +1 -1
- package/package.json +1 -1
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
|
@@ -36209,7 +36209,7 @@ ${d}`,
|
|
|
36209
36209
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
36210
36210
|
}
|
|
36211
36211
|
}
|
|
36212
|
-
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.15-
|
|
36212
|
+
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.15-dev11");
|
|
36213
36213
|
showCodeInRunModeAtom = atom(true);
|
|
36214
36214
|
atom(null);
|
|
36215
36215
|
var import_compiler_runtime = require_compiler_runtime();
|
package/dist/main.js
CHANGED
|
@@ -26,7 +26,7 @@ import { $ as reducer, $t as isUninstantiated, A as ChevronDownIcon, An as Item$
|
|
|
26
26
|
import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
27
27
|
import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
28
28
|
import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-Cz-Bj1JI.js";
|
|
29
|
-
import { $ as PANEL_TYPES, $t as hasFunctionProperty, A as prettifyRowCount, At as INDEX_COLUMN_NAME, B as CompactChipRow, Bt as Tabs, C as downloadSizeLimitAtom, Ct as ColumnChartSpecModel, D as ErrorState, Dt as getPageIndexForRow, E as EmptyState, Et as usePrevious$1, F as ContextMenuItem, Ft as isNullishFilter, G as Command, Gt as ChartInfoState, H as DateRangePicker, Ht as TabsList, I as ContextMenuSeparator, It as Maps, J as CommandItem, Jt as useOverflowDetection, K as CommandEmpty, Kt as ChartLoadingState, L as ContextMenuTrigger, Lt as dateToLocalISODate, M as getUserColumnVisibilityCounts, Mt as TOO_MANY_ROWS, N as ContextMenu, Nt as toFieldTypes, O as LoadingState, Ot as loadTableAndRawData, P as ContextMenuContent, Pt as getMimeValues, Q as ContextAwarePanelItem, Qt as EmotionCacheProvider, R as useInternalStateWithSync, Rt as dateToLocalISODateTime, S as Filenames, St as ColumnChartContext, T as ColumnPreviewContainer, Tt as useIntersectionObserver, U as Combobox, Ut as TabsTrigger, V as DatePicker, Vt as TabsContent, W as ComboboxItem, Wt as ChartErrorState, X as CommandSeparator, Xt as Kbd, Y as CommandList, Yt as RenderTextWithLinks, Z as smartMatch, Zt as HtmlOutput, _ as ADD_PRINTING_CLASS, _t as TableRow, an as Funnel, at as slotsController, b as downloadHTMLAsImage, bt as inferFieldTypes, c as Slide, cn as Ellipsis, d as RadioGroupItem, dn as ChevronsUpDown, dt as Provider$1, en as isRecord, et as contextAwarePanelOpen, f as JsonOutput, fn as ChevronsRight, ft as Table, g as InstallPackageButton, gn as ArrowDownWideNarrow, gt as TableHeader, h as DataTable, hn as ChevronLeft, ht as TableHead, in as GripHorizontal, it as SlotNames, j as getColumnCountForDisplay, jt as SELECT_COLUMN_ID, k as prettifyRowColumnCount, kt as loadTableData, l as Switch, ln as Download, m as OutputRenderer, mn as ChevronsDownUp, mt as TableCell, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$6b862160d295c8e, nt as contextAwarePanelType, o as SLIDE_TYPE_OPTIONS_BY_VALUE, on as EyeOff, ot as Toggle, p as OutputArea, pn as ChevronsLeft, pt as TableBody, q as CommandInput, qt as LazyVegaEmbed, r as showCodeInRunModeAtom, rn as TextWrap, rt as isCellAwareAtom, t as useNotebookCodeAvailable, tn as $fae977aafc393c5c$export$588937bcd60ade55, tt as contextAwarePanelOwner, u as RadioGroup, un as Code, ut as Fill, v as downloadBlob, vt as NAMELESS_COLUMN_PREFIX, w as ColumnName, wt as DelayMount, x as Progress, xt as renderCellValue, y as downloadByURL, yt as generateColumns, z as useSelectList, zt as dateToLocalISOTime, __tla as __tla_2 } from "./code-visibility-
|
|
29
|
+
import { $ as PANEL_TYPES, $t as hasFunctionProperty, A as prettifyRowCount, At as INDEX_COLUMN_NAME, B as CompactChipRow, Bt as Tabs, C as downloadSizeLimitAtom, Ct as ColumnChartSpecModel, D as ErrorState, Dt as getPageIndexForRow, E as EmptyState, Et as usePrevious$1, F as ContextMenuItem, Ft as isNullishFilter, G as Command, Gt as ChartInfoState, H as DateRangePicker, Ht as TabsList, I as ContextMenuSeparator, It as Maps, J as CommandItem, Jt as useOverflowDetection, K as CommandEmpty, Kt as ChartLoadingState, L as ContextMenuTrigger, Lt as dateToLocalISODate, M as getUserColumnVisibilityCounts, Mt as TOO_MANY_ROWS, N as ContextMenu, Nt as toFieldTypes, O as LoadingState, Ot as loadTableAndRawData, P as ContextMenuContent, Pt as getMimeValues, Q as ContextAwarePanelItem, Qt as EmotionCacheProvider, R as useInternalStateWithSync, Rt as dateToLocalISODateTime, S as Filenames, St as ColumnChartContext, T as ColumnPreviewContainer, Tt as useIntersectionObserver, U as Combobox, Ut as TabsTrigger, V as DatePicker, Vt as TabsContent, W as ComboboxItem, Wt as ChartErrorState, X as CommandSeparator, Xt as Kbd, Y as CommandList, Yt as RenderTextWithLinks, Z as smartMatch, Zt as HtmlOutput, _ as ADD_PRINTING_CLASS, _t as TableRow, an as Funnel, at as slotsController, b as downloadHTMLAsImage, bt as inferFieldTypes, c as Slide, cn as Ellipsis, d as RadioGroupItem, dn as ChevronsUpDown, dt as Provider$1, en as isRecord, et as contextAwarePanelOpen, f as JsonOutput, fn as ChevronsRight, ft as Table, g as InstallPackageButton, gn as ArrowDownWideNarrow, gt as TableHeader, h as DataTable, hn as ChevronLeft, ht as TableHead, in as GripHorizontal, it as SlotNames, j as getColumnCountForDisplay, jt as SELECT_COLUMN_ID, k as prettifyRowColumnCount, kt as loadTableData, l as Switch, ln as Download, m as OutputRenderer, mn as ChevronsDownUp, mt as TableCell, n as marimoVersionAtom, nn as $fae977aafc393c5c$export$6b862160d295c8e, nt as contextAwarePanelType, o as SLIDE_TYPE_OPTIONS_BY_VALUE, on as EyeOff, ot as Toggle, p as OutputArea, pn as ChevronsLeft, pt as TableBody, q as CommandInput, qt as LazyVegaEmbed, r as showCodeInRunModeAtom, rn as TextWrap, rt as isCellAwareAtom, t as useNotebookCodeAvailable, tn as $fae977aafc393c5c$export$588937bcd60ade55, tt as contextAwarePanelOwner, u as RadioGroup, un as Code, ut as Fill, v as downloadBlob, vt as NAMELESS_COLUMN_PREFIX, w as ColumnName, wt as DelayMount, x as Progress, xt as renderCellValue, y as downloadByURL, yt as generateColumns, z as useSelectList, zt as dateToLocalISOTime, __tla as __tla_2 } from "./code-visibility-HO1U02IJ.js";
|
|
30
30
|
import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-DegSo0lV.js";
|
|
31
31
|
import { t as Check } from "./check-C9OoNtR4.js";
|
|
32
32
|
import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-Dq_j3Rxw.js";
|
|
@@ -97,7 +97,7 @@ let __tla = Promise.all([
|
|
|
97
97
|
}
|
|
98
98
|
})()
|
|
99
99
|
]).then(async () => {
|
|
100
|
-
var _a, _e2, _t2, _n2, _r, _i, _e_instances, a_fn, o_fn, s_fn, _c, _b, _e3, _WidgetDefRegistry_instances, t_fn, _c2, _e4, _t3, _n3, _r2, _i2, _a2, _o2, _e_instances2, s_fn2, c_fn,
|
|
100
|
+
var _a, _e2, _t2, _n2, _r, _i, _e_instances, a_fn, o_fn, s_fn, _c, _b, _e3, _WidgetDefRegistry_instances, t_fn, _c2, _e4, _t3, _n3, _r2, _i2, _a2, _o2, _e_instances2, s_fn2, c_fn, _d, _e5, _t4, _n4, _r3, _i3, _a3, _o3, _s, _c3, _l, _u, _d2, _f, _p, _WidgetRuntime_instances, m_fn, h_fn, g_fn, __fn, v_fn, y_fn, b_fn, x_fn, _e6, _e7, _t5, _n5, _instances, r_fn, _f2, _e8, _t6, _n6, _r4, _i4, _a4, _o4, _s2, _MatplotlibRenderer_instances, c_fn2, l_fn, u_fn, _d3, _f3, _p2, _m, _h, _g, __, _v, _y, _b2, _x, _S, _C, _w, _g2, _h2;
|
|
101
101
|
var defaultIconDimensions = Object.freeze({
|
|
102
102
|
left: 0,
|
|
103
103
|
top: 0,
|
|
@@ -3483,32 +3483,29 @@ let __tla = Promise.all([
|
|
|
3483
3483
|
return e == null ? e : _baseSet_default(e, r, c);
|
|
3484
3484
|
}
|
|
3485
3485
|
var set_default = set$1;
|
|
3486
|
-
function modelProxy(e, r
|
|
3486
|
+
function modelProxy(e, r) {
|
|
3487
3487
|
return {
|
|
3488
3488
|
get(r2) {
|
|
3489
3489
|
return e.get(r2);
|
|
3490
3490
|
},
|
|
3491
|
-
set(r2,
|
|
3492
|
-
e.set(r2,
|
|
3491
|
+
set(r2, c) {
|
|
3492
|
+
e.set(r2, c);
|
|
3493
3493
|
},
|
|
3494
3494
|
save_changes() {
|
|
3495
3495
|
e.save_changes();
|
|
3496
3496
|
},
|
|
3497
3497
|
send: e.send.bind(e),
|
|
3498
|
-
on(
|
|
3499
|
-
r.aborted || (e.on(
|
|
3498
|
+
on(c, l) {
|
|
3499
|
+
r.aborted || (e.on(c, l), r.addEventListener("abort", () => e.off(c, l), {
|
|
3500
3500
|
once: true
|
|
3501
|
-
}), c == null ? void 0 : c({
|
|
3502
|
-
event: l,
|
|
3503
|
-
callback: u
|
|
3504
3501
|
}));
|
|
3505
3502
|
},
|
|
3506
|
-
off(r2,
|
|
3507
|
-
e.off(r2 ?? null,
|
|
3503
|
+
off(r2, c) {
|
|
3504
|
+
e.off(r2 ?? null, c ?? null);
|
|
3508
3505
|
},
|
|
3509
3506
|
widget_manager: {
|
|
3510
|
-
async get_model(
|
|
3511
|
-
return modelProxy(await e.widget_manager.get_model(
|
|
3507
|
+
async get_model(c) {
|
|
3508
|
+
return modelProxy(await e.widget_manager.get_model(c), r);
|
|
3512
3509
|
}
|
|
3513
3510
|
}
|
|
3514
3511
|
};
|
|
@@ -3839,17 +3836,17 @@ let __tla = Promise.all([
|
|
|
3839
3836
|
]);
|
|
3840
3837
|
if (l.aborted) return;
|
|
3841
3838
|
e.el.innerHTML = "";
|
|
3842
|
-
let u = __privateGet(this, _i2).call(this, l), f =
|
|
3843
|
-
model: modelProxy(__privateGet(this, _n3), l
|
|
3839
|
+
let u = __privateGet(this, _i2).call(this, l), f = await c.render({
|
|
3840
|
+
model: modelProxy(__privateGet(this, _n3), l),
|
|
3844
3841
|
el: e.el,
|
|
3845
3842
|
experimental,
|
|
3846
3843
|
signal: l,
|
|
3847
3844
|
host: u
|
|
3848
3845
|
});
|
|
3849
|
-
if (isCleanup(
|
|
3846
|
+
if (isCleanup(f)) {
|
|
3850
3847
|
let e2 = () => {
|
|
3851
3848
|
let e3 = r.signal.aborted ? "view unmount" : "binding destroyed";
|
|
3852
|
-
return Logger.debug(`[WidgetBinding] Render cleanup triggered (reason: ${e3})`), __privateMethod(this, _e_instances2, c_fn).call(this,
|
|
3849
|
+
return Logger.debug(`[WidgetBinding] Render cleanup triggered (reason: ${e3})`), __privateMethod(this, _e_instances2, c_fn).call(this, f, "render");
|
|
3853
3850
|
};
|
|
3854
3851
|
if (l.aborted) {
|
|
3855
3852
|
await e2();
|
|
@@ -3859,22 +3856,9 @@ let __tla = Promise.all([
|
|
|
3859
3856
|
once: true
|
|
3860
3857
|
});
|
|
3861
3858
|
}
|
|
3862
|
-
__privateMethod(this, _e_instances2, l_fn).call(this, f, l);
|
|
3863
3859
|
}, c_fn = function(e, r) {
|
|
3864
3860
|
let c = safelyRunCleanup(e, r);
|
|
3865
3861
|
return __privateGet(this, _a2).add(c), c.finally(() => __privateGet(this, _a2).delete(c)), c;
|
|
3866
|
-
}, l_fn = function(e, r) {
|
|
3867
|
-
for (let { event: c, callback: l } of e) {
|
|
3868
|
-
if (r.aborted) return;
|
|
3869
|
-
try {
|
|
3870
|
-
if (c.startsWith("change:")) {
|
|
3871
|
-
let e2 = c.slice(7);
|
|
3872
|
-
l(__privateGet(this, _n3).get(e2));
|
|
3873
|
-
} else c === "change" && l();
|
|
3874
|
-
} catch (e2) {
|
|
3875
|
-
Logger.error("[WidgetBinding] Error replaying state", e2);
|
|
3876
|
-
}
|
|
3877
|
-
}
|
|
3878
3862
|
}, _d);
|
|
3879
3863
|
const WIDGET_DEF_REGISTRY = new WidgetDefRegistry();
|
|
3880
3864
|
var WidgetRuntime = (_e6 = class {
|
|
@@ -26246,7 +26230,7 @@ ${c}
|
|
|
26246
26230
|
signal: r.signal
|
|
26247
26231
|
}), e.addEventListener("keydown", __privateGet(this, _w), {
|
|
26248
26232
|
signal: r.signal
|
|
26249
|
-
}), __privateMethod(this, _MatplotlibRenderer_instances,
|
|
26233
|
+
}), __privateMethod(this, _MatplotlibRenderer_instances, l_fn).call(this, r.signal), r.signal.addEventListener("abort", () => {
|
|
26250
26234
|
cancelAnimationFrame(__privateGet(this, _i4)), __privateGet(this, _e8).remove();
|
|
26251
26235
|
}), __privateMethod(this, _MatplotlibRenderer_instances, u_fn).call(this, __privateGet(this, _n6).chartBase64), __privateGet(this, _b2).call(this, __privateGet(this, _n6).value);
|
|
26252
26236
|
}
|
|
@@ -26268,9 +26252,9 @@ ${c}
|
|
|
26268
26252
|
}, _e8 = new WeakMap(), _t6 = new WeakMap(), _n6 = new WeakMap(), _r4 = new WeakMap(), _i4 = new WeakMap(), _a4 = new WeakMap(), _o4 = new WeakMap(), _s2 = new WeakMap(), _MatplotlibRenderer_instances = new WeakSet(), c_fn2 = function(e = __privateGet(this, _e8)) {
|
|
26269
26253
|
let r = globalThis.devicePixelRatio ?? 1, { width: c, height: l } = __privateGet(this, _n6);
|
|
26270
26254
|
e.width = c * r, e.height = l * r, e.style.width = `${c}px`, e.style.maxWidth = "100%", e.style.height = "auto", e.style.aspectRatio = `${c} / ${l}`;
|
|
26271
|
-
},
|
|
26255
|
+
}, l_fn = function(e) {
|
|
26272
26256
|
e.aborted || matchMedia(`(resolution: ${globalThis.devicePixelRatio ?? 1}dppx)`).addEventListener("change", () => {
|
|
26273
|
-
__privateMethod(this, _MatplotlibRenderer_instances, c_fn2).call(this), __privateGet(this, _d3).call(this), __privateMethod(this, _MatplotlibRenderer_instances,
|
|
26257
|
+
__privateMethod(this, _MatplotlibRenderer_instances, c_fn2).call(this), __privateGet(this, _d3).call(this), __privateMethod(this, _MatplotlibRenderer_instances, l_fn).call(this, e);
|
|
26274
26258
|
}, {
|
|
26275
26259
|
once: true,
|
|
26276
26260
|
signal: e
|
|
@@ -38040,7 +38024,7 @@ ${c}
|
|
|
38040
38024
|
function _temp2$2(e) {
|
|
38041
38025
|
e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
|
|
38042
38026
|
}
|
|
38043
|
-
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-
|
|
38027
|
+
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-BuQ2GKAM.js"));
|
|
38044
38028
|
const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
|
|
38045
38029
|
var _a5;
|
|
38046
38030
|
let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
|
|
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
|
9
9
|
import { lt as kioskModeAtom, pt as outputIsStale } from "./html-to-image-JXL8cvmt.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
12
|
-
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-
|
|
12
|
+
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-HO1U02IJ.js";
|
|
13
13
|
import { X as useDebouncedCallback } from "./input-DtsN7xm-.js";
|
|
14
14
|
import "./toDate-CWNNlFEX.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
package/package.json
CHANGED
|
@@ -277,25 +277,22 @@ describe("WidgetBinding receives a host in render props", () => {
|
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
describe("host-mounted views
|
|
281
|
-
it("
|
|
282
|
-
const childId = asModelId("host-
|
|
280
|
+
describe("host-mounted views", () => {
|
|
281
|
+
it("render with the child's current model state", async () => {
|
|
282
|
+
const childId = asModelId("host-child-state");
|
|
283
283
|
const parentController = new AbortController();
|
|
284
284
|
const getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
|
|
285
285
|
try {
|
|
286
286
|
const childWidget = {
|
|
287
287
|
render: vi.fn(({ model, el }) => {
|
|
288
|
-
el.textContent =
|
|
289
|
-
model.on("change:count", () => {
|
|
290
|
-
el.textContent = `count is ${model.get("count")}`;
|
|
291
|
-
});
|
|
288
|
+
el.textContent = `count is ${model.get("count")}`;
|
|
292
289
|
}),
|
|
293
290
|
};
|
|
294
291
|
const childModel = new Model<ModelState>({ count: 8 }, createMockComm());
|
|
295
292
|
WIDGET_REGISTRY.setModel(childId, childModel);
|
|
296
293
|
WIDGET_REGISTRY.setSpec(childId, {
|
|
297
|
-
url: "./@file/10-host-
|
|
298
|
-
hash: "hash-host-
|
|
294
|
+
url: "./@file/10-host-child-state.js",
|
|
295
|
+
hash: "hash-host-child-state",
|
|
299
296
|
});
|
|
300
297
|
getModuleSpy.mockResolvedValue({ default: childWidget });
|
|
301
298
|
|
|
@@ -337,13 +337,12 @@ describe("WidgetBinding", () => {
|
|
|
337
337
|
{ el: document.createElement("div") },
|
|
338
338
|
{ signal: controller.signal },
|
|
339
339
|
);
|
|
340
|
-
// Called once by the hydration replay at mount, once by the set.
|
|
341
340
|
model.set("count", 1);
|
|
342
|
-
expect(onCount).toHaveBeenCalledTimes(
|
|
341
|
+
expect(onCount).toHaveBeenCalledTimes(1);
|
|
343
342
|
|
|
344
343
|
controller.abort();
|
|
345
344
|
model.set("count", 2);
|
|
346
|
-
expect(onCount).toHaveBeenCalledTimes(
|
|
345
|
+
expect(onCount).toHaveBeenCalledTimes(1);
|
|
347
346
|
});
|
|
348
347
|
|
|
349
348
|
it("should auto-clear initialize listeners on destroy", async () => {
|
|
@@ -366,92 +365,54 @@ describe("WidgetBinding", () => {
|
|
|
366
365
|
});
|
|
367
366
|
});
|
|
368
367
|
|
|
369
|
-
describe("WidgetBinding
|
|
370
|
-
it("
|
|
368
|
+
describe("WidgetBinding render listeners", () => {
|
|
369
|
+
it("fire only for changes after mount, never for pre-existing state", async () => {
|
|
371
370
|
const model = new Model<ModelState>({ count: 8 }, createMockComm());
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
render: vi.fn(({ model, el }) => {
|
|
375
|
-
// A widget view that starts with a local default and relies on
|
|
376
|
-
// change events for hydration.
|
|
377
|
-
el.textContent = "count is 5";
|
|
378
|
-
model.on("change:count", () => {
|
|
379
|
-
el.textContent = `count is ${model.get("count")}`;
|
|
380
|
-
});
|
|
381
|
-
}),
|
|
382
|
-
};
|
|
383
|
-
const binding = await createBinding(widgetDef, model);
|
|
384
|
-
|
|
385
|
-
const controller = new AbortController();
|
|
386
|
-
await binding.createView({ el }, { signal: controller.signal });
|
|
387
|
-
expect(el.textContent).toBe("count is 8");
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("does not re-fire listeners of already-mounted views", async () => {
|
|
391
|
-
// Mounting view B must not double-paint view A: the replay is
|
|
392
|
-
// scoped to the listeners the new render attached.
|
|
393
|
-
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
394
|
-
const listeners: Array<ReturnType<typeof vi.fn>> = [];
|
|
371
|
+
const onCount = vi.fn();
|
|
372
|
+
const onAnyChange = vi.fn();
|
|
395
373
|
const widgetDef = {
|
|
396
374
|
render: vi.fn(({ model }) => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
model.on("change:count", listener);
|
|
375
|
+
model.on("change:count", onCount);
|
|
376
|
+
model.on("change", onAnyChange);
|
|
400
377
|
}),
|
|
401
378
|
};
|
|
402
379
|
const binding = await createBinding(widgetDef, model);
|
|
403
380
|
const controller = new AbortController();
|
|
404
|
-
|
|
405
381
|
await binding.createView(
|
|
406
382
|
{ el: document.createElement("div") },
|
|
407
383
|
{ signal: controller.signal },
|
|
408
384
|
);
|
|
409
|
-
expect(
|
|
385
|
+
expect(onCount).not.toHaveBeenCalled();
|
|
386
|
+
expect(onAnyChange).not.toHaveBeenCalled();
|
|
410
387
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
);
|
|
415
|
-
// View B's listener hydrated once; view A's was left alone.
|
|
416
|
-
expect(listeners[1]).toHaveBeenCalledTimes(1);
|
|
417
|
-
expect(listeners[0]).toHaveBeenCalledTimes(1);
|
|
388
|
+
model.set("count", 9);
|
|
389
|
+
expect(onCount).toHaveBeenCalledTimes(1);
|
|
390
|
+
expect(onCount).toHaveBeenCalledWith(9);
|
|
418
391
|
});
|
|
419
392
|
|
|
420
|
-
it("
|
|
421
|
-
const model = new Model<ModelState>({ count:
|
|
422
|
-
const
|
|
393
|
+
it("mounting a second view does not fire the first view's listeners", async () => {
|
|
394
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
395
|
+
const listeners: Array<ReturnType<typeof vi.fn>> = [];
|
|
423
396
|
const widgetDef = {
|
|
424
397
|
render: vi.fn(({ model }) => {
|
|
425
|
-
|
|
398
|
+
const listener = vi.fn();
|
|
399
|
+
listeners.push(listener);
|
|
400
|
+
model.on("change:count", listener);
|
|
426
401
|
}),
|
|
427
402
|
};
|
|
428
403
|
const binding = await createBinding(widgetDef, model);
|
|
429
404
|
const controller = new AbortController();
|
|
405
|
+
|
|
430
406
|
await binding.createView(
|
|
431
407
|
{ el: document.createElement("div") },
|
|
432
408
|
{ signal: controller.signal },
|
|
433
409
|
);
|
|
434
|
-
expect(onAnyChange).toHaveBeenCalledTimes(1);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("does not replay initialize listeners", async () => {
|
|
438
|
-
// The guarantee is per-view: initialize listeners existed before
|
|
439
|
-
// any view, and replaying at them would fire once per mount.
|
|
440
|
-
const model = new Model<ModelState>({ count: 1 }, createMockComm());
|
|
441
|
-
const initListener = vi.fn();
|
|
442
|
-
const widgetDef = {
|
|
443
|
-
initialize: vi.fn(({ model }) => {
|
|
444
|
-
model.on("change:count", initListener);
|
|
445
|
-
}),
|
|
446
|
-
render: vi.fn(),
|
|
447
|
-
};
|
|
448
|
-
const binding = await createBinding(widgetDef, model);
|
|
449
|
-
const controller = new AbortController();
|
|
450
410
|
await binding.createView(
|
|
451
411
|
{ el: document.createElement("div") },
|
|
452
412
|
{ signal: controller.signal },
|
|
453
413
|
);
|
|
454
|
-
expect(
|
|
414
|
+
expect(listeners[0]).not.toHaveBeenCalled();
|
|
415
|
+
expect(listeners[1]).not.toHaveBeenCalled();
|
|
455
416
|
});
|
|
456
417
|
|
|
457
418
|
it("clears the element before rendering into it", async () => {
|
|
@@ -5,15 +5,6 @@ import type { ModelState } from "./types";
|
|
|
5
5
|
|
|
6
6
|
type ModelEventCallback = Parameters<AnyModel<ModelState>["on"]>[1];
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* A listener registered through a model proxy (see the hydration
|
|
10
|
-
* replay in `WidgetBinding.createView`).
|
|
11
|
-
*/
|
|
12
|
-
export interface ProxyRegistration {
|
|
13
|
-
event: string;
|
|
14
|
-
callback: ModelEventCallback;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
8
|
/**
|
|
18
9
|
* Wrap a model so every `on()` call from inside `initialize` or `render`
|
|
19
10
|
* is auto-tied to a lifetime `AbortSignal`. When the signal aborts,
|
|
@@ -27,14 +18,11 @@ export interface ProxyRegistration {
|
|
|
27
18
|
* The proxy is purely a host-side ergonomic helper. The widget author
|
|
28
19
|
* still writes `model.on("change:foo", handler)` exactly as before; the
|
|
29
20
|
* cleanup signal is supplied transparently.
|
|
30
|
-
*
|
|
31
|
-
* `onRegister`, when given, observes each `on()` registration.
|
|
32
21
|
*/
|
|
33
22
|
// oxlint-disable-next-line marimo/prefer-object-params -- concise internal helper used at protocol call sites
|
|
34
23
|
export function modelProxy<T extends ModelState>(
|
|
35
24
|
model: AnyModel<T>,
|
|
36
25
|
signal: AbortSignal,
|
|
37
|
-
onRegister?: (registration: ProxyRegistration) => void,
|
|
38
26
|
): AnyModel<T> {
|
|
39
27
|
return {
|
|
40
28
|
get(key) {
|
|
@@ -55,7 +43,6 @@ export function modelProxy<T extends ModelState>(
|
|
|
55
43
|
signal.addEventListener("abort", () => model.off(name, callback), {
|
|
56
44
|
once: true,
|
|
57
45
|
});
|
|
58
|
-
onRegister?.({ event: name, callback });
|
|
59
46
|
},
|
|
60
47
|
off(name?: string | null, callback?: ModelEventCallback | null): void {
|
|
61
48
|
model.off(name ?? null, callback ?? null);
|
|
@@ -12,7 +12,7 @@ import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
|
|
|
12
12
|
import { Logger } from "@/utils/Logger";
|
|
13
13
|
import type { Host } from "./host";
|
|
14
14
|
import type { Model } from "./model";
|
|
15
|
-
import { modelProxy
|
|
15
|
+
import { modelProxy } from "./model-proxy";
|
|
16
16
|
import type { ModelState } from "./types";
|
|
17
17
|
|
|
18
18
|
export const experimental: Experimental = {
|
|
@@ -266,9 +266,8 @@ export class WidgetBinding<T extends ModelState = ModelState> {
|
|
|
266
266
|
* when the caller's `signal` fires or the binding is destroyed, and
|
|
267
267
|
* listeners registered inside `render` auto-clear on abort.
|
|
268
268
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* to this view's listeners; re-firing at other views double-paints.
|
|
269
|
+
* `render` reads current state via `model.get`; change listeners
|
|
270
|
+
* observe only subsequent changes, matching Jupyter semantics.
|
|
272
271
|
*/
|
|
273
272
|
async createView(
|
|
274
273
|
target: { el: HTMLElement },
|
|
@@ -305,11 +304,8 @@ export class WidgetBinding<T extends ModelState = ModelState> {
|
|
|
305
304
|
// Each view gets a host scoped to its own signal so child views tear
|
|
306
305
|
// down with this view.
|
|
307
306
|
const host = this.#createHost(renderSignal);
|
|
308
|
-
const registrations: ProxyRegistration[] = [];
|
|
309
307
|
const renderCleanup = await widget.render({
|
|
310
|
-
model: modelProxy(this.#model, renderSignal,
|
|
311
|
-
registrations.push(registration),
|
|
312
|
-
),
|
|
308
|
+
model: modelProxy(this.#model, renderSignal),
|
|
313
309
|
el: target.el,
|
|
314
310
|
experimental,
|
|
315
311
|
signal: renderSignal,
|
|
@@ -333,7 +329,6 @@ export class WidgetBinding<T extends ModelState = ModelState> {
|
|
|
333
329
|
once: true,
|
|
334
330
|
});
|
|
335
331
|
}
|
|
336
|
-
this.#replayState(registrations, renderSignal);
|
|
337
332
|
}
|
|
338
333
|
|
|
339
334
|
#trackCleanup(cleanup: Cleanup, reason: string): Promise<void> {
|
|
@@ -343,31 +338,6 @@ export class WidgetBinding<T extends ModelState = ModelState> {
|
|
|
343
338
|
return task;
|
|
344
339
|
}
|
|
345
340
|
|
|
346
|
-
/**
|
|
347
|
-
* The hydration guarantee documented on `createView`.
|
|
348
|
-
*/
|
|
349
|
-
#replayState(
|
|
350
|
-
registrations: readonly ProxyRegistration[],
|
|
351
|
-
renderSignal: AbortSignal,
|
|
352
|
-
): void {
|
|
353
|
-
const changePrefix = "change:";
|
|
354
|
-
for (const { event, callback } of registrations) {
|
|
355
|
-
if (renderSignal.aborted) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
if (event.startsWith(changePrefix)) {
|
|
360
|
-
const key = event.slice(changePrefix.length);
|
|
361
|
-
callback(this.#model.get(key));
|
|
362
|
-
} else if (event === "change") {
|
|
363
|
-
callback();
|
|
364
|
-
}
|
|
365
|
-
} catch (error) {
|
|
366
|
-
Logger.error("[WidgetBinding] Error replaying state", error);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
341
|
/**
|
|
372
342
|
* Destroy this generation, running initialize/render cleanups and
|
|
373
343
|
* clearing listeners registered through its model proxies.
|