@marimo-team/islands 0.19.8-dev5 → 0.19.8-dev50
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/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
- package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
- package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
- package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
- package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
- package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
- package/dist/assets/worker-DUYMdbtA.js +73 -0
- package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
- package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
- package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
- package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
- package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
- package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
- package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
- package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
- package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
- package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
- package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
- package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
- package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
- package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
- package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
- package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
- package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
- package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
- package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
- package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
- package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
- package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
- package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
- package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
- package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
- package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
- package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
- package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
- package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
- package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
- package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
- package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
- package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
- package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
- package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
- package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
- package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
- package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
- package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
- package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
- package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
- package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
- package/dist/main.js +2000 -1887
- package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
- package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
- package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
- package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
- package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
- package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
- package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
- package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
- package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
- package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
- package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
- package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
- package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
- package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
- package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
- package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
- package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
- package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
- package/dist/style.css +1 -1
- package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
- package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
- package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
- package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-BhZZsis0.js} +12 -8
- package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
- package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
- package/dist/{vega-component-rDX7xwxH.js → vega-component-DCxUyPnb.js} +10 -10
- package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
- package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
- package/package.json +18 -18
- package/src/components/app-config/ai-config.tsx +11 -2
- package/src/components/app-config/optional-features.tsx +1 -1
- package/src/components/app-config/user-config-form.tsx +0 -54
- package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
- package/src/components/chat/acp/__tests__/state.test.ts +69 -0
- package/src/components/chat/acp/agent-panel.tsx +26 -77
- package/src/components/chat/acp/state.ts +6 -6
- package/src/components/chat/chat-components.tsx +114 -1
- package/src/components/chat/chat-panel.tsx +79 -134
- package/src/components/chat/chat-utils.ts +42 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
- package/src/components/editor/actions/useCellActionButton.tsx +14 -1
- package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
- package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
- package/src/components/editor/cell/CreateCellButton.tsx +2 -1
- package/src/components/editor/cell/code/cell-editor.tsx +12 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
- package/src/components/editor/database/__tests__/__snapshots__/as-code.test.ts.snap +15 -0
- package/src/components/editor/database/__tests__/as-code.test.ts +8 -0
- package/src/components/editor/database/as-code.ts +3 -0
- package/src/components/editor/database/schemas.ts +9 -0
- package/src/components/editor/renderers/cell-array.tsx +2 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
- package/src/components/pages/gallery-page.tsx +37 -6
- package/src/core/MarimoApp.tsx +12 -8
- package/src/core/ai/context/providers/file.ts +1 -1
- package/src/core/cells/__tests__/cells.test.ts +120 -0
- package/src/core/cells/__tests__/session.test.ts +37 -1
- package/src/core/cells/cells.ts +14 -0
- package/src/core/cells/session.ts +20 -8
- package/src/core/codemirror/language/languages/markdown.ts +7 -0
- package/src/core/config/feature-flag.tsx +0 -4
- package/src/core/dom/uiregistry.ts +4 -1
- package/src/core/islands/__tests__/bridge.test.ts +7 -2
- package/src/core/islands/bridge.ts +1 -1
- package/src/core/islands/main.ts +7 -0
- package/src/core/network/types.ts +2 -2
- package/src/core/run-app.tsx +11 -4
- package/src/core/static/__tests__/files.test.ts +195 -1
- package/src/core/static/files.ts +39 -9
- package/src/core/wasm/bridge.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
- package/src/plugins/core/registerReactComponent.tsx +9 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +93 -168
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
- package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
- package/src/plugins/impl/anywidget/model.ts +348 -223
- package/src/plugins/impl/anywidget/schemas.ts +32 -0
- package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
- package/src/plugins/impl/anywidget/types.ts +27 -0
- package/src/plugins/impl/chat/chat-ui.tsx +22 -20
- package/src/utils/Deferred.ts +21 -0
- package/src/utils/__tests__/blob.test.ts +3 -3
- package/src/utils/__tests__/id-tree.test.ts +22 -7
- package/src/utils/__tests__/mime-types.test.ts +8 -10
- package/src/utils/__tests__/url-parser.test.ts +22 -0
- package/src/utils/blob.ts +14 -27
- package/src/utils/id-tree.tsx +11 -19
- package/src/utils/json/base64.ts +38 -8
- package/src/utils/mime-types.ts +5 -5
- package/src/utils/url-parser.ts +1 -1
- package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
- package/dist/assets/worker-SqntmiwV.js +0 -73
- package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
package/src/core/static/files.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { Loader } from "@/plugins/impl/vega/vega-loader";
|
|
4
|
+
import { deserializeBlob } from "@/utils/blob";
|
|
5
|
+
import type { DataURLString } from "@/utils/json/base64";
|
|
4
6
|
import { Logger } from "@/utils/Logger";
|
|
5
7
|
import { getStaticVirtualFiles } from "./static-state";
|
|
6
8
|
import type { StaticVirtualFiles } from "./types";
|
|
@@ -120,6 +122,24 @@ function withoutLeadingDot(path: string): string {
|
|
|
120
122
|
return path.startsWith(".") ? path.slice(1) : path;
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a URL to a blob URL if it's a virtual file, for use with dynamic import().
|
|
127
|
+
* Unlike fetch, import() can't be patched, so we need to convert data URLs to blob URLs.
|
|
128
|
+
*
|
|
129
|
+
* @returns The original URL if not a virtual file, or a blob URL if it is
|
|
130
|
+
*/
|
|
131
|
+
export function resolveVirtualFileURL(
|
|
132
|
+
url: string,
|
|
133
|
+
files: StaticVirtualFiles = getStaticVirtualFiles(),
|
|
134
|
+
): string {
|
|
135
|
+
const vfile = maybeGetVirtualFile(url, files);
|
|
136
|
+
if (!vfile) {
|
|
137
|
+
return url;
|
|
138
|
+
}
|
|
139
|
+
const blob = deserializeBlob(vfile as DataURLString);
|
|
140
|
+
return URL.createObjectURL(blob);
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
function maybeGetVirtualFile(
|
|
124
144
|
url: string,
|
|
125
145
|
files: StaticVirtualFiles,
|
|
@@ -130,14 +150,11 @@ function maybeGetVirtualFile(
|
|
|
130
150
|
}
|
|
131
151
|
const pathname = new URL(url, base).pathname;
|
|
132
152
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
url = url.slice(indexOfFile);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
153
|
+
// Extract the /@file/... suffix from the URL or pathname
|
|
154
|
+
// This handles URLs like https://example.com/prefix/@file/foo.js
|
|
155
|
+
// or file:///path/to/@file/foo.js
|
|
156
|
+
const filePathFromUrl = extractFilePath(url);
|
|
157
|
+
const filePathFromPathname = extractFilePath(pathname);
|
|
141
158
|
|
|
142
159
|
// Few variations to grab the URL.
|
|
143
160
|
// This can happen if a static file was open at file:// or https://
|
|
@@ -145,6 +162,19 @@ function maybeGetVirtualFile(
|
|
|
145
162
|
files[url] ||
|
|
146
163
|
files[withoutLeadingDot(url)] ||
|
|
147
164
|
files[pathname] ||
|
|
148
|
-
files[withoutLeadingDot(pathname)]
|
|
165
|
+
files[withoutLeadingDot(pathname)] ||
|
|
166
|
+
(filePathFromUrl && files[filePathFromUrl]) ||
|
|
167
|
+
(filePathFromPathname && files[filePathFromPathname])
|
|
149
168
|
);
|
|
150
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract the /@file/... path from a URL string
|
|
173
|
+
*/
|
|
174
|
+
function extractFilePath(url: string): string | null {
|
|
175
|
+
const indexOfFile = url.indexOf("/@file/");
|
|
176
|
+
if (indexOfFile !== -1) {
|
|
177
|
+
return url.slice(indexOfFile);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -541,7 +541,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
541
541
|
|
|
542
542
|
sendModelValue: RunRequests["sendModelValue"] = async (request) => {
|
|
543
543
|
await this.putControlRequest({
|
|
544
|
-
type: "
|
|
544
|
+
type: "model",
|
|
545
545
|
...request,
|
|
546
546
|
});
|
|
547
547
|
return null;
|
|
@@ -11,7 +11,6 @@ import { useConnectionTransport } from "@/core/websocket/useWebSocket";
|
|
|
11
11
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
12
12
|
import {
|
|
13
13
|
handleWidgetMessage,
|
|
14
|
-
isMessageWidgetState,
|
|
15
14
|
MODEL_MANAGER,
|
|
16
15
|
} from "@/plugins/impl/anywidget/model";
|
|
17
16
|
import { logNever } from "@/utils/assertNever";
|
|
@@ -141,31 +140,22 @@ export function useMarimoKernelConnection(opts: {
|
|
|
141
140
|
return;
|
|
142
141
|
|
|
143
142
|
case "send-ui-element-message": {
|
|
144
|
-
const modelId = msg.data.model_id;
|
|
145
143
|
const uiElement = msg.data.ui_element;
|
|
146
|
-
const message = msg.data.message;
|
|
147
|
-
const buffers = safeExtractSetUIElementMessageBuffers(msg.data);
|
|
148
|
-
|
|
149
|
-
if (modelId && isMessageWidgetState(message)) {
|
|
150
|
-
handleWidgetMessage({
|
|
151
|
-
modelId,
|
|
152
|
-
msg: message,
|
|
153
|
-
buffers,
|
|
154
|
-
modelManager: MODEL_MANAGER,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
144
|
if (uiElement) {
|
|
145
|
+
const buffers = safeExtractSetUIElementMessageBuffers(msg.data);
|
|
159
146
|
UI_ELEMENT_REGISTRY.broadcastMessage(
|
|
160
147
|
uiElement as UIElementId,
|
|
161
148
|
msg.data.message,
|
|
162
149
|
buffers,
|
|
163
150
|
);
|
|
164
151
|
}
|
|
165
|
-
|
|
166
152
|
return;
|
|
167
153
|
}
|
|
168
154
|
|
|
155
|
+
case "model-lifecycle":
|
|
156
|
+
handleWidgetMessage(MODEL_MANAGER, msg.data);
|
|
157
|
+
return;
|
|
158
|
+
|
|
169
159
|
case "remove-ui-elements":
|
|
170
160
|
handleRemoveUIElements(msg.data);
|
|
171
161
|
return;
|
|
@@ -101,10 +101,17 @@ function PluginSlotInternal<T>(
|
|
|
101
101
|
return plugin.validator.safeParse(parseDataset(hostElement));
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// Incremented on each reset to invalidate memoized function references.
|
|
105
|
+
// This ensures that plugin functions (e.g., search) are re-created when
|
|
106
|
+
// the underlying UI element instance changes (new object-id), even if
|
|
107
|
+
// the element's data attributes haven't changed.
|
|
108
|
+
const [resetNonce, setResetNonce] = useState(0);
|
|
109
|
+
|
|
104
110
|
useImperativeHandle(ref, () => ({
|
|
105
111
|
reset: () => {
|
|
106
112
|
setValue(getInitialValue());
|
|
107
113
|
setParsedResult(plugin.validator.safeParse(parseDataset(hostElement)));
|
|
114
|
+
setResetNonce((n) => n + 1);
|
|
108
115
|
},
|
|
109
116
|
setChildren: (children) => {
|
|
110
117
|
setChildNodes(children);
|
|
@@ -224,7 +231,8 @@ function PluginSlotInternal<T>(
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
return methods;
|
|
227
|
-
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
}, [plugin.functions, hostElement, resetNonce]);
|
|
228
236
|
|
|
229
237
|
// If we failed to parse the initial value, render an error
|
|
230
238
|
if (!parsedResult.success) {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
|
4
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import { Provider } from "jotai";
|
|
6
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import type { DownloadAsArgs } from "@/components/data-table/schemas";
|
|
8
|
+
import type { FieldTypesWithExternalType } from "@/components/data-table/types";
|
|
9
|
+
import { store } from "@/core/state/jotai";
|
|
10
|
+
import {
|
|
11
|
+
type GetDataUrl,
|
|
12
|
+
type GetRowIds,
|
|
13
|
+
LoadingDataTableComponent,
|
|
14
|
+
} from "../DataTablePlugin";
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
global.ResizeObserver = class ResizeObserver {
|
|
18
|
+
observe() {
|
|
19
|
+
// do nothing
|
|
20
|
+
}
|
|
21
|
+
unobserve() {
|
|
22
|
+
// do nothing
|
|
23
|
+
}
|
|
24
|
+
disconnect() {
|
|
25
|
+
// do nothing
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("LoadingDataTableComponent", () => {
|
|
31
|
+
/**
|
|
32
|
+
* Regression test for https://github.com/marimo-team/marimo/issues/8023
|
|
33
|
+
*
|
|
34
|
+
* When a table is replaced via mo.output.replace() with updated data,
|
|
35
|
+
* but the initial page data (unsorted first page) hasn't changed,
|
|
36
|
+
* the useAsyncData hook's deps may all remain the same.
|
|
37
|
+
* Previously, the `search` function reference was memoized on
|
|
38
|
+
* [plugin.functions, hostElement] and wouldn't change on reset(),
|
|
39
|
+
* so the useAsyncData effect wouldn't re-fire.
|
|
40
|
+
*
|
|
41
|
+
* The fix adds a resetNonce to the functionMethods memo deps,
|
|
42
|
+
* so when the plugin is reset (table instance changes), the search
|
|
43
|
+
* function reference changes, triggering useAsyncData to re-fetch.
|
|
44
|
+
*
|
|
45
|
+
* This test verifies that when the search function reference changes
|
|
46
|
+
* (simulating reset()), the component re-fetches data even if
|
|
47
|
+
* props.data hasn't changed.
|
|
48
|
+
*/
|
|
49
|
+
it("should refetch data when search function reference changes", async () => {
|
|
50
|
+
const host = document.createElement("div");
|
|
51
|
+
const setValue = vi.fn();
|
|
52
|
+
|
|
53
|
+
// The initial page data string - identical for both renders.
|
|
54
|
+
// This simulates the case where only a row on page 2 changed,
|
|
55
|
+
// so the first page data is the same.
|
|
56
|
+
const initialPageData = JSON.stringify([
|
|
57
|
+
{ id: 1, status: "pending", value: 10 },
|
|
58
|
+
{ id: 2, status: "pending", value: 20 },
|
|
59
|
+
{ id: 3, status: "pending", value: 30 },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const searchResult = {
|
|
63
|
+
data: [
|
|
64
|
+
{ id: 1, status: "pending", value: 10 },
|
|
65
|
+
{ id: 2, status: "pending", value: 20 },
|
|
66
|
+
{ id: 3, status: "pending", value: 30 },
|
|
67
|
+
],
|
|
68
|
+
total_rows: 4,
|
|
69
|
+
cell_styles: null,
|
|
70
|
+
cell_hover_texts: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const searchFn1 = vi.fn().mockResolvedValue(searchResult);
|
|
74
|
+
const searchFn2 = vi.fn().mockResolvedValue(searchResult);
|
|
75
|
+
|
|
76
|
+
const fieldTypes: FieldTypesWithExternalType = [
|
|
77
|
+
["id", ["integer", "integer"]],
|
|
78
|
+
["status", ["string", "string"]],
|
|
79
|
+
["value", ["integer", "integer"]],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const commonProps = {
|
|
83
|
+
label: null,
|
|
84
|
+
totalRows: 4,
|
|
85
|
+
pagination: true,
|
|
86
|
+
pageSize: 3,
|
|
87
|
+
selection: "single" as const,
|
|
88
|
+
showDownload: false,
|
|
89
|
+
showFilters: false,
|
|
90
|
+
showColumnSummaries: false as const,
|
|
91
|
+
showDataTypes: false,
|
|
92
|
+
showPageSizeSelector: false,
|
|
93
|
+
showColumnExplorer: false,
|
|
94
|
+
showRowExplorer: false,
|
|
95
|
+
showChartBuilder: false,
|
|
96
|
+
rowHeaders: [] as FieldTypesWithExternalType,
|
|
97
|
+
fieldTypes,
|
|
98
|
+
totalColumns: 3,
|
|
99
|
+
maxColumns: "all" as const,
|
|
100
|
+
hasStableRowId: false,
|
|
101
|
+
lazy: false,
|
|
102
|
+
host,
|
|
103
|
+
enableSearch: true,
|
|
104
|
+
value: [] as (number | string | { rowId: string; columnName?: string })[],
|
|
105
|
+
setValue,
|
|
106
|
+
download_as: vi.fn() as DownloadAsArgs,
|
|
107
|
+
get_column_summaries: vi.fn().mockResolvedValue({
|
|
108
|
+
data: null,
|
|
109
|
+
stats: {},
|
|
110
|
+
bin_values: {},
|
|
111
|
+
value_counts: {},
|
|
112
|
+
show_charts: false,
|
|
113
|
+
}),
|
|
114
|
+
get_data_url: vi.fn() as GetDataUrl,
|
|
115
|
+
get_row_ids: vi.fn() as GetRowIds,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
119
|
+
<Provider store={store}>
|
|
120
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
121
|
+
</Provider>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Render with first search function
|
|
125
|
+
const { rerender } = render(
|
|
126
|
+
<Wrapper>
|
|
127
|
+
<LoadingDataTableComponent
|
|
128
|
+
{...commonProps}
|
|
129
|
+
data={initialPageData}
|
|
130
|
+
search={searchFn1}
|
|
131
|
+
/>
|
|
132
|
+
</Wrapper>,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Wait for the table to render with data
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(screen.getAllByRole("row").length).toBeGreaterThan(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Search was called on initial load (fire-and-forget for canShowInitialPage)
|
|
141
|
+
expect(searchFn1).toHaveBeenCalled();
|
|
142
|
+
|
|
143
|
+
// Now rerender with the same data but a NEW search function reference.
|
|
144
|
+
// This simulates what happens after reset() when resetNonce increments
|
|
145
|
+
// and functionMethods is recreated.
|
|
146
|
+
await act(async () => {
|
|
147
|
+
rerender(
|
|
148
|
+
<Wrapper>
|
|
149
|
+
<LoadingDataTableComponent
|
|
150
|
+
{...commonProps}
|
|
151
|
+
data={initialPageData}
|
|
152
|
+
search={searchFn2}
|
|
153
|
+
/>
|
|
154
|
+
</Wrapper>,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// The new search function should be called because the search
|
|
159
|
+
// dependency changed in useAsyncData.
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(searchFn2).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -1,99 +1,58 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
|
|
4
|
-
import type { AnyWidget
|
|
5
|
-
import {
|
|
6
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
7
|
-
import useEvent from "react-use-event-hook";
|
|
4
|
+
import type { AnyWidget } from "@anywidget/types";
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
8
6
|
import { z } from "zod";
|
|
9
|
-
import { MarimoIncomingMessageEvent } from "@/core/dom/events";
|
|
10
7
|
import { asRemoteURL } from "@/core/runtime/config";
|
|
8
|
+
import { resolveVirtualFileURL } from "@/core/static/files";
|
|
9
|
+
import { isStaticNotebook } from "@/core/static/static-state";
|
|
11
10
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
type HTMLElementNotDerivedFromRef,
|
|
15
|
-
useEventListener,
|
|
16
|
-
} from "@/hooks/useEventListener";
|
|
11
|
+
import type { HTMLElementNotDerivedFromRef } from "@/hooks/useEventListener";
|
|
17
12
|
import { createPlugin } from "@/plugins/core/builder";
|
|
18
|
-
import { rpc } from "@/plugins/core/rpc";
|
|
19
13
|
import type { IPluginProps } from "@/plugins/types";
|
|
20
|
-
import {
|
|
21
|
-
decodeFromWire,
|
|
22
|
-
isWireFormat,
|
|
23
|
-
serializeBuffersToBase64,
|
|
24
|
-
type WireFormat,
|
|
25
|
-
} from "@/utils/data-views";
|
|
26
14
|
import { prettyError } from "@/utils/errors";
|
|
27
|
-
import type { Base64String } from "@/utils/json/base64";
|
|
28
15
|
import { Logger } from "@/utils/Logger";
|
|
29
16
|
import { ErrorBanner } from "../common/error-banner";
|
|
30
|
-
import { MODEL_MANAGER, Model } from "./model";
|
|
17
|
+
import { getMarimoInternal, MODEL_MANAGER, type Model } from "./model";
|
|
18
|
+
import type { ModelState, WidgetModelId } from "./types";
|
|
31
19
|
|
|
20
|
+
/**
|
|
21
|
+
* AnyWidget asset data
|
|
22
|
+
*/
|
|
32
23
|
interface Data {
|
|
33
24
|
jsUrl: string;
|
|
34
25
|
jsHash: string;
|
|
35
26
|
css?: string | null;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
41
|
-
type PluginFunctions = {
|
|
42
|
-
send_to_widget: <T>(req: {
|
|
43
|
-
content: unknown;
|
|
44
|
-
buffers: Base64String[];
|
|
45
|
-
}) => Promise<null | undefined>;
|
|
46
|
-
};
|
|
29
|
+
type AnyWidgetState = ModelState;
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.withFunctions<PluginFunctions>({
|
|
57
|
-
send_to_widget: rpc
|
|
58
|
-
.input(
|
|
59
|
-
z.object({
|
|
60
|
-
content: z.unknown(),
|
|
61
|
-
buffers: z.array(z.string().transform((v) => v as Base64String)),
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
.output(z.null().optional()),
|
|
65
|
-
})
|
|
66
|
-
.renderer((props) => <AnyWidgetSlot {...props} />);
|
|
67
|
-
|
|
68
|
-
const AnyWidgetSlot = (
|
|
69
|
-
props: IPluginProps<WireFormat<T>, Data, PluginFunctions>,
|
|
70
|
-
) => {
|
|
71
|
-
const { css, jsUrl, jsHash } = props.data;
|
|
31
|
+
/**
|
|
32
|
+
* Initial value is a model_id reference.
|
|
33
|
+
* The backend sends just { model_id: string } and the frontend
|
|
34
|
+
* retrieves the actual state from the 'open' message.
|
|
35
|
+
*/
|
|
36
|
+
interface ModelIdRef {
|
|
37
|
+
model_id: WidgetModelId;
|
|
38
|
+
}
|
|
72
39
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
if (isWireFormat(props.value)) {
|
|
76
|
-
const decoded = decodeFromWire(props.value);
|
|
77
|
-
Logger.debug("AnyWidget decoded wire format:", {
|
|
78
|
-
bufferPaths: props.value.bufferPaths,
|
|
79
|
-
buffersCount: props.value.buffers?.length,
|
|
80
|
-
decodedKeys: Object.keys(decoded),
|
|
81
|
-
});
|
|
82
|
-
return decoded;
|
|
83
|
-
}
|
|
84
|
-
Logger.warn("AnyWidget value is not wire format:", props.value);
|
|
85
|
-
return props.value;
|
|
86
|
-
}, [props.value]);
|
|
40
|
+
export function useAnyWidgetModule(opts: { jsUrl: string; jsHash: string }) {
|
|
41
|
+
const { jsUrl, jsHash } = opts;
|
|
87
42
|
|
|
88
43
|
// JS is an ESM file with a render function on it
|
|
89
44
|
// export function render({ model, el }) {
|
|
90
45
|
// ...
|
|
91
46
|
const {
|
|
92
|
-
data:
|
|
47
|
+
data: jsModule,
|
|
93
48
|
error,
|
|
94
49
|
refetch,
|
|
95
50
|
} = useAsyncData(async () => {
|
|
96
|
-
|
|
51
|
+
let url = asRemoteURL(jsUrl).toString();
|
|
52
|
+
// In static notebooks, resolve virtual files to blob URLs for import()
|
|
53
|
+
if (isStaticNotebook()) {
|
|
54
|
+
url = resolveVirtualFileURL(url);
|
|
55
|
+
}
|
|
97
56
|
return await import(/* @vite-ignore */ url);
|
|
98
57
|
// Re-render on jsHash change (which is a hash of the contents of the file)
|
|
99
58
|
// instead of a jsUrl change because URLs may change without the contents
|
|
@@ -111,9 +70,16 @@ const AnyWidgetSlot = (
|
|
|
111
70
|
}
|
|
112
71
|
}, [hasError, jsUrl]);
|
|
113
72
|
|
|
73
|
+
return {
|
|
74
|
+
jsModule,
|
|
75
|
+
error,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useMountCss(css: string | null | undefined, host: HTMLElement) {
|
|
114
80
|
// Mount the CSS
|
|
115
81
|
useEffect(() => {
|
|
116
|
-
const shadowRoot =
|
|
82
|
+
const shadowRoot = host.shadowRoot;
|
|
117
83
|
if (!css || !shadowRoot) {
|
|
118
84
|
return;
|
|
119
85
|
}
|
|
@@ -150,23 +116,38 @@ const AnyWidgetSlot = (
|
|
|
150
116
|
return () => {
|
|
151
117
|
style.remove();
|
|
152
118
|
};
|
|
153
|
-
}, [css,
|
|
119
|
+
}, [css, host]);
|
|
120
|
+
}
|
|
154
121
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
122
|
+
export const AnyWidgetPlugin = createPlugin<ModelIdRef>("marimo-anywidget")
|
|
123
|
+
.withData(
|
|
124
|
+
z.object({
|
|
125
|
+
jsUrl: z.string(),
|
|
126
|
+
jsHash: z.string(),
|
|
127
|
+
css: z.string().nullish(),
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
.withFunctions({})
|
|
131
|
+
.renderer((props) => <AnyWidgetSlot {...props} />);
|
|
132
|
+
|
|
133
|
+
const AnyWidgetSlot = (props: IPluginProps<ModelIdRef, Data>) => {
|
|
134
|
+
const { css, jsUrl, jsHash } = props.data;
|
|
135
|
+
const { model_id: modelId } = props.value;
|
|
136
|
+
const host = props.host as HTMLElementNotDerivedFromRef;
|
|
137
|
+
|
|
138
|
+
const { jsModule, error } = useAnyWidgetModule({ jsUrl, jsHash });
|
|
139
|
+
|
|
140
|
+
useMountCss(css, host);
|
|
160
141
|
|
|
161
142
|
if (error) {
|
|
162
143
|
return <ErrorBanner error={error} />;
|
|
163
144
|
}
|
|
164
145
|
|
|
165
|
-
if (!
|
|
146
|
+
if (!jsModule) {
|
|
166
147
|
return null;
|
|
167
148
|
}
|
|
168
149
|
|
|
169
|
-
if (!isAnyWidgetModule(
|
|
150
|
+
if (!isAnyWidgetModule(jsModule)) {
|
|
170
151
|
const error = new Error(
|
|
171
152
|
`Module at ${jsUrl} does not appear to be a valid anywidget`,
|
|
172
153
|
);
|
|
@@ -183,10 +164,8 @@ const AnyWidgetSlot = (
|
|
|
183
164
|
// Plugins may be stateful and we cannot make assumptions that we won't be
|
|
184
165
|
// so it is safer to just re-render.
|
|
185
166
|
key={key}
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
setValue={wrappedSetValue}
|
|
189
|
-
value={valueWithBuffers}
|
|
167
|
+
widget={jsModule.default}
|
|
168
|
+
modelId={modelId}
|
|
190
169
|
/>
|
|
191
170
|
);
|
|
192
171
|
};
|
|
@@ -194,125 +173,72 @@ const AnyWidgetSlot = (
|
|
|
194
173
|
/**
|
|
195
174
|
* Run the anywidget module
|
|
196
175
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
176
|
+
* Per AFM spec (anywidget.dev/en/afm):
|
|
177
|
+
* - initialize() is called once per model lifetime
|
|
178
|
+
* - render() is called once per view (can be multiple per model)
|
|
199
179
|
*/
|
|
200
|
-
async function runAnyWidgetModule(
|
|
201
|
-
widgetDef: AnyWidget
|
|
180
|
+
async function runAnyWidgetModule<T extends AnyWidgetState>(
|
|
181
|
+
widgetDef: AnyWidget<T>,
|
|
202
182
|
model: Model<T>,
|
|
203
183
|
el: HTMLElement,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
invoke: async (_name, _msg, _options) => {
|
|
207
|
-
const message =
|
|
208
|
-
"anywidget.invoke not supported in marimo. Please file an issue at https://github.com/marimo-team/marimo/issues";
|
|
209
|
-
Logger.warn(message);
|
|
210
|
-
throw new Error(message);
|
|
211
|
-
},
|
|
212
|
-
};
|
|
184
|
+
signal: AbortSignal,
|
|
185
|
+
): Promise<void> {
|
|
213
186
|
// Clear the element, in case the widget is re-rendering
|
|
214
187
|
el.innerHTML = "";
|
|
215
|
-
|
|
216
|
-
typeof widgetDef === "function" ? await widgetDef() : widgetDef;
|
|
217
|
-
await widget.initialize?.({ model, experimental });
|
|
188
|
+
|
|
218
189
|
try {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
unsub?.();
|
|
222
|
-
};
|
|
190
|
+
const render = await getMarimoInternal(model).resolveWidget(widgetDef);
|
|
191
|
+
await render(el, signal);
|
|
223
192
|
} catch (error) {
|
|
224
193
|
Logger.error("Error rendering anywidget", error);
|
|
225
194
|
el.classList.add("text-error");
|
|
226
195
|
el.innerHTML = `Error rendering anywidget: ${prettyError(error)}`;
|
|
227
|
-
return () => {
|
|
228
|
-
// No-op
|
|
229
|
-
};
|
|
230
196
|
}
|
|
231
197
|
}
|
|
232
198
|
|
|
233
199
|
function isAnyWidgetModule(mod: any): mod is { default: AnyWidget } {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
mod.default?.render ||
|
|
238
|
-
mod.default?.initialize)
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function getDirtyFields(value: T, initialValue: T): Set<keyof T> {
|
|
243
|
-
return new Set(
|
|
244
|
-
Object.keys(value).filter((key) => !isEqual(value[key], initialValue[key])),
|
|
245
|
-
);
|
|
246
|
-
}
|
|
200
|
+
if (!mod.default) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
247
203
|
|
|
248
|
-
function hasModelId(message: unknown): message is { model_id: string } {
|
|
249
204
|
return (
|
|
250
|
-
typeof
|
|
205
|
+
typeof mod.default === "function" ||
|
|
206
|
+
typeof mod.default?.render === "function" ||
|
|
207
|
+
typeof mod.default?.initialize === "function"
|
|
251
208
|
);
|
|
252
209
|
}
|
|
253
210
|
|
|
254
|
-
interface Props
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
value: T;
|
|
258
|
-
setValue: (value: Partial<T>) => void;
|
|
211
|
+
interface Props<T extends AnyWidgetState> {
|
|
212
|
+
widget: AnyWidget<T>;
|
|
213
|
+
modelId: WidgetModelId;
|
|
259
214
|
}
|
|
260
215
|
|
|
261
|
-
const LoadedSlot = ({
|
|
262
|
-
value,
|
|
263
|
-
setValue,
|
|
216
|
+
const LoadedSlot = <T extends AnyWidgetState>({
|
|
264
217
|
widget,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
host,
|
|
268
|
-
}: Props & { widget: AnyWidget }) => {
|
|
218
|
+
modelId,
|
|
219
|
+
}: Props<T> & { widget: AnyWidget<T> }) => {
|
|
269
220
|
const htmlRef = useRef<HTMLDivElement>(null);
|
|
270
221
|
|
|
271
|
-
// value is already decoded from wire format
|
|
272
|
-
const model =
|
|
273
|
-
new Model(value, setValue, functions.send_to_widget, new Set()),
|
|
274
|
-
);
|
|
222
|
+
// value is already decoded from wire format, may be null if waiting for open message
|
|
223
|
+
const model = MODEL_MANAGER.getSync(modelId);
|
|
275
224
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
MarimoIncomingMessageEvent.TYPE,
|
|
280
|
-
(e) => {
|
|
281
|
-
const message = e.detail.message;
|
|
282
|
-
if (hasModelId(message)) {
|
|
283
|
-
MODEL_MANAGER.get(message.model_id).then((model) => {
|
|
284
|
-
model.receiveCustomMessage(message, e.detail.buffers);
|
|
285
|
-
});
|
|
286
|
-
} else {
|
|
287
|
-
model.current.receiveCustomMessage(message, e.detail.buffers);
|
|
288
|
-
}
|
|
289
|
-
},
|
|
290
|
-
);
|
|
225
|
+
if (!model) {
|
|
226
|
+
Logger.error("Model not found for modelId", modelId);
|
|
227
|
+
}
|
|
291
228
|
|
|
292
229
|
useEffect(() => {
|
|
293
|
-
if (!htmlRef.current) {
|
|
230
|
+
if (!htmlRef.current || !model) {
|
|
294
231
|
return;
|
|
295
232
|
}
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
return () => {
|
|
302
|
-
unsubPromise.then((unsub) => unsub());
|
|
303
|
-
};
|
|
304
|
-
// We re-run the widget when the jsUrl changes, which means the cell
|
|
233
|
+
const controller = new AbortController();
|
|
234
|
+
runAnyWidgetModule(widget, model, htmlRef.current, controller.signal);
|
|
235
|
+
return () => controller.abort();
|
|
236
|
+
// We re-run the widget when the modelId changes, which means the cell
|
|
305
237
|
// that created the Widget has been re-run.
|
|
306
238
|
// We need to re-run the widget because it may contain initialization code
|
|
307
239
|
// that could be reset by the new widget.
|
|
308
240
|
// See example: https://github.com/marimo-team/marimo/issues/3962#issuecomment-2703184123
|
|
309
|
-
}, [widget,
|
|
310
|
-
|
|
311
|
-
// When the value changes, update the model
|
|
312
|
-
const valueMemo = useDeepCompareMemoize(value);
|
|
313
|
-
useEffect(() => {
|
|
314
|
-
model.current.updateAndEmitDiffs(valueMemo);
|
|
315
|
-
}, [valueMemo]);
|
|
241
|
+
}, [widget, modelId, model]);
|
|
316
242
|
|
|
317
243
|
return <div ref={htmlRef} />;
|
|
318
244
|
};
|
|
@@ -321,5 +247,4 @@ export const visibleForTesting = {
|
|
|
321
247
|
LoadedSlot,
|
|
322
248
|
runAnyWidgetModule,
|
|
323
249
|
isAnyWidgetModule,
|
|
324
|
-
getDirtyFields,
|
|
325
250
|
};
|