`
460 | which can affect the layout of your application. When ipyreact provides React, we can build a true ReactJS application with a normal/true
461 | React render tree.
462 |
463 | ## I get a React error
464 |
465 | For instance, if you see `"Cannot read properties of null (reading 'useReducer')"` it means that you are loading in your own ReactJS version.
466 |
467 | If you use https://esh.sh, make sure you add `??external=react,react-dom` at the end of the url, so that your esm bundle doesn't include its own
468 | ReactJS version, but uses the one provided with ipyreact.
469 |
470 | If you make your own bundle using esbuild, make sure to add the `--external:react --external:react-dom` flags on the CLI.
471 |
--------------------------------------------------------------------------------
/src/widget.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Maarten A. Breddels
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import {
5 | WidgetModel,
6 | DOMWidgetModel,
7 | DOMWidgetView,
8 | ISerializers,
9 | unpack_models,
10 | } from "@jupyter-widgets/base";
11 |
12 | import * as React from "react";
13 | import { useEffect, useState } from "react";
14 | import * as ReactJsxRuntime from "react/jsx-runtime";
15 | import * as ReactReconcilerContants from "react-reconciler/constants";
16 | import * as ReactReconciler from "react-reconciler";
17 | import * as ReactDOM from "react-dom";
18 | // @ts-ignore
19 | import * as ReactDOMClient from "react-dom/client";
20 | // @ts-ignore
21 | import "../css/widget.css";
22 | import { eventToObject, expose, loadScript, setUpMuiFixModule } from "./utils";
23 | import { MODULE_NAME, MODULE_VERSION } from "./version";
24 | // import * as Babel from '@babel/standalone';
25 | // TODO: find a way to ship es-module-shims with the widget
26 | // @ts-ignore
27 | // import 'es-module-shims';
28 | import { transform } from "sucrase";
29 | import { ErrorBoundary, JupyterWidget } from "./components";
30 | import { Root } from "react-dom/client";
31 | import { ModelDestroyOptions } from "backbone";
32 | import { isEqual } from "lodash";
33 |
34 | declare function importShim
(
35 | specifier: string,
36 | parentUrl?: string,
37 | ): Promise<{ default: Default } & Exports>;
38 |
39 | declare namespace importShim {
40 | const resolve: (id: string, parentURL?: string) => string;
41 | const addImportMap: (importMap: Partial) => void;
42 | const getImportMap: () => any;
43 | }
44 |
45 | const moduleFunctions: any = {};
46 | const modules: any = {};
47 |
48 | function provideModule(moduleName: string, module: any) {
49 | if (module instanceof Error) {
50 | if (moduleFunctions[moduleName]) {
51 | moduleFunctions[moduleName].reject(module);
52 | } else {
53 | modules[moduleName] = Promise.reject(module);
54 | }
55 | } else {
56 | if (moduleFunctions[moduleName]) {
57 | moduleFunctions[moduleName].resolve(module);
58 | } else {
59 | modules[moduleName] = Promise.resolve(module);
60 | }
61 | }
62 | }
63 |
64 | function requestModule(moduleName: string) {
65 | if (!modules[moduleName]) {
66 | modules[moduleName] = new Promise((resolve, reject) => {
67 | moduleFunctions[moduleName] = { resolve, reject };
68 | });
69 | }
70 | return modules[moduleName];
71 | }
72 |
73 | let importMapConfigurationResolver: any = null;
74 | let importMapConfigurationPromise: any = null;
75 |
76 | function provideImportMapConfiguration() {
77 | if (importMapConfigurationResolver) {
78 | importMapConfigurationResolver();
79 | } else {
80 | importMapConfigurationPromise = Promise.resolve();
81 | }
82 | }
83 |
84 | function requestImportMapConfiguration() {
85 | if (!importMapConfigurationPromise) {
86 | importMapConfigurationPromise = new Promise((resolve) => {
87 | importMapConfigurationResolver = resolve;
88 | });
89 | }
90 | }
91 |
92 | // @ts-ignore
93 | // const react16Code = require('!!raw-loader!./react16.js');
94 | // import react16Code from 'raw-loader!./react16.mjs';
95 | // console.log(react16Code)
96 |
97 | // this will do for now
98 | let importShimLoaded: any = null;
99 | async function ensureImportShimLoaded() {
100 | if (importShimLoaded == null) {
101 | importShimLoaded = loadScript(
102 | "module",
103 | "https://ga.jspm.io/npm:es-module-shims@1.7.0/dist/es-module-shims.js",
104 | );
105 | }
106 | return await importShimLoaded;
107 | }
108 |
109 | // function autoExternalReactResolve(
110 | // id: string,
111 | // parentUrl: string,
112 | // resolve: (arg0: any, arg1: any) => any,
113 | // ) {
114 | // const shipsWith =
115 | // id == "react" ||
116 | // id == "react-dom" ||
117 | // id == "react/jsx-runtime" ||
118 | // id == "react-dom/client" ||
119 | // id == "react-reconciler" ||
120 | // id == "react-reconciler/constants";
121 | // const alreadyPatched = parentUrl.includes("?external=react,react-dom");
122 | // const parentIsEsmSh = parentUrl.startsWith("https://esm.sh/");
123 | // const isBlob = id.startsWith("blob:");
124 | // if (!shipsWith && !id.includes("://") && !parentIsEsmSh) {
125 | // id = "https://esm.sh/" + id;
126 | // }
127 | // if (!shipsWith && !alreadyPatched && !isBlob) {
128 | // id = id + "?external=react,react-dom";
129 | // }
130 | // return resolve(id, parentUrl);
131 | // }
132 |
133 | // @ts-ignore
134 | window.esmsInitOptions = {
135 | shimMode: true,
136 | mapOverrides: true,
137 | // resolve: (
138 | // id: string,
139 | // parentUrl: string,
140 | // resolve: (id: string, parentUrl: string) => any,
141 | // ) => autoExternalReactResolve(id, parentUrl, resolve),
142 | };
143 |
144 | let react18ESMUrls: any = null;
145 | let react16ESMUrls: any = null;
146 |
147 | function ensureReactSetup(version: number) {
148 | if (version == 18) {
149 | if (react18ESMUrls == null) {
150 | react18ESMUrls = {
151 | react: expose(React),
152 | "react-dom": expose(ReactDOM),
153 | "react/jsx-runtime": expose(ReactJsxRuntime),
154 | "react-dom/client": expose(ReactDOMClient),
155 | "react-reconciler": expose(ReactReconciler),
156 | "react-reconciler/constants": expose(ReactReconcilerContants),
157 | };
158 | }
159 | return react18ESMUrls;
160 | } else if (version == 16) {
161 | if (react16ESMUrls == null) {
162 | // react16ESMUrls = {urlReact: expose(React16), urlReactDom: expose(ReactDOM16)};
163 | }
164 | return react16ESMUrls;
165 | }
166 | }
167 |
168 | class ComponentData {
169 | component: any;
170 | key: string;
171 |
172 | constructor(component: any, key: string) {
173 | this.component = component;
174 | this.key = key;
175 | }
176 |
177 | asElement(view: DOMWidgetView) {
178 | return React.createElement(this.component, { view, key: this.key });
179 | }
180 | }
181 |
182 | const widgetToReactComponent = async (widget: WidgetModel) => {
183 | if (widget instanceof ReactModel) {
184 | return new ComponentData(await widget.component, widget.model_id);
185 | } else {
186 | return new ComponentData(
187 | ({ view }: { view: DOMWidgetView }) => JupyterWidget({ widget, view }),
188 | widget.model_id,
189 | );
190 | }
191 | };
192 |
193 | const isPlainObject = (value: any) =>
194 | value && [undefined, Object].includes(value.constructor);
195 |
196 | const entriesToObj = (acc: any, [key, value]: any[]) => {
197 | acc[key] = value;
198 | return acc;
199 | };
200 |
201 | async function replaceWidgetWithComponent(
202 | data: any,
203 | get_model: (model_id: string) => Promise,
204 | ): Promise {
205 | const type = typeof data;
206 | if (type === "string" && data.startsWith("IPY_MODEL_")) {
207 | const modelId = data.substring("IPY_MODEL_".length);
208 | const model = await get_model(modelId);
209 | return widgetToReactComponent(model);
210 | }
211 | if (
212 | ["string", "number", "boolean", "bigint"].includes(type) ||
213 | data == null
214 | ) {
215 | return data;
216 | }
217 | if (data instanceof WidgetModel) {
218 | return widgetToReactComponent(data);
219 | }
220 | if (Array.isArray(data)) {
221 | return Promise.all(
222 | data.map(async (d) => replaceWidgetWithComponent(d, get_model)),
223 | );
224 | }
225 | if (isPlainObject(data)) {
226 | return (
227 | await Promise.all(
228 | Object.entries(data).map(async ([key, value]) => [
229 | key,
230 | await replaceWidgetWithComponent(value, get_model),
231 | ]),
232 | )
233 | ).reduce(entriesToObj, {});
234 | }
235 | return data;
236 | }
237 |
238 | function replaceComponentWithElement(data: any, view: DOMWidgetView): any {
239 | const type = typeof data;
240 | if (
241 | ["string", "number", "boolean", "bigint"].includes(type) ||
242 | data == null
243 | ) {
244 | return data;
245 | }
246 | if (data instanceof ComponentData) {
247 | return data.asElement(view);
248 | }
249 | if (Array.isArray(data)) {
250 | return data.map((d) => replaceComponentWithElement(d, view));
251 | }
252 | if (isPlainObject(data)) {
253 | const entriesToObj = (acc: any, [key, value]: any[]) => {
254 | acc[key] = value;
255 | return acc;
256 | };
257 | return Object.entries(data)
258 | .map(([key, value]) => [key, replaceComponentWithElement(value, view)])
259 | .reduce(entriesToObj, {});
260 | }
261 | return data;
262 | }
263 |
264 | export class Module extends WidgetModel {
265 | defaults() {
266 | return {
267 | ...super.defaults(),
268 | _model_name: Module.model_name,
269 | _model_module: Module.model_module,
270 | _model_module_version: Module.model_module_version,
271 | _view_name: Module.view_name,
272 | _view_module: Module.view_module,
273 | _view_module_version: Module.view_module_version,
274 | };
275 | }
276 | initialize(attributes: any, options: any): void {
277 | super.initialize(attributes, options);
278 | this.addModule();
279 | }
280 | destroy(options?: any): any {
281 | if (this.codeUrl) {
282 | URL.revokeObjectURL(this.codeUrl);
283 | }
284 | return super.destroy(options);
285 | }
286 | async updateImportMap() {
287 | await ensureImportShimLoaded();
288 | await requestImportMapConfiguration();
289 | const reactImportMap = ensureReactSetup(this.get("react_version"));
290 | const importMap = {
291 | imports: {
292 | ...reactImportMap,
293 | },
294 | };
295 | importShim.addImportMap(importMap);
296 | }
297 | async addModule() {
298 | const code = this.get("code");
299 | let name = this.get("name");
300 | try {
301 | if (this.codeUrl) {
302 | URL.revokeObjectURL(this.codeUrl);
303 | }
304 | this.codeUrl = URL.createObjectURL(
305 | new Blob([code], { type: "text/javascript" }),
306 | );
307 | let dependencies = this.get("dependencies") || [];
308 | this.set(
309 | "status",
310 | "Waiting for dependencies: " + dependencies.join(", "),
311 | );
312 | await Promise.all(dependencies.map((x: any) => requestModule(x)));
313 | await ensureImportShimLoaded();
314 | await this.updateImportMap();
315 | this.set("status", "Loading module...");
316 | let module = await importShim(this.codeUrl);
317 | importShim.addImportMap({ imports: { [name]: this.codeUrl } });
318 | this.set("status", "Loaded module!");
319 | provideModule(name, module);
320 | } catch (e) {
321 | console.error(e);
322 | provideModule(name, e);
323 | this.set("status", "Error loading module: " + e);
324 | }
325 | }
326 |
327 | static model_name = "Module";
328 | static model_module = MODULE_NAME;
329 | static model_module_version = MODULE_VERSION;
330 | static view_name = "ModuleView"; // Set to null if no view
331 | static view_module = MODULE_NAME; // Set to null if no view
332 | static view_module_version = MODULE_VERSION;
333 | private codeUrl: string | null;
334 | }
335 |
336 | export class ModuleView extends DOMWidgetView {
337 | private root: Root | null = null;
338 |
339 | async render() {
340 | this.el.classList.add("jupyter-react-widget");
341 | this.root = ReactDOMClient.createRoot(this.el);
342 | const Component = () => {
343 | const [status, setStatus] = useState(this.model.get("status"));
344 | useEffect(() => {
345 | this.listenTo(this.model, "change:status", () => {
346 | setStatus(this.model.get("status"));
347 | });
348 | return () => {
349 | this.stopListening(this.model, "change:status");
350 | };
351 | }, []);
352 | const name = this.model.get("name");
353 | return (
354 |
355 | {name} status: {status}
356 |
357 | );
358 | };
359 | this.root.render();
360 | }
361 |
362 | remove() {
363 | this.root?.unmount();
364 | }
365 | }
366 |
367 | export class ImportMap extends WidgetModel {
368 | defaults() {
369 | return {
370 | ...super.defaults(),
371 | _model_name: ImportMap.model_name,
372 | _model_module: ImportMap.model_module,
373 | _model_module_version: ImportMap.model_module_version,
374 | _view_name: ImportMap.view_name,
375 | _view_module: ImportMap.view_module,
376 | _view_module_version: ImportMap.view_module_version,
377 | import_map: {
378 | imports: {},
379 | scopes: {},
380 | },
381 | };
382 | }
383 | initialize(attributes: any, options: any): void {
384 | super.initialize(attributes, options);
385 | this.updateImportMap();
386 | this.on("change:import_map", () => {
387 | this.updateImportMap();
388 | });
389 | }
390 | destroy(options?: any): any {
391 | this.off("change:import_map");
392 | return super.destroy(options);
393 | }
394 | async updateImportMap() {
395 | await ensureImportShimLoaded();
396 | const importMapWidget = this.get("import_map");
397 | const importMap = {
398 | imports: {
399 | ...importMapWidget.imports,
400 | },
401 | scopes: {
402 | ...importMapWidget.scopes,
403 | },
404 | };
405 | importShim.addImportMap(importMap);
406 | provideImportMapConfiguration();
407 | }
408 |
409 | static model_name = "ImportMap";
410 | static model_module = MODULE_NAME;
411 | static model_module_version = MODULE_VERSION;
412 | static view_name = "ImportMap"; // Set to null if no view
413 | static view_module = MODULE_NAME; // Set to null if no view
414 | static view_module_version = MODULE_VERSION;
415 | }
416 |
417 | export class ImportMapView extends DOMWidgetView {
418 | private root: Root | null = null;
419 |
420 | async render() {
421 | this.el.classList.add("jupyter-react-widget");
422 | this.root = ReactDOMClient.createRoot(this.el);
423 | const Component = () => {
424 | const [importMap, setImportMap] = useState(this.model.get("import_map"));
425 | useEffect(() => {
426 | this.listenTo(this.model, "change:import_map", () => {
427 | setImportMap(this.model.get("import_map"));
428 | });
429 | return () => {
430 | this.stopListening(this.model, "change:import_map");
431 | };
432 | }, []);
433 | const importMapJson = JSON.stringify(importMap, null, 2);
434 | return (
435 |
436 | importmap:
437 | {importMapJson}
438 |
439 | );
440 | };
441 | this.root.render();
442 | }
443 |
444 | remove() {
445 | this.root?.unmount();
446 | }
447 | }
448 |
449 | export class ReactModel extends DOMWidgetModel {
450 | defaults() {
451 | return {
452 | ...super.defaults(),
453 | _model_name: ReactModel.model_name,
454 | _model_module: ReactModel.model_module,
455 | _model_module_version: ReactModel.model_module_version,
456 | _view_name: ReactModel.view_name,
457 | _view_module: ReactModel.view_module,
458 | _view_module_version: ReactModel.view_module_version,
459 | };
460 | // TODO: ideally, we only compile code in the widget model, but the react hooks are
461 | // super convenient.
462 | }
463 |
464 | static serializers: ISerializers = {
465 | ...DOMWidgetModel.serializers,
466 | children: { deserialize: unpack_models as any },
467 | };
468 |
469 | initialize(attributes: any, options: any): void {
470 | super.initialize(attributes, options);
471 | this.component = new Promise((resolve, reject) => {
472 | this.resolveComponent = resolve;
473 | this.rejectComponent = reject;
474 | });
475 | this.queue = Promise.resolve();
476 | this.on("change:_esm", async () => {
477 | this.enqueue(async () => {
478 | this.compileCode();
479 | await this.updateComponentToWrap();
480 | });
481 | });
482 | this.on("change:_module change:_type", async () => {
483 | this.enqueue(async () => {
484 | await this.updateImportMap();
485 | await this.updateComponentToWrap();
486 | });
487 | });
488 | this._initialSetup();
489 | }
490 | enqueue(fn: () => Promise) {
491 | // this makes sure that callbacks and _initialSetup are executed in order
492 | // and not in parallel, which can lead to race conditions
493 | this.queue = this.queue.then(async () => {
494 | await fn();
495 | });
496 | return this.queue;
497 | }
498 | async _initialSetup() {
499 | await this.enqueue(async () => {
500 | await this.updateImportMap();
501 | this.compileCode();
502 | try {
503 | let component: any = await this.createWrapperComponent();
504 | this.resolveComponent(component);
505 | } catch (e) {
506 | console.error(e);
507 | this.rejectComponent(e);
508 | }
509 | });
510 | // await this.createComponen();
511 | }
512 | async updateImportMap() {
513 | await ensureImportShimLoaded();
514 | await requestImportMapConfiguration();
515 | const reactImportMap = ensureReactSetup(this.get("_react_version"));
516 | const importMap = {
517 | imports: {
518 | ...reactImportMap,
519 | },
520 | };
521 | importShim.addImportMap(importMap);
522 | }
523 | compileCode() {
524 | // using babel is a bit of an art, so leaving this code for if we
525 | // want to switch back to babel. However, babel is very large compared
526 | // to sucrase
527 | // Babel.registerPreset("my-preset", {
528 | // presets: [
529 | // [Babel.availablePresets["react"]],
530 | // // [Babel.availablePresets["typescript"], { allExtensions: true }],
531 | // ]
532 | // });
533 | // Babel.registerPlugin("importmap", pluginImport());
534 | const code = this.get("_esm");
535 | this.compileError = null;
536 | if (!code) {
537 | this.compiledCode = null;
538 | return;
539 | }
540 | if (this.get("_debug")) {
541 | console.log("original code:\n", code);
542 | }
543 | try {
544 | // using babel:
545 | // return Babel.transform(code, { presets: ["react", "es2017"], plugins: ["importmap"] }).code;
546 | // using sucrase:
547 | this.compiledCode = transform(code, {
548 | transforms: ["jsx", "typescript"],
549 | filePath: "test.tsx",
550 | }).code;
551 | if (this.get("_debug")) {
552 | console.log("compiledCode:\n", this.compiledCode);
553 | }
554 | } catch (e) {
555 | console.error(e);
556 | this.compileError = e;
557 | }
558 | }
559 | async updateComponentToWrap() {
560 | try {
561 | let component: any = await this.createComponentToWrap();
562 | this.currentComponentToWrapOrError = component;
563 | this.trigger("component", component);
564 | } catch (e) {
565 | console.error(e);
566 | this.trigger("component", e);
567 | }
568 | }
569 | async createComponentToWrap() {
570 | let moduleName = this.get("_module");
571 | let type = this.get("_type");
572 | let _dependencies = this.get("_dependencies") || [];
573 | await Promise.all(_dependencies.map((x: any) => requestModule(x)));
574 | if (this.compileError) {
575 | return () => {this.compileError.message};
576 | } else {
577 | let module: any = null;
578 | // html element like div or button
579 | if (!moduleName && !this.compiledCode && type) {
580 | return type;
581 | }
582 |
583 | if (!this.compiledCode && !moduleName && !type) {
584 | return () => (
585 | no component provided, pass _esm, or _module and _type
586 | );
587 | } else if (this.compiledCode) {
588 | if (this.codeUrl) {
589 | URL.revokeObjectURL(this.codeUrl);
590 | }
591 | this.codeUrl = URL.createObjectURL(
592 | new Blob([this.compiledCode], { type: "text/javascript" }),
593 | );
594 | module = await importShim(this.codeUrl);
595 | if (!module) {
596 | throw new Error(`Error loading module`);
597 | }
598 | } else {
599 | module = await importShim(moduleName);
600 | if (!module) {
601 | throw new Error(`no module found with name ${moduleName}`);
602 | }
603 | }
604 | let component = module[type || "default"];
605 | if (!component) {
606 | if (type) {
607 | throw new Error(`no component ${type} found in module ${moduleName}`);
608 | } else {
609 | throw new Error(`
610 | no component found in module ${moduleName} (it should be exported as default)`);
611 | }
612 | } else {
613 | if (this.compiledCode) {
614 | const needsMuiFix = this.compiledCode.indexOf("@mui") !== -1;
615 | if (needsMuiFix) {
616 | let muiFix = await setUpMuiFixModule();
617 | const componentToWrap = component;
618 | // console.log("muiFix", muiFix);
619 | // @ts-ignore
620 | component = (props: any) => {
621 | // console.log("component wrapper fix", props)
622 | // return componentToWrap(props);
623 | return muiFix.styleWrapper(componentToWrap(props));
624 | };
625 | }
626 | }
627 | return component;
628 | }
629 | }
630 | }
631 | async createWrapperComponent() {
632 | // we wrap the component in a wrapper that puts in all the props from the
633 | // widget model, and handles events, etc
634 |
635 | const get_model = this.widget_manager.get_model.bind(this.widget_manager);
636 | let initialChildrenComponents = await replaceWidgetWithComponent(
637 | { children: this.get("children") },
638 | get_model,
639 | );
640 | // const resolveFormatters = async () => {
641 | // let formatterDict = this.get("formatters") || {};
642 | // let formatterModules : any = {};
643 | // for (const key of Object.keys(formatterDict)) {
644 | // // @ts-ignore
645 | // let module = await importShim(formatterDict[key]);
646 | // formatterModules[key] = module;
647 | // }
648 | // return formatterModules;
649 | // }
650 |
651 | // let formatterModules = await resolveFormatters();
652 | // console.log("formatterModules", formatterModules);
653 | const initialModelProps = await replaceWidgetWithComponent(
654 | this.get("props"),
655 | get_model,
656 | );
657 | try {
658 | this.currentComponentToWrapOrError = await this.createComponentToWrap();
659 | } catch (e) {
660 | this.currentComponentToWrapOrError = e;
661 | }
662 |
663 | const isSpecialProp = (key: string) => {
664 | const specialProps = [
665 | "children",
666 | "props",
667 | "tabbable",
668 | "layout",
669 | "tooltip",
670 | ];
671 | if (specialProps.find((x) => x === key)) {
672 | return true;
673 | }
674 | if (key.startsWith("_")) {
675 | return true;
676 | }
677 | return false;
678 | };
679 |
680 | const WrapperComponent = ({ view, ...parentProps }: { view: any }) => {
681 | const [component, setComponent] = useState(
682 | () => this.currentComponentToWrapOrError,
683 | );
684 | React.useEffect(() => {
685 | this.listenTo(this, "component", (component) => {
686 | console.log("set component", component);
687 | setComponent(() => component);
688 | });
689 | return () => {
690 | this.stopListening(this, "component");
691 | };
692 | }, []);
693 | const [childrenComponents, setChildrenComponents] = useState(
694 | initialChildrenComponents,
695 | );
696 | const updateChildren = () => {
697 | console.log("update children");
698 | this.enqueue(async () => {
699 | setChildrenComponents(
700 | await replaceWidgetWithComponent(
701 | { children: this.get("children") },
702 | get_model,
703 | ),
704 | );
705 | });
706 | };
707 | const [modelProps, setModelProps] = useState(initialModelProps);
708 | const updateModelProps = () => {
709 | this.enqueue(async () => {
710 | setModelProps(
711 | await replaceWidgetWithComponent(this.get("props"), get_model),
712 | );
713 | });
714 | };
715 | useEffect(() => {
716 | this.listenTo(this, "change:props", updateModelProps);
717 | this.listenTo(this, "change:children", updateChildren);
718 | for (const key of Object.keys(this.attributes)) {
719 | if (isSpecialProp(key)) {
720 | continue;
721 | }
722 | this.listenTo(this, `change:${key}`, updateChildren);
723 | }
724 | // If props or children were updated while we were initializing the view,
725 | // we want to do a rerender
726 | const checkPropsChange = async () => {
727 | const [currentProps, currentChildren] = await Promise.all([
728 | replaceWidgetWithComponent(this.get("props"), get_model),
729 | replaceWidgetWithComponent(
730 | { children: this.get("children") },
731 | get_model,
732 | ),
733 | ]);
734 | if (!isEqual(currentProps, initialModelProps)) {
735 | updateModelProps();
736 | }
737 | if (!isEqual(currentChildren, initialChildrenComponents)) {
738 | updateChildren();
739 | }
740 | };
741 | this.enqueue(checkPropsChange);
742 | return () => {
743 | this.stopListening(this, "change:props", updateModelProps);
744 | this.stopListening(this, "change:children", updateChildren);
745 | for (const key of Object.keys(this.attributes)) {
746 | if (isSpecialProp(key)) {
747 | continue;
748 | }
749 | this.stopListening(this, `change:${key}`, updateChildren);
750 | }
751 | };
752 | }, []);
753 | const events: any = {};
754 | for (const event_name of this.attributes["_event_names"]) {
755 | const handler = (value: any, buffers: any) => {
756 | if (buffers) {
757 | const validBuffers =
758 | buffers instanceof Array && buffers[0] instanceof ArrayBuffer;
759 | if (!validBuffers) {
760 | console.warn("second argument is not an BufferArray[View] array");
761 | buffers = undefined;
762 | }
763 | }
764 | const saveValue = eventToObject(value);
765 | console.log("sending", event_name, saveValue, view);
766 | this.send(
767 | { event_name, data: saveValue },
768 | this.callbacks(view),
769 | buffers,
770 | );
771 | };
772 | events[event_name] = handler;
773 | }
774 | // React.createElement('div', {"aria-activedescendant": "foo"}})
775 | //
776 | // for (const key of Object.keys(modelProps)) {
777 | // if(formatterModules[key]) {
778 | // modelProps[key] = formatterModules[key].py2js(modelProps[key]);
779 | // }
780 | // }
781 | // console.log("children", children);
782 | const childrenProps = replaceComponentWithElement(
783 | childrenComponents,
784 | view,
785 | );
786 | if (childrenProps.children && childrenProps.children.length === 1) {
787 | childrenProps.children = childrenProps.children[0];
788 | }
789 | // useEffect(() => {
790 | // // force render every 2 seconds
791 | // const interval = setInterval(() => {
792 | // forceRerender();
793 | // }, 2000);
794 | // return () => {
795 | // clearInterval(interval);
796 | // }
797 | // }, []);
798 | //const [r//]
799 | const backboneProps: any = {};
800 | for (const key of Object.keys(this.attributes)) {
801 | if (isSpecialProp(key)) {
802 | continue;
803 | }
804 | backboneProps[key] = this.get(key);
805 | backboneProps["set" + key.charAt(0).toUpperCase() + key.slice(1)] = (
806 | value: any,
807 | ) => {
808 | this.set(key, value);
809 | // this.touch();
810 | this.save_changes(this.callbacks(view));
811 | };
812 | }
813 |
814 | const props = {
815 | ...replaceComponentWithElement(modelProps, view),
816 | ...backboneProps,
817 | ...parentProps,
818 | ...events,
819 | ...childrenProps,
820 | };
821 |
822 | if (component instanceof Error) {
823 | throw component;
824 | }
825 | return React.createElement(component, props);
826 | };
827 | return WrapperComponent;
828 | }
829 | destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR {
830 | if (this.codeUrl) {
831 | URL.revokeObjectURL(this.codeUrl);
832 | }
833 | return super.destroy(options);
834 | }
835 | public component: Promise;
836 | private resolveComponent: (value: any) => void;
837 | private rejectComponent: (value: any) => void;
838 | private compiledCode: string | null = null;
839 | private compileError: any | null = null;
840 | private codeUrl: string | null = null;
841 | // this used so that the WrapperComponent can be rendered synchronously,
842 | private currentComponentToWrapOrError: any = null;
843 | private queue: Promise;
844 |
845 | static model_name = "ReactModel";
846 | static model_module = MODULE_NAME;
847 | static model_module_version = MODULE_VERSION;
848 | static view_name = "ReactView"; // Set to null if no view
849 | static view_module = MODULE_NAME; // Set to null if no view
850 | static view_module_version = MODULE_VERSION;
851 | }
852 |
853 | export class ReactView extends DOMWidgetView {
854 | private root: Root | null = null;
855 |
856 | async render() {
857 | this.el.classList.add("jupyter-react-widget");
858 | // using babel is a bit of an art, so leaving this code for if we
859 | this.root = ReactDOMClient.createRoot(this.el);
860 | const Component: any = await (this.model as ReactModel).component;
861 | this.root.render(
862 |
863 |
864 | ,
865 | );
866 | }
867 |
868 | remove() {
869 | this.root?.unmount();
870 | }
871 | }
872 |
--------------------------------------------------------------------------------
/examples/full_tutorial.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "d8ec9a76-0819-463d-8955-55789ee36400",
6 | "metadata": {},
7 | "source": [
8 | "# Ipyreact walkthrough"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "id": "083f2dc7-c0ec-482f-a720-aa2896ad193e",
14 | "metadata": {
15 | "tags": []
16 | },
17 | "source": [
18 | "Welcome to this ipyreact walkthrough! \n",
19 | "The tutorial will be based on a very simple react button to show all the ipyreact features. \n",
20 | "\n",
21 | "**Content** \n",
22 | "* use the %react cell magic\n",
23 | "* write a widget\n",
24 | "* style this with CSS\n",
25 | "* add parameters to your widgets (traitlets)\n",
26 | "* interact with these parameters\n",
27 | "* simple traitlet oberservation using `change`\n",
28 | "* observe a traitlet and call python function\n",
29 | "* observe a traitlet and call JavaScript function\n",
30 | "* call python functions from JavaScript\n",
31 | "* loading components from external files\n",
32 | "* enable hot reloading \n",
33 | "* enable autocompletion in IDEs\n",
34 | "* print a message at class initialization"
35 | ]
36 | },
37 | {
38 | "cell_type": "markdown",
39 | "id": "06cb067d-0aea-4851-827d-d07f22466693",
40 | "metadata": {},
41 | "source": [
42 | "First, we will use the **`%react` magic** from ipyreact. \n",
43 | "The following line registers the cellmagic:"
44 | ]
45 | },
46 | {
47 | "cell_type": "code",
48 | "execution_count": null,
49 | "id": "de17b188",
50 | "metadata": {
51 | "scrolled": true
52 | },
53 | "outputs": [],
54 | "source": [
55 | "%pip install -q ipyreact\n",
56 | "# This line is for JupyterLite (if this takes more than 10 seconds, something probably hung, restart the kernel and run this cell again)"
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": null,
62 | "id": "8d85f911",
63 | "metadata": {},
64 | "outputs": [],
65 | "source": [
66 | "%load_ext ipyreact"
67 | ]
68 | },
69 | {
70 | "cell_type": "code",
71 | "execution_count": null,
72 | "id": "c70f7253-98d5-4131-bf6b-1ac3433e2985",
73 | "metadata": {
74 | "tags": []
75 | },
76 | "outputs": [],
77 | "source": [
78 | "%%react\n",
79 | "\n",
80 | "import * as React from \"react\";\n",
81 | "\n",
82 | "export default function MyButton() {\n",
83 | " return ( < button > X < /button>);\n",
84 | "}"
85 | ]
86 | },
87 | {
88 | "cell_type": "markdown",
89 | "id": "4b51d34f-8abc-4b7b-936e-6d582a1b64fe",
90 | "metadata": {},
91 | "source": [
92 | "Great, here we can see react code rendering in the jupyter notebook! \n",
93 | "Next, we **convert this into a widget.** \n",
94 | "For that, we need the code in a `_esm` string inside a class that inherits from `ipyreact.Widget`. \n",
95 | "`esm` is short for for EcmaScript module, and thats standard for structuring JavaScript code in reusable components."
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "id": "7c70385c-638e-482b-80e1-6f27614da867",
102 | "metadata": {
103 | "tags": []
104 | },
105 | "outputs": [],
106 | "source": [
107 | "import ipyreact\n",
108 | "\n",
109 | "class MyExampleWidget(ipyreact.Widget):\n",
110 | " _esm = \"\"\"\n",
111 | " import * as React from \"react\";\n",
112 | "\n",
113 | " export default function MyButton() {\n",
114 | " return