├── .binder └── requirements.txt ├── .coveragerc ├── .prettierignore ├── examples ├── antd │ ├── antd-minimal.js │ └── antd.ipynb ├── threejs-fiber │ ├── threejs-fiber.js │ └── threejs-fiber.ipynb ├── autocomplete_screenshot.png ├── my_component.tsx ├── styles_orange.css ├── children.ipynb ├── Observe_example.ipynb ├── events.ipynb └── full_tutorial.ipynb ├── ipyreact.json ├── .jupyterlite └── requirements.txt ├── docs ├── source │ ├── examples │ │ ├── introduction.nblink │ │ └── index.rst │ ├── introduction.rst │ ├── _static │ │ └── helper.js │ ├── index.rst │ ├── develop-install.rst │ ├── installing.rst │ └── conf.py ├── environment.yml ├── Makefile └── make.bat ├── setup.py ├── css └── widget.css ├── pytest.ini ├── .npmignore ├── src ├── index.ts ├── version.ts ├── extension.ts ├── __tests__ │ ├── index.spec.ts │ └── utils.ts ├── plugin.ts ├── components.tsx ├── utils.ts └── widget.tsx ├── readthedocs.yml ├── ipyreact ├── _version.py ├── _frontend.py ├── basic.tsx ├── nbextension │ └── extension.js ├── module.py ├── __init__.py ├── cellmagic.py ├── importmap.py └── widget.py ├── install.json ├── lab └── jupyter-lite.json ├── tests ├── ui │ ├── snapshots │ │ └── tests │ │ │ └── ui │ │ │ ├── library_test.py │ │ │ └── test_material_ui-chromium-linux-reference.png │ │ │ └── jupyter_test.py │ │ │ ├── test_widget_ipyreact-voila-chromium-linux-reference.png │ │ │ ├── test_widget_ipyreact-solara-chromium-linux-reference.png │ │ │ ├── test_widget_ipyreact-jupyter_lab-chromium-linux-reference.png │ │ │ └── test_widget_ipyreact-jupyter_notebook-chromium-linux-reference.png │ ├── serialize_test.py │ ├── event_test.py │ ├── jupyter_test.py │ ├── library_test.py │ ├── basics_test.py │ └── module_test.py └── unit │ └── create_test.py ├── babel.config.js ├── release.sh ├── codecov.yml ├── .pre-commit-config.yaml ├── jest.config.js ├── .github ├── workflows │ ├── pycafe.yml │ ├── code-quality.yml │ ├── deploy.yml │ └── build.yml └── pycafe-create-status.py ├── tsconfig.json ├── .bumpversion.cfg ├── MANIFEST.in ├── LICENSE.txt ├── .gitignore ├── webpack.config.js ├── pyproject.toml ├── package.json └── README.md /.binder/requirements.txt: -------------------------------------------------------------------------------- 1 | ipyreact -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = ipyreact/tests/* 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json -------------------------------------------------------------------------------- /examples/antd/antd-minimal.js: -------------------------------------------------------------------------------- 1 | export { Button, Flex, Slider } from "antd"; 2 | -------------------------------------------------------------------------------- /ipyreact.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "jupyter-react/extension": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.jupyterlite/requirements.txt: -------------------------------------------------------------------------------- 1 | jupyterlite 2 | jupyterlab 3 | ipyreact 4 | jupyterlite-pyodide-kernel 5 | -------------------------------------------------------------------------------- /docs/source/examples/introduction.nblink: -------------------------------------------------------------------------------- 1 | { 2 | "path": "../../../examples/introduction.ipynb" 3 | } 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim for use with applications that require it. 2 | __import__("setuptools").setup() 3 | -------------------------------------------------------------------------------- /css/widget.css: -------------------------------------------------------------------------------- 1 | .jupyter-react-widget { 2 | /* background-color: lightseagreen; */ 3 | padding: 0px 2px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/threejs-fiber/threejs-fiber.js: -------------------------------------------------------------------------------- 1 | export * from "@react-three/fiber"; 2 | export * from "@react-three/drei"; 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = ipyreact/tests examples 3 | norecursedirs = node_modules .ipynb_checkpoints 4 | -------------------------------------------------------------------------------- /examples/autocomplete_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widgetti/ipyreact/master/examples/autocomplete_screenshot.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tests/ 4 | .jshintrc 5 | # Ignore any build output from python: 6 | dist/*.tar.gz 7 | dist/*.wheel 8 | -------------------------------------------------------------------------------- /examples/my_component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default function MyButton() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Introduction 3 | ============= 4 | 5 | .. todo:: 6 | 7 | add prose explaining project purpose and usage here 8 | -------------------------------------------------------------------------------- /docs/source/_static/helper.js: -------------------------------------------------------------------------------- 1 | var cache_require = window.require; 2 | 3 | window.addEventListener("load", function () { 4 | window.require = cache_require; 5 | }); 6 | -------------------------------------------------------------------------------- /examples/styles_orange.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: orange; 3 | border-color: orange; 4 | font-weight: bold; 5 | border-width: 2.5px; 6 | user-select: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten A. Breddels 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | export * from "./version"; 5 | export * from "./widget"; 6 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | type: sphinx 2 | python: 3 | version: 3.5 4 | pip_install: true 5 | extra_requirements: 6 | - examples 7 | - docs 8 | conda: 9 | file: docs/environment.yml 10 | -------------------------------------------------------------------------------- /ipyreact/_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Maarten A. Breddels. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | __version__ = "0.5.0" 8 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "ipyreact", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package ipyreact" 5 | } 6 | -------------------------------------------------------------------------------- /lab/jupyter-lite.json: -------------------------------------------------------------------------------- 1 | { 2 | "jupyter-config-data": { 3 | "enableMemoryStorage": true, 4 | "settingsStorageDrivers": ["memoryStorageDriver"], 5 | "contentsStorageDrivers": ["memoryStorageDriver"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/library_test.py/test_material_ui-chromium-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widgetti/ipyreact/master/tests/ui/snapshots/tests/ui/library_test.py/test_material_ui-chromium-linux-reference.png -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: ipyreact_docs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.* 6 | - nodejs 7 | - jupyter_sphinx 8 | - sphinx 9 | - sphinx_rtd_theme 10 | - nbsphinx 11 | - nbsphinx-link 12 | -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-voila-chromium-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widgetti/ipyreact/master/tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-voila-chromium-linux-reference.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceMap: "inline", 3 | presets: [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | targets: { 8 | node: "current", 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-solara-chromium-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widgetti/ipyreact/master/tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-solara-chromium-linux-reference.png -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-jupyter_lab-chromium-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widgetti/ipyreact/master/tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-jupyter_lab-chromium-linux-reference.png -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -o pipefail 3 | # usage: ./release minor -n 4 | version=$(bump2version --dry-run --list $* | grep "new_version=" | sed -r s,"^.*=",,) 5 | echo Version tag v$version 6 | bumpversion $* --verbose && git push upstream master v$version 7 | -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-jupyter_notebook-chromium-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widgetti/ipyreact/master/tests/ui/snapshots/tests/ui/jupyter_test.py/test_widget_ipyreact-jupyter_notebook-chromium-linux-reference.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | # show coverage in CI status, but never consider it a failure 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: 0% 8 | patch: 9 | default: 10 | target: 0% 11 | ignore: 12 | - "ipyreact/tests" 13 | -------------------------------------------------------------------------------- /ipyreact/_frontend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Maarten A. Breddels. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | """ 8 | Information about the frontend package of the widgets. 9 | """ 10 | 11 | module_name = "@widgetti/jupyter-react" 12 | module_version = "^0.5.0" 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: "https://github.com/pre-commit/mirrors-prettier" 3 | rev: "v3.1.0" 4 | hooks: 5 | - id: prettier 6 | stages: [commit] 7 | - repo: https://github.com/charliermarsh/ruff-pre-commit 8 | rev: "v0.1.9" 9 | hooks: 10 | - id: ruff 11 | stages: [commit] 12 | - id: ruff-format 13 | stages: [commit] 14 | -------------------------------------------------------------------------------- /tests/unit/create_test.py: -------------------------------------------------------------------------------- 1 | import ipyreact 2 | 3 | 4 | def test_create(): 5 | class Counter(ipyreact.ReactWidget): 6 | _esm = """ 7 | import * as React from "react"; 8 | 9 | export default function({value, set_value, debug}) { 10 | return 13 | };""" 14 | 15 | Counter() 16 | -------------------------------------------------------------------------------- /ipyreact/basic.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | import confetti from "canvas-confetti"; 3 | 4 | import * as React from "react"; 5 | export default function ({ value, on_value, debug }) { 6 | if (debug) { 7 | console.log("value=", value, on_value); 8 | } 9 | return ( 10 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /docs/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Examples 3 | ======== 4 | 5 | This section contains several examples generated from Jupyter notebooks. 6 | The widgets have been embedded into the page for demonstrative purposes. 7 | 8 | .. todo:: 9 | 10 | Add links to notebooks in examples folder similar to the initial 11 | one. This is a manual step to ensure only those examples that 12 | are suited for inclusion are used. 13 | 14 | 15 | .. toctree:: 16 | :glob: 17 | 18 | * 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | automock: false, 3 | moduleNameMapper: { 4 | "\\.(css|less|sass|scss)$": "identity-obj-proxy", 5 | }, 6 | preset: "ts-jest/presets/js-with-babel", 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 8 | testPathIgnorePatterns: ["/lib/", "/node_modules/"], 9 | testRegex: "/__tests__/.*.spec.ts[x]?$", 10 | transformIgnorePatterns: ["/node_modules/(?!(@jupyter(lab|-widgets)/.*)/)"], 11 | globals: { 12 | "ts-jest": { 13 | tsconfig: "/tsconfig.json", 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/pycafe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PyCafe Playground Link 3 | on: 4 | workflow_run: 5 | workflows: [Build] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | create-status: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - name: Create PyCafe status link 16 | run: | 17 | pip install PyGithub 18 | python .github/pycafe-create-status.py ${{ github.event.workflow_run.head_sha }} ${{ github.event.workflow_run.id }} 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: code-quality 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.7 18 | - name: Install dependencies 19 | run: | 20 | pip install ".[dev]" 21 | pre-commit install 22 | - name: run pre-commit 23 | run: | 24 | pre-commit run --all-files 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["es2019", "dom"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noUnusedLocals": true, 10 | "outDir": "lib", 11 | "resolveJsonModule": true, 12 | "rootDir": "src", 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | "target": "es2015", 18 | "types": ["jest"], 19 | "jsx": "react" 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.tsx"], 22 | "exclude": ["src/**/__tests__"] 23 | } 24 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten A. Breddels 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-ignore 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const data = require("../package.json"); 8 | 9 | /** 10 | * The _model_module_version/_view_module_version this package implements. 11 | * 12 | * The html widget manager assumes that this is the same as the npm package 13 | * version number. 14 | */ 15 | export const MODULE_VERSION = data.version; 16 | 17 | /* 18 | * The current package name. 19 | */ 20 | export const MODULE_NAME = data.name; 21 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | // Entry point for the notebook bundle containing custom model definitions. 5 | // 6 | // Setup notebook base URL 7 | // 8 | // Some static assets may be required by the custom widget javascript. The base 9 | // url for the notebook is not known at build time and is therefore computed 10 | // dynamically. 11 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 12 | (window as any).__webpack_public_path__ = 13 | document.querySelector("body")!.getAttribute("data-base-url") + 14 | "nbextensions/jupyter-react"; 15 | 16 | export * from "./index"; 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = ipyreact 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.5.0 3 | commit = True 4 | tag = True 5 | message = chore: bump version {current_version} → {new_version} 6 | tag_message = chore: bump version {current_version} → {new_version} 7 | parse = (?P\d+)(\.(?P\d+))(\.(?P\d+))((?P.)(?P\d+))? 8 | serialize = 9 | {major}.{minor}.{patch}{release}{build} 10 | {major}.{minor}.{patch} 11 | 12 | [bumpversion:part:release] 13 | optional_value = g 14 | first_value = g 15 | values = 16 | a 17 | b 18 | g 19 | 20 | [bumpversion:file:ipyreact/_version.py] 21 | 22 | [bumpversion:file:ipyreact/_frontend.py] 23 | 24 | [bumpversion:file:package.json] 25 | 26 | [bumpversion:file:.github/pycafe-create-status.py] 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | 4 | include setup.py 5 | include pyproject.toml 6 | include pytest.ini 7 | include .coverage.rc 8 | 9 | include tsconfig.json 10 | include package.json 11 | include webpack.config.js 12 | include ipyreact/labextension/*.tgz 13 | 14 | # Documentation 15 | graft docs 16 | exclude docs/\#* 17 | prune docs/build 18 | prune docs/gh-pages 19 | prune docs/dist 20 | 21 | # Examples 22 | graft examples 23 | 24 | # Tests 25 | graft tests 26 | prune tests/build 27 | 28 | # Javascript files 29 | graft ipyreact/nbextension 30 | graft src 31 | graft css 32 | prune **/node_modules 33 | prune coverage 34 | prune lib 35 | 36 | # Patterns to exclude from any directory 37 | global-exclude *~ 38 | global-exclude *.pyc 39 | global-exclude *.pyo 40 | global-exclude .git 41 | global-exclude .ipynb_checkpoints 42 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | ipyreact 3 | ===================================== 4 | 5 | Version: |release| 6 | 7 | React for ipywidgets that just works 8 | 9 | 10 | Quickstart 11 | ---------- 12 | 13 | To get started with ipyreact, install with pip:: 14 | 15 | pip install ipyreact 16 | 17 | or with conda:: 18 | 19 | conda install ipyreact 20 | 21 | 22 | Contents 23 | -------- 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | :caption: Installation and usage 28 | 29 | installing 30 | introduction 31 | 32 | .. toctree:: 33 | :maxdepth: 1 34 | 35 | examples/index 36 | 37 | 38 | .. toctree:: 39 | :maxdepth: 2 40 | :caption: Development 41 | 42 | develop-install 43 | 44 | 45 | .. links 46 | 47 | .. _`Jupyter widgets`: https://jupyter.org/widgets.html 48 | 49 | .. _`notebook`: https://jupyter-notebook.readthedocs.io/en/latest/ 50 | -------------------------------------------------------------------------------- /tests/ui/serialize_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import playwright.sync_api 3 | from IPython.display import display 4 | 5 | import ipyreact 6 | 7 | code = """ 8 | import {Button} from '@mui/material'; 9 | import * as React from "react"; 10 | 11 | export default function({floatArrayDataView}) { 12 | const floatArray = new Float32Array(floatArrayDataView.buffer); 13 | console.log({floatArray, floatArrayDataView}); 14 | return
{`v:${floatArray[0]}`}
15 | }; 16 | """ 17 | 18 | 19 | def test_material_ui(solara_test, assert_solara_snapshot, page_session: playwright.sync_api.Page): 20 | class SerializeTest(ipyreact.ReactWidget): 21 | _esm = code 22 | 23 | ar = np.array([42.0], dtype=np.float32) 24 | b = SerializeTest(props={"floatArrayDataView": memoryview(ar)}) 25 | display(b) 26 | 27 | page_session.locator("text=42").wait_for() 28 | -------------------------------------------------------------------------------- /tests/ui/event_test.py: -------------------------------------------------------------------------------- 1 | import playwright.sync_api 2 | import traitlets 3 | from IPython.display import display 4 | 5 | import ipyreact 6 | 7 | 8 | class ButtonWithHandler(ipyreact.ReactWidget): 9 | label = traitlets.Unicode("Click me").tag(sync=True) 10 | _esm = """ 11 | import * as React from "react"; 12 | export default function({on_click, label}) { 13 | return 16 | }; 17 | """ 18 | 19 | def event_on_click(self): 20 | self.label = "Clicked" 21 | 22 | 23 | def test_event(solara_test, page_session: playwright.sync_api.Page): 24 | b = ButtonWithHandler() 25 | display(b) 26 | button = page_session.locator(".event-button") 27 | button.click() 28 | page_session.locator(".event-button >> text=Clicked").wait_for() 29 | assert b.label == "Clicked" 30 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=ipyreact 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /tests/ui/jupyter_test.py: -------------------------------------------------------------------------------- 1 | import playwright.sync_api 2 | from IPython.display import display 3 | 4 | 5 | def test_widget_ipyreact(ipywidgets_runner, page_session: playwright.sync_api.Page, assert_solara_snapshot): 6 | def kernel_code(): 7 | import ipyreact 8 | 9 | class Counter(ipyreact.ReactWidget): 10 | _esm = """ 11 | import * as React from "react"; 12 | 13 | export default function({value, setValue, debug}) { 14 | return 17 | };""" 18 | 19 | c = Counter() 20 | display(c) 21 | 22 | ipywidgets_runner(kernel_code) 23 | counter = page_session.locator(".counter-widget") 24 | counter.click() 25 | page_session.locator("text=1 clicks").wait_for() 26 | assert_solara_snapshot(counter.screenshot()) 27 | -------------------------------------------------------------------------------- /src/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | // Add any needed widget imports here (or from controls) 5 | // import {} from '@jupyter-widgets/base'; 6 | 7 | import { createTestModel } from "./utils"; 8 | 9 | import { ExampleModel } from ".."; 10 | 11 | describe("Example", () => { 12 | describe("ExampleModel", () => { 13 | it("should be createable", () => { 14 | const model = createTestModel(ExampleModel); 15 | expect(model).toBeInstanceOf(ExampleModel); 16 | expect(model.get("value")).toEqual("Hello World"); 17 | }); 18 | 19 | it("should be createable with a value", () => { 20 | const state = { value: "Foo Bar!" }; 21 | const model = createTestModel(ExampleModel, state); 22 | expect(model).toBeInstanceOf(ExampleModel); 23 | expect(model.get("value")).toEqual("Foo Bar!"); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /docs/source/develop-install.rst: -------------------------------------------------------------------------------- 1 | 2 | Developer install 3 | ================= 4 | 5 | 6 | To install a developer version of ipyreact, you will first need to clone 7 | the repository:: 8 | 9 | git clone https://github.com/\widgetti/ipyreact 10 | cd ipyreact 11 | 12 | Next, install it with a develop install using pip:: 13 | 14 | pip install -e . 15 | 16 | 17 | If you are planning on working on the JS/frontend code, you should also do 18 | a link installation of the extension:: 19 | 20 | jupyter nbextension install [--sys-prefix / --user / --system] --symlink --py ipyreact 21 | 22 | jupyter nbextension enable [--sys-prefix / --user / --system] --py ipyreact 23 | 24 | with the `appropriate flag`_. Or, if you are using Jupyterlab:: 25 | 26 | jupyter labextension install . 27 | 28 | 29 | .. links 30 | 31 | .. _`appropriate flag`: https://jupyter-notebook.readthedocs.io/en/stable/extending/frontend_extensions.html#installing-and-enabling-extensions 32 | -------------------------------------------------------------------------------- /ipyreact/nbextension/extension.js: -------------------------------------------------------------------------------- 1 | // Entry point for the notebook bundle containing custom model definitions. 2 | // 3 | define(function () { 4 | "use strict"; 5 | 6 | window["requirejs"].config({ 7 | map: { 8 | "*": { 9 | "@widgetti/jupyter-react": "nbextensions/jupyter-react/index", 10 | // 'jupyter-react16': 'nbextensions/ipyreact/index16', 11 | }, 12 | }, 13 | }); 14 | // Export the required load_ipython_extension function 15 | return { 16 | load_ipython_extension: function () { 17 | require(["notebook/js/codecell"], function (codecell) { 18 | codecell.CodeCell.options_default.highlight_modes[ 19 | "text/typescript-jsx" 20 | ] = { reg: [/^%%(ipy)?react/] }; 21 | IPython.notebook.events.one("kernel_ready.Kernel", function () { 22 | IPython.notebook.get_cells().map(function (cell) { 23 | if (cell.cell_type == "code") { 24 | cell.auto_highlight(); 25 | } 26 | }); 27 | }); 28 | }); 29 | }, 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /tests/ui/library_test.py: -------------------------------------------------------------------------------- 1 | import playwright.sync_api 2 | from IPython.display import display 3 | 4 | code = """ 5 | import {Button} from '@mui/material'; 6 | import * as React from "react"; 7 | 8 | export default function({value, setValue, debug}) { 9 | return 12 | }; 13 | """ 14 | 15 | 16 | def test_material_ui(solara_test, assert_solara_snapshot, page_session: playwright.sync_api.Page): 17 | # this can be removed if solara adds support to ipyreact/ipyesm 18 | import ipyreact.importmap 19 | 20 | ipyreact.importmap._import_map_widget = None 21 | ipyreact.importmap._update_import_map() 22 | 23 | class Button(ipyreact.ReactWidget): 24 | _esm = code 25 | 26 | b = Button() 27 | display(b) 28 | 29 | button = page_session.locator("text=0 times") 30 | button.click() 31 | button = page_session.locator("text=1 times") 32 | button.wait_for() 33 | page_session.wait_for_timeout(300) # wait for animation 34 | assert_solara_snapshot(button.screenshot()) 35 | -------------------------------------------------------------------------------- /docs/source/installing.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _installation: 3 | 4 | Installation 5 | ============ 6 | 7 | 8 | The simplest way to install ipyreact is via pip:: 9 | 10 | pip install ipyreact 11 | 12 | or via conda:: 13 | 14 | conda install ipyreact 15 | 16 | 17 | If you installed via pip, and notebook version < 5.3, you will also have to 18 | install / configure the front-end extension as well. If you are using classic 19 | notebook (as opposed to Jupyterlab), run:: 20 | 21 | jupyter nbextension install [--sys-prefix / --user / --system] --py ipyreact 22 | 23 | jupyter nbextension enable [--sys-prefix / --user / --system] --py ipyreact 24 | 25 | with the `appropriate flag`_. If you are using Jupyterlab, install the extension 26 | with:: 27 | 28 | jupyter labextension install jupyter-react 29 | 30 | If you are installing using conda, these commands should be unnecessary, but If 31 | you need to run them the commands should be the same (just make sure you choose the 32 | `--sys-prefix` flag). 33 | 34 | 35 | .. links 36 | 37 | .. _`appropriate flag`: https://jupyter-notebook.readthedocs.io/en/stable/extending/frontend_extensions.html#installing-and-enabling-extensions 38 | -------------------------------------------------------------------------------- /examples/children.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "a828d6c2", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ipyreact\n", 11 | "import ipywidgets as widgets\n", 12 | "\n", 13 | "ipyreact.Widget(_type=\"div\", children=[\n", 14 | " \"normal text\",\n", 15 | " ipyreact.Widget(_type=\"button\", children=[\"nested react widgets\"]),\n", 16 | " widgets.FloatSlider(description=\"regular ipywidgets\")\n", 17 | "])" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "id": "6b9e745b", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [] 27 | } 28 | ], 29 | "metadata": { 30 | "kernelspec": { 31 | "display_name": "Python 3 (ipykernel)", 32 | "language": "python", 33 | "name": "python3" 34 | }, 35 | "language_info": { 36 | "codemirror_mode": { 37 | "name": "ipython", 38 | "version": 3 39 | }, 40 | "file_extension": ".py", 41 | "mimetype": "text/x-python", 42 | "name": "python", 43 | "nbconvert_exporter": "python", 44 | "pygments_lexer": "ipython3", 45 | "version": "3.9.16" 46 | } 47 | }, 48 | "nbformat": 4, 49 | "nbformat_minor": 5 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build the JupyterLite site 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Setup Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.10" 21 | - name: Install the dependencies 22 | run: | 23 | python -m pip install -r .jupyterlite/requirements.txt 24 | - name: Build the JupyterLite site 25 | run: | 26 | cp README.md examples 27 | jupyter lite build --contents examples --output-dir dist 28 | - name: Upload artifact 29 | uses: actions/upload-pages-artifact@v1 30 | with: 31 | path: ./dist 32 | 33 | deploy: 34 | needs: build 35 | if: github.ref == 'refs/heads/master' 36 | permissions: 37 | pages: write 38 | id-token: write 39 | 40 | environment: 41 | name: github-pages 42 | url: ${{ steps.deployment.outputs.page_url }} 43 | 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v1 49 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten A. Breddels 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { Application, IPlugin } from "@lumino/application"; 5 | 6 | import { Widget } from "@lumino/widgets"; 7 | 8 | import { IJupyterWidgetRegistry } from "@jupyter-widgets/base"; 9 | 10 | import * as widgetExports from "./widget"; 11 | 12 | import { MODULE_NAME, MODULE_VERSION } from "./version"; 13 | 14 | const EXTENSION_ID = "@widgetti/jupyter-react:plugin"; 15 | 16 | /** 17 | * The example plugin. 18 | */ 19 | const examplePlugin: IPlugin, void> = { 20 | id: EXTENSION_ID, 21 | requires: [IJupyterWidgetRegistry], 22 | activate: activateWidgetExtension, 23 | autoStart: true, 24 | } as unknown as IPlugin, void>; 25 | // the "as unknown as ..." typecast above is solely to support JupyterLab 1 26 | // and 2 in the same codebase and should be removed when we migrate to Lumino. 27 | 28 | export default examplePlugin; 29 | 30 | /** 31 | * Activate the widget extension. 32 | */ 33 | function activateWidgetExtension( 34 | app: Application, 35 | registry: IJupyterWidgetRegistry, 36 | ): void { 37 | registry.registerWidget({ 38 | name: MODULE_NAME, 39 | version: MODULE_VERSION, 40 | exports: widgetExports, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Maarten A. Breddels 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /ipyreact/module.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List, Union 3 | 4 | import traitlets 5 | from ipywidgets import DOMWidget 6 | from traitlets import Int, Unicode, default 7 | 8 | from ._frontend import module_name, module_version 9 | 10 | _standard_dependencies: List[str] = [] 11 | 12 | 13 | class Module(DOMWidget): 14 | _model_name = Unicode("Module").tag(sync=True) 15 | _model_module = Unicode(module_name).tag(sync=True) 16 | _model_module_version = Unicode(module_version).tag(sync=True) 17 | _view_name = Unicode("ModuleView").tag(sync=True) 18 | _view_module = Unicode(module_name).tag(sync=True) 19 | _view_module_version = Unicode(module_version).tag(sync=True) 20 | name = Unicode(allow_none=False).tag(sync=True) 21 | code = Unicode(allow_none=False).tag(sync=True) 22 | dependencies = traitlets.List(Unicode(), allow_none=True).tag(sync=True) 23 | status = Unicode(allow_none=True).tag(sync=True) 24 | react_version = Int(18).tag(sync=True) 25 | 26 | @default("dependencies") 27 | def _default_dependencies(self): 28 | return [k for k in get_module_names() if k != self.name] 29 | 30 | 31 | def get_module_names(): 32 | return _standard_dependencies 33 | 34 | 35 | def define_module(name, module: Union[str, Path]): 36 | """Register a ES module under a name. 37 | 38 | Parameters 39 | ---------- 40 | name: str 41 | Name of the es module to register 42 | module: str | Path 43 | The module code to register 44 | """ 45 | _standard_dependencies.append(name) 46 | return Module(code=module if not isinstance(module, Path) else module.read_text(encoding="utf8"), name=name) 47 | -------------------------------------------------------------------------------- /.github/pycafe-create-status.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from urllib.parse import quote 4 | 5 | from github import Github 6 | 7 | # Authenticate with GitHub 8 | access_token = os.getenv("GITHUB_TOKEN") 9 | g = Github(access_token) 10 | 11 | 12 | repo_name = "widgetti/ipyreact" 13 | commit_sha = sys.argv[1] # e.g d39677a321bca34df41ecc87ff7e539b450207f2 14 | run_id = sys.argv[2] # e.g 1324, usually obtained via ${{ github.run_id }} or ${{ github.event.workflow_run.id }} in GitHub Actions workflow files 15 | type = "solara" # streamlit/dash/vizro/solara/panel 16 | 17 | # your default code 18 | code = """import ipyreact 19 | 20 | 21 | class ConfettiWidget(ipyreact.ValueWidget): 22 | _esm = \""" 23 | import confetti from "canvas-confetti"; 24 | import * as React from "react"; 25 | 26 | export default function({value, setValue}) { 27 | return 30 | };\""" 31 | page = ConfettiWidget() 32 | """ 33 | 34 | artifact_name = "ipyreact-dist" # name given in the GitHub Actions workflow file for the artifact 35 | 36 | # your default requirements, the wheel version number (0.5.0) is bumped up for each new release using bump2version 37 | requirements = f"""solara 38 | https://py.cafe/gh/artifact/{repo_name}/actions/runs/{run_id}/{artifact_name}/ipyreact-0.5.0-py3-none-any.whl 39 | """ 40 | 41 | # GitHub Python API 42 | repo = g.get_repo(repo_name) 43 | 44 | base_url = f"https://py.cafe/snippet/{type}/v1" 45 | url = f"{base_url}#code={quote(code)}&requirements={quote(requirements)}" 46 | 47 | # Define the deployment status 48 | state = "success" # Options: 'error', 'failure', 'pending', 'success' 49 | description = "Test out this PR on a PyCafe playground environment" 50 | context = "PyCafe" 51 | 52 | # Create the status on the commit 53 | commit = repo.get_commit(commit_sha) 54 | commit.create_status(state="success", target_url=url, description=description, context="PyCafe") 55 | print(f"Deployment status added to commit {commit_sha}") 56 | -------------------------------------------------------------------------------- /ipyreact/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Maarten A. Breddels. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | from ._version import __version__ 8 | from .importmap import define_import_map 9 | from .module import define_module 10 | from .widget import ReactWidget, ValueWidget, Widget 11 | 12 | 13 | def _jupyter_labextension_paths(): 14 | """Called by Jupyter Lab Server to detect if it is a valid labextension and 15 | to install the widget 16 | Returns 17 | ======= 18 | src: Source directory name to copy files from. Webpack outputs generated files 19 | into this directory and Jupyter Lab copies from this directory during 20 | widget installation 21 | dest: Destination directory name to install widget files to. Jupyter Lab copies 22 | from `src` directory into /labextensions/ directory 23 | during widget installation 24 | """ 25 | return [ 26 | { 27 | "src": "labextension", 28 | "dest": "jupyter-react", 29 | } 30 | ] 31 | 32 | 33 | def _jupyter_nbextension_paths(): 34 | """Called by Jupyter Notebook Server to detect if it is a valid nbextension and 35 | to install the widget 36 | Returns 37 | ======= 38 | section: The section of the Jupyter Notebook Server to change. 39 | Must be 'notebook' for widget extensions 40 | src: Source directory name to copy files from. Webpack outputs generated files 41 | into this directory and Jupyter Notebook copies from this directory during 42 | widget installation 43 | dest: Destination directory name to install widget files to. Jupyter Notebook copies 44 | from `src` directory into /nbextensions/ directory 45 | during widget installation 46 | require: Path to importable AMD Javascript module inside the 47 | /nbextensions/ directory 48 | """ 49 | return [ 50 | { 51 | "section": "notebook", 52 | "src": "nbextension", 53 | "dest": "jupyter-react", 54 | "require": "jupyter-react/extension", 55 | } 56 | ] 57 | 58 | 59 | def load_ipython_extension(ipython): 60 | from .cellmagic import load_ipython_extension 61 | 62 | load_ipython_extension(ipython) 63 | -------------------------------------------------------------------------------- /ipyreact/cellmagic.py: -------------------------------------------------------------------------------- 1 | from IPython.core.magic import ( 2 | Magics, 3 | cell_magic, 4 | magics_class, 5 | needs_local_scope, 6 | ) 7 | from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring 8 | from IPython.display import display 9 | 10 | from . import widget 11 | 12 | 13 | @magics_class 14 | class ReactMagics(Magics): 15 | @needs_local_scope 16 | @magic_arguments() 17 | @argument( 18 | "-n", 19 | "--name", 20 | type=str, 21 | default="_last_react_widget", 22 | help=("Name of the widget variable injected into the local namespace" " (default = _last_react_widget)."), 23 | ) 24 | @argument( 25 | "-d", 26 | "--debug", 27 | action="store_true", 28 | default=False, 29 | help="Show debug information in the JS console.", 30 | ) 31 | @argument( 32 | "-c", 33 | "--cleanup", 34 | action="store_true", 35 | default=False, 36 | help="Destroy the previous widget before creating a new one.", 37 | ) 38 | @cell_magic 39 | def react(self, line, cell, local_ns): 40 | """Excute react code in a cell. 41 | 42 | Example: 43 | %%react -n my_widget -d 44 | import {Button} from '@mui/material'; 45 | import confetti from "canvas-confetti"; 46 | import * as React from "react"; 47 | 48 | export default function({value, on_value, debug}) { 49 | if(debug) { 50 | console.log("value=", value, on_value); 51 | } 52 | return ( 53 | 59 | ); 60 | } 61 | """ 62 | args = parse_argstring(ReactMagics.react, line) 63 | if args.cleanup and args.name in local_ns: 64 | local_ns[args.name].close() 65 | code = self.shell.transform_cell(cell) 66 | 67 | class Widget(widget.ReactWidget): 68 | _esm = code 69 | 70 | react_widget = Widget(_esm=code, debug=args.debug) 71 | local_ns[args.name] = react_widget 72 | display(react_widget) 73 | 74 | 75 | def load_ipython_extension(ipython): 76 | """ 77 | Use `%load_ext ipyreact` 78 | """ 79 | ipython.register_magics(ReactMagics) 80 | -------------------------------------------------------------------------------- /ipyreact/importmap.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Dict 3 | 4 | import traitlets 5 | from ipywidgets import DOMWidget 6 | from traitlets import Unicode 7 | 8 | from ._frontend import module_name, module_version 9 | 10 | _effective_import_map: Dict = {} 11 | _import_map_widget = None 12 | _lock = threading.Lock() 13 | 14 | 15 | class ImportMap(DOMWidget): 16 | _model_name = Unicode("ImportMap").tag(sync=True) 17 | _model_module = Unicode(module_name).tag(sync=True) 18 | _model_module_version = Unicode(module_version).tag(sync=True) 19 | _view_name = Unicode("ImportMapView").tag(sync=True) 20 | _view_module = Unicode(module_name).tag(sync=True) 21 | _view_module_version = Unicode(module_version).tag(sync=True) 22 | import_map = traitlets.Dict({}).tag(sync=True) 23 | 24 | 25 | def define_import_map(imports={}, scopes={}): 26 | """Define the import map for ESM modules. 27 | 28 | See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap 29 | 30 | Example: 31 | 32 | ```python 33 | import ipyreact 34 | ipyreact.define_import_map( 35 | imports={ 36 | "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", 37 | } 38 | ) 39 | ``` 40 | 41 | Now you can import the module `canvas-confetti` in your ESM modules. 42 | 43 | ```javascript 44 | import { confetti } from "canvas-confetti"; 45 | ... 46 | ``` 47 | 48 | """ 49 | global _effective_import_map 50 | _effective_import_map = { 51 | "imports": {**_effective_import_map.get("imports", {}), **imports}, 52 | "scopes": {**_effective_import_map.get("scopes", {}), **scopes}, 53 | } 54 | _update_import_map() 55 | 56 | 57 | def _get_import_map_widget(): 58 | global _import_map_widget 59 | with _lock: 60 | if _import_map_widget is None: 61 | _import_map_widget = ImportMap(import_map=_effective_import_map) 62 | return _import_map_widget 63 | 64 | 65 | def _update_import_map(): 66 | _get_import_map_widget().import_map = _effective_import_map 67 | 68 | 69 | # still for backwards compatibility 70 | define_import_map( 71 | { 72 | "@mui/material": "https://esm.sh/@mui/material@5.11.10?external=react,react-dom", 73 | "@mui/material/": "https://esm.sh/@mui/material@5.11.10&external=react,react-dom/", 74 | "@mui/icons-material/": "https://esm.sh/@mui/icons-material/?external=react,react-dom", 75 | "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0?external=react,react-dom", 76 | } 77 | ) 78 | -------------------------------------------------------------------------------- /examples/Observe_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "aba8b4d5-199f-4fe0-97a2-5a2a3f43cbf2", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ipyreact\n", 11 | "from traitlets import Int, observe, Unicode" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "b11af66b-f256-4441-8c38-0283866024bd", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "def check_is_even(number):\n", 22 | " if number %2 == 0:\n", 23 | " return True\n", 24 | " else:\n", 25 | " return False\n", 26 | "\n", 27 | "\n", 28 | "class EvenOddWidget(ipyreact.ReactWidget):\n", 29 | " #note that when we add these traitlets, they will automatically be made available to the react component\n", 30 | " message = Unicode(\"Click the button!\").tag(sync=True)\n", 31 | " \n", 32 | " count = Int(0).tag(sync=True)\n", 33 | "\n", 34 | " #it is key that we also take the change argument, otherwise a hard to find python error will be thrown\n", 35 | " @observe('count')\n", 36 | " def _observe_count(self, change):\n", 37 | " self.message = \"Yes ✅✅✅\" if check_is_even(self.count) else \"No 🧊🧊🧊\"\n", 38 | "\n", 39 | " _esm = \"\"\"\n", 40 | " import confetti from \"canvas-confetti\";\n", 41 | " import * as React from \"react\";\n", 42 | "\n", 43 | " export default function({setCount, count, message}) {\n", 44 | " return
\n", 47 | "
\n", 48 | " Is number even? {message} \n", 49 | "
\n", 50 | " };\"\"\"\n", 51 | "w = EvenOddWidget()\n", 52 | "w" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "d52fbf41", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [] 62 | } 63 | ], 64 | "metadata": { 65 | "kernelspec": { 66 | "display_name": "Python 3 (ipykernel)", 67 | "language": "python", 68 | "name": "python3" 69 | }, 70 | "language_info": { 71 | "codemirror_mode": { 72 | "name": "ipython", 73 | "version": 3 74 | }, 75 | "file_extension": ".py", 76 | "mimetype": "text/x-python", 77 | "name": "python", 78 | "nbconvert_exporter": "python", 79 | "pygments_lexer": "ipython3", 80 | "version": "3.9.16" 81 | } 82 | }, 83 | "nbformat": 4, 84 | "nbformat_minor": 5 85 | } 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | docs/source/_static/embed-bundle.js 65 | docs/source/_static/embed-bundle.js.map 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # ========================= 93 | # Operating System Files 94 | # ========================= 95 | 96 | # OSX 97 | # ========================= 98 | 99 | .DS_Store 100 | .AppleDouble 101 | .LSOverride 102 | 103 | # Thumbnails 104 | ._* 105 | 106 | # Files that might appear in the root of a volume 107 | .DocumentRevisions-V100 108 | .fseventsd 109 | .Spotlight-V100 110 | .TemporaryItems 111 | .Trashes 112 | .VolumeIcon.icns 113 | 114 | # Directories potentially created on remote AFP share 115 | .AppleDB 116 | .AppleDesktop 117 | Network Trash Folder 118 | Temporary Items 119 | .apdisk 120 | 121 | # Windows 122 | # ========================= 123 | 124 | # Windows image file caches 125 | Thumbs.db 126 | ehthumbs.db 127 | 128 | # Folder config file 129 | Desktop.ini 130 | 131 | # Recycle Bin used on file shares 132 | $RECYCLE.BIN/ 133 | 134 | # Windows Installer files 135 | *.cab 136 | *.msi 137 | *.msm 138 | *.msp 139 | 140 | # Windows shortcuts 141 | *.lnk 142 | 143 | 144 | # NPM 145 | # ---- 146 | 147 | **/node_modules/ 148 | ipyreact/nbextension/index.* 149 | 150 | # Coverage data 151 | # ------------- 152 | **/coverage/ 153 | 154 | # Packed lab extensions 155 | ipyreact/labextension 156 | -------------------------------------------------------------------------------- /examples/events.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "a7ea28e1", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ipyreact\n", 11 | "ipyreact.Widget(_type=\"button\", children=[\"click me\"], events={\"onClick\": print})" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "83522980", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import ipyreact\n", 22 | "import traitlets\n", 23 | "\n", 24 | "class ButtonWithHandler(ipyreact.ReactWidget):\n", 25 | " # extra trait\n", 26 | " label = traitlets.Unicode(\"Click me\").tag(sync=True)\n", 27 | " _esm = \"\"\"\n", 28 | " import * as React from \"react\";\n", 29 | "\n", 30 | " export default function({on_click, on_pass_data, label}) {\n", 31 | " // make sure the arguments passed matches the\n", 32 | " // python function\n", 33 | " return \\\n", 34 | "
\n", 35 | " \n", 38 | " \n", 41 | "
\n", 42 | " };\n", 43 | " \"\"\"\n", 44 | " \n", 45 | " # all on_* methods are automatically available from the frontend\n", 46 | " # with the same name as a prop\n", 47 | " def event_on_click(self):\n", 48 | " self.label = \"Clicked\"\n", 49 | "\n", 50 | " # an optional argument can be passed\n", 51 | " # an optional third argument can contain buffers (not used here)\n", 52 | " def event_on_pass_data(self, data):\n", 53 | " print(data)\n", 54 | " self.label = f'Clicked \"Pass data\" at {data[\"x\"]},{data[\"y\"]} when label was {data[\"label\"]}'\n", 55 | " \n", 56 | "b = ButtonWithHandler(debug=True)\n", 57 | "b" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "b09c0518", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [] 67 | } 68 | ], 69 | "metadata": { 70 | "dca-init": true, 71 | "kernelspec": { 72 | "display_name": "Python 3 (ipykernel)", 73 | "language": "python", 74 | "name": "python3" 75 | }, 76 | "language_info": { 77 | "codemirror_mode": { 78 | "name": "ipython", 79 | "version": 3 80 | }, 81 | "file_extension": ".py", 82 | "mimetype": "text/x-python", 83 | "name": "python", 84 | "nbconvert_exporter": "python", 85 | "pygments_lexer": "ipython3", 86 | "version": "3.9.16" 87 | } 88 | }, 89 | "nbformat": 4, 90 | "nbformat_minor": 5 91 | } 92 | -------------------------------------------------------------------------------- /src/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import * as widgets from "@jupyter-widgets/base"; 5 | import * as baseManager from "@jupyter-widgets/base-manager"; 6 | import * as services from "@jupyterlab/services"; 7 | 8 | let numComms = 0; 9 | 10 | export class MockComm implements widgets.IClassicComm { 11 | constructor() { 12 | this.comm_id = `mock-comm-id-${numComms}`; 13 | numComms += 1; 14 | } 15 | on_close(fn: ((x?: any) => void) | null): void { 16 | this._on_close = fn; 17 | } 18 | on_msg(fn: (x?: any) => void): void { 19 | this._on_msg = fn; 20 | } 21 | _process_msg(msg: services.KernelMessage.ICommMsgMsg): void | Promise { 22 | if (this._on_msg) { 23 | return this._on_msg(msg); 24 | } else { 25 | return Promise.resolve(); 26 | } 27 | } 28 | close(): string { 29 | if (this._on_close) { 30 | this._on_close(); 31 | } 32 | return "dummy"; 33 | } 34 | send(): string { 35 | return "dummy"; 36 | } 37 | 38 | open(): string { 39 | return "dummy"; 40 | } 41 | 42 | comm_id: string; 43 | target_name = "dummy"; 44 | _on_msg: ((x?: any) => void) | null = null; 45 | _on_close: ((x?: any) => void) | null = null; 46 | } 47 | 48 | export class DummyManager extends baseManager.ManagerBase { 49 | constructor() { 50 | super(); 51 | this.el = window.document.createElement("div"); 52 | } 53 | 54 | display_view( 55 | msg: services.KernelMessage.IMessage, 56 | view: widgets.DOMWidgetView, 57 | options: any, 58 | ) { 59 | // TODO: make this a spy 60 | // TODO: return an html element 61 | return Promise.resolve(view).then((view) => { 62 | this.el.appendChild(view.el); 63 | view.on("remove", () => console.log("view removed", view)); 64 | return view.el; 65 | }); 66 | } 67 | 68 | protected loadClass( 69 | className: string, 70 | moduleName: string, 71 | moduleVersion: string, 72 | ): Promise { 73 | if (moduleName === "@jupyter-widgets/base") { 74 | if ((widgets as any)[className]) { 75 | return Promise.resolve((widgets as any)[className]); 76 | } else { 77 | return Promise.reject(`Cannot find class ${className}`); 78 | } 79 | } else if (moduleName === "jupyter-datawidgets") { 80 | if (this.testClasses[className]) { 81 | return Promise.resolve(this.testClasses[className]); 82 | } else { 83 | return Promise.reject(`Cannot find class ${className}`); 84 | } 85 | } else { 86 | return Promise.reject(`Cannot find module ${moduleName}`); 87 | } 88 | } 89 | 90 | _get_comm_info() { 91 | return Promise.resolve({}); 92 | } 93 | 94 | _create_comm() { 95 | return Promise.resolve(new MockComm()); 96 | } 97 | 98 | el: HTMLElement; 99 | 100 | testClasses: { [key: string]: any } = {}; 101 | } 102 | 103 | export interface Constructor { 104 | new (attributes?: any, options?: any): T; 105 | } 106 | 107 | export function createTestModel( 108 | constructor: Constructor, 109 | attributes?: any, 110 | ): T { 111 | const id = widgets.uuid(); 112 | const widget_manager = new DummyManager(); 113 | const modelOptions = { 114 | widget_manager: widget_manager, 115 | model_id: id, 116 | }; 117 | 118 | return new constructor(attributes, modelOptions); 119 | } 120 | -------------------------------------------------------------------------------- /src/components.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DOMWidgetView, WidgetModel } from "@jupyter-widgets/base"; 3 | import * as base from "@jupyter-widgets/base"; 4 | 5 | const JupyterPhosphorWidget = 6 | base.JupyterPhosphorWidget || base.JupyterLuminoWidget; 7 | 8 | export class ErrorBoundary extends React.Component { 9 | constructor(props: any) { 10 | super(props); 11 | this.state = { hasError: false, error: null }; 12 | } 13 | 14 | static getDerivedStateFromError(error: any) { 15 | return { hasError: true, error }; 16 | } 17 | 18 | componentDidCatch(error: any, errorInfo: any) {} 19 | 20 | render() { 21 | // @ts-ignore 22 | if (this.state.hasError) { 23 | // @ts-ignore 24 | const error = this.state.error.message; 25 | return ( 26 |
27 |

Error

;
{error}
28 | 31 |
32 | ); 33 | } 34 | // @ts-ignore 35 | return this.props.children; 36 | } 37 | } 38 | 39 | export function JupyterWidget({ 40 | widget, 41 | view, 42 | }: { 43 | widget: WidgetModel; 44 | view: DOMWidgetView; 45 | }) { 46 | const [element, setElement] = React.useState(null); 47 | 48 | React.useLayoutEffect(() => { 49 | /* Copied from ipyvue: 50 | https://github.com/widgetti/ipyvue/blob/d307dbdceaa05f01fe22be57d157d02c9ee8fbdf/js/src/VueRenderer.js#L11 */ 51 | let currentView: any = null; 52 | let destroyed = false; 53 | 54 | if (element != null) { 55 | view.create_child_view(widget).then((newView) => { 56 | currentView = newView; 57 | 58 | // since create view is async, the component instance might be destroyed before the view is created 59 | if (!destroyed) { 60 | element.innerHTML = ""; 61 | if ( 62 | JupyterPhosphorWidget && 63 | // @ts-ignore 64 | (newView.pWidget || newView.luminoWidget || newView.lmWidget) 65 | ) { 66 | JupyterPhosphorWidget.attach( 67 | // @ts-ignore 68 | newView.pWidget || newView.luminoWidget || newView.lmWidget, 69 | element, 70 | ); 71 | } else { 72 | // @ts-ignore 73 | console.error( 74 | "Could not attach widget to DOM using Lumino or Phosphor. Fallback to normal DOM attach", 75 | JupyterPhosphorWidget, 76 | newView, 77 | ); 78 | element.appendChild(newView.el); 79 | } 80 | } else { 81 | currentView.remove(); 82 | } 83 | }); 84 | } 85 | return () => { 86 | if (currentView) { 87 | // In order to avoid an error in phosphor, we add the node to the body before removing it. 88 | // (current.remove triggers a phosphor detach) 89 | // To be sure we do not cause any flickering, we hide the node before moving it. 90 | const widget = 91 | currentView.pWidget || 92 | currentView.luminoWidget || 93 | currentView.lmWidget; 94 | widget.node.style.display = "none"; 95 | document.body.appendChild(widget.node); 96 | currentView.remove(); 97 | } else { 98 | destroyed = true; 99 | } 100 | }; 101 | }, [element, widget.cid]); 102 | 103 | return
widget placeholder
; 104 | } 105 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { transform } from "sucrase"; 2 | 3 | const muiStyleFix = ` 4 | // This is a specific 'fix' for the notebook only, since its fontsize is non-16 5 | import React from 'react'; 6 | // import PropTypes from 'prop-types'; 7 | // import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 8 | import Typography from '@mui/material/Typography'; 9 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 10 | 11 | const theme = createTheme({ 12 | typography: { 13 | // Tell Material-UI what the font-size on the html element is. 14 | htmlFontSize: 10, 15 | useNextVariants: true, 16 | }, 17 | }); 18 | 19 | function FontSizeTheme({ children }) { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | 27 | // FontSizeTheme.propTypes = { 28 | // children: PropTypes.node.isRequired, 29 | // }; 30 | 31 | export 32 | function styleWrapper(element) { 33 | if(window.Jupyter) { 34 | return {element}; 35 | } else { 36 | return element; 37 | } 38 | } 39 | `; 40 | 41 | export async function setUpMuiFixModule() { 42 | const code = transform(muiStyleFix, { 43 | transforms: ["jsx", "typescript"], 44 | filePath: "muifix.tsx", 45 | }).code; 46 | let url = URL.createObjectURL(new Blob([code], { type: "text/javascript" })); 47 | // @ts-ignore 48 | return await importShim(url); 49 | } 50 | 51 | export function expose(module: any) { 52 | const id = "_ipyreact_" + Math.random().toString(36); 53 | // @ts-ignore 54 | window[id] = module; 55 | const names = Object.keys(module) 56 | .filter((n) => n !== "default") 57 | .join(", "); 58 | return toModuleUrl(` 59 | const { ${names} } = window["${id}"]; 60 | export default window["${id}"].default; 61 | delete window["${id}"]; 62 | export { ${names} };`); 63 | } 64 | 65 | export function toModuleUrl(code: string) { 66 | return URL.createObjectURL(new Blob([code], { type: "text/javascript" })); 67 | } 68 | 69 | export async function loadScript(type: string, src: string) { 70 | const script = document.createElement("script"); 71 | script.type = type; 72 | script.src = src; 73 | script.defer = true; 74 | document.head.appendChild(script); 75 | return new Promise((resolve, reject) => { 76 | script.onload = () => { 77 | resolve(); 78 | }; 79 | script.onerror = () => { 80 | reject(); 81 | }; 82 | }); 83 | } 84 | 85 | // based on https://stackoverflow.com/a/58416333/5397207 86 | function pickSerializable(object: any, depth = 0, max_depth = 2) { 87 | // change max_depth to see more levels, for a touch event, 2 is good 88 | if (depth > max_depth) return "Object"; 89 | 90 | const obj: any = {}; 91 | for (let key in object) { 92 | let value = object[key]; 93 | if (value instanceof Window) value = "Window"; 94 | else if (value && value.getModifierState) 95 | value = pickSerializable(value, depth + 1, max_depth); 96 | else { 97 | // test if serializable 98 | try { 99 | JSON.stringify(value); 100 | } catch (e) { 101 | value = "Object"; 102 | } 103 | } 104 | obj[key] = value; 105 | } 106 | 107 | return obj; 108 | } 109 | 110 | export function eventToObject(event: any) { 111 | if (event instanceof Event || (event && event.getModifierState)) { 112 | return pickSerializable(event); 113 | } 114 | return event; 115 | } 116 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const version = require("./package.json").version; 3 | 4 | // Custom webpack rules 5 | const rules = [ 6 | { 7 | test: /\.mjs$/i, 8 | use: [ 9 | { 10 | loader: "raw-loader", 11 | options: { 12 | esModule: false, 13 | }, 14 | }, 15 | ], 16 | }, 17 | { test: /\.ts$/, loader: "ts-loader" }, 18 | { test: /\.tsx$/, loader: "ts-loader" }, 19 | { test: /\.js$/, loader: "source-map-loader" }, 20 | { test: /\.css$/, use: ["style-loader", "css-loader"] }, 21 | ]; 22 | 23 | // Packages that shouldn't be bundled but loaded at runtime 24 | const externals = ["@jupyter-widgets/base"]; 25 | 26 | const resolve = { 27 | // Add '.ts' and '.tsx' as resolvable extensions. 28 | extensions: [".webpack.js", ".web.js", ".ts", ".js", ".tsx", ".jsx"], 29 | }; 30 | 31 | const resolve16 = { 32 | // Add '.ts' and '.tsx' as resolvable extensions. 33 | extensions: [".webpack.js", ".web.js", ".ts", ".js", ".tsx", ".jsx"], 34 | alias: { 35 | react: "react16", 36 | "react-dom": "react-dom16", 37 | "react-dom/client": path.resolve(__dirname, "src/client_react16.js"), 38 | }, 39 | }; 40 | 41 | module.exports = [ 42 | /** 43 | * Notebook extension 44 | * 45 | * This bundle only contains the part of the JavaScript that is run on load of 46 | * the notebook. 47 | */ 48 | { 49 | entry: "./src/extension.ts", 50 | output: { 51 | filename: "index.js", 52 | path: path.resolve(__dirname, "ipyreact", "nbextension"), 53 | libraryTarget: "amd", 54 | publicPath: "", 55 | }, 56 | module: { 57 | rules: rules, 58 | }, 59 | devtool: "source-map", 60 | externals, 61 | resolve, 62 | }, 63 | 64 | // cannot get this to work yet 65 | // { 66 | // entry: './src/extension.ts', 67 | // output: { 68 | // filename: 'index16.js', 69 | // path: path.resolve(__dirname, 'ipyreact', 'nbextension'), 70 | // libraryTarget: 'amd', 71 | // publicPath: '', 72 | // }, 73 | // module: { 74 | // rules: rules 75 | // }, 76 | // devtool: 'source-map', 77 | // externals, 78 | // resolve: resolve16, 79 | // }, 80 | 81 | /** 82 | * Embeddable @widgetti/jupyter-react bundle 83 | * 84 | * This bundle is almost identical to the notebook extension bundle. The only 85 | * difference is in the configuration of the webpack public path for the 86 | * static assets. 87 | * 88 | * The target bundle is always `dist/index.js`, which is the path required by 89 | * the custom widget embedder. 90 | */ 91 | { 92 | entry: "./src/index.ts", 93 | output: { 94 | filename: "index.js", 95 | path: path.resolve(__dirname, "dist"), 96 | libraryTarget: "amd", 97 | library: "@widgetti/jupyter-react", 98 | publicPath: 99 | "https://unpkg.com/@widgetti/jupyter-react@" + version + "/dist/", 100 | }, 101 | devtool: "source-map", 102 | module: { 103 | rules: rules, 104 | }, 105 | externals, 106 | resolve, 107 | }, 108 | 109 | /** 110 | * Documentation widget bundle 111 | * 112 | * This bundle is used to embed widgets in the package documentation. 113 | */ 114 | { 115 | entry: "./src/index.ts", 116 | output: { 117 | filename: "embed-bundle.js", 118 | path: path.resolve(__dirname, "docs", "source", "_static"), 119 | library: "@widgetti/jupyter-react", 120 | libraryTarget: "amd", 121 | }, 122 | module: { 123 | rules: rules, 124 | }, 125 | devtool: "source-map", 126 | externals, 127 | resolve, 128 | }, 129 | ]; 130 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling>=1.3.1", 4 | "jupyterlab==3.*", 5 | ] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "ipyreact" 10 | dynamic = ["version"] 11 | description = "React for ipywidgets that just works" 12 | readme = "README.md" 13 | license = { file = "LICENSE.txt" } 14 | requires-python = ">=3.7" 15 | authors = [ 16 | { name = "Maarten A. Breddels", email = "maartenbreddels@gmail.com" }, 17 | ] 18 | keywords = [ 19 | "IPython", 20 | "Jupyter", 21 | "Widgets", 22 | ] 23 | classifiers = [ 24 | "Framework :: Jupyter", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: Science/Research", 27 | "License :: OSI Approved :: BSD License", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | ] 35 | dependencies = [ 36 | "ipywidgets>=7.0.0", 37 | "anywidget>=0.2.0", 38 | ] 39 | 40 | [project.optional-dependencies] 41 | docs = [ 42 | "jupyter_sphinx", 43 | "nbsphinx", 44 | "nbsphinx-link", 45 | "pypandoc", 46 | "pytest_check_links", 47 | "recommonmark", 48 | "sphinx>=1.5", 49 | "sphinx_rtd_theme", 50 | ] 51 | examples = [] 52 | unit-test = [ 53 | "pytest>=6.0", 54 | ] 55 | ui-test = [ 56 | "solara[pytest]", 57 | "pytest>=6.0", 58 | ] 59 | dev = [ 60 | "pre-commit", 61 | ] 62 | 63 | [project.urls] 64 | Homepage = "https://github.com/widgetti/ipyreact" 65 | 66 | [tool.hatch.version] 67 | path = "ipyreact/_version.py" 68 | 69 | [tool.hatch.build] 70 | artifacts = [ 71 | "ipyreact/nbextension/index.*", 72 | "ipyreact/labextension/*.tgz", 73 | "ipyreact/labextension", 74 | ] 75 | 76 | [tool.hatch.build.targets.wheel.shared-data] 77 | "ipyreact/nbextension" = "share/jupyter/nbextensions/jupyter-react" 78 | "ipyreact/labextension" = "share/jupyter/labextensions/@widgetti/jupyter-react" 79 | "./install.json" = "share/jupyter/labextensions/@widgetti/jupyter-react/install.json" 80 | "./ipyreact.json" = "etc/jupyter/nbconfig/notebook.d/ipyreact.json" 81 | 82 | [tool.hatch.build.targets.sdist] 83 | exclude = [ 84 | ".github", 85 | ] 86 | 87 | [tool.hatch.build.hooks.jupyter-builder] 88 | build-function = "hatch_jupyter_builder.npm_builder" 89 | ensured-targets = [ 90 | "ipyreact/nbextension/index.js", 91 | "ipyreact/labextension/package.json", 92 | ] 93 | skip-if-exists = [ 94 | "ipyreact/nbextension/index.js", 95 | "ipyreact/labextension/package.json", 96 | ] 97 | dependencies = [ 98 | "hatch-jupyter-builder>=0.5.0", 99 | ] 100 | 101 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 102 | path = "." 103 | build_cmd = "build:prod" 104 | 105 | [tool.tbump] 106 | field = [ 107 | { name = "channel", default = "" }, 108 | { name = "release", default = "" }, 109 | ] 110 | file = [ 111 | { src = "pyproject.toml", version_template = "version = \"{major}.{minor}.{patch}{channel}{release}\"" }, 112 | { src = "ipyreact/_version.py" }, 113 | ] 114 | 115 | [tool.tbump.version] 116 | current = "0.1.0" 117 | regex = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)((?Pa|b|rc|.dev)(?P\\d+))?" 118 | 119 | [tool.tbump.git] 120 | message_template = "Bump to {new_version}" 121 | tag_template = "v{new_version}" 122 | 123 | [tool.ruff] 124 | fix = true 125 | exclude = [ 126 | '.git', 127 | 'dist', 128 | '.eggs', 129 | ] 130 | line-length = 160 131 | select = ["E", "W", "F", "Q", "I"] 132 | 133 | [tool.ruff.per-file-ignores] 134 | "__init__.py" = ["F401"] 135 | "docs/source/conf.py" = ["E402"] 136 | -------------------------------------------------------------------------------- /tests/ui/basics_test.py: -------------------------------------------------------------------------------- 1 | import ipywidgets 2 | import playwright.sync_api 3 | from solara import display 4 | 5 | import ipyreact 6 | 7 | 8 | def test_children_text(solara_test, page_session: playwright.sync_api.Page): 9 | def on_click(event_data): 10 | b.children = ["Clicked"] 11 | 12 | b = ipyreact.Widget( 13 | _type="button", 14 | events={"onClick": on_click}, 15 | children=["Click me"], 16 | props={"class": "test-button"}, 17 | ) 18 | 19 | display(b) 20 | button = page_session.locator(".test-button") 21 | button.click() 22 | page_session.locator(".test-button >> text=Clicked").wait_for() 23 | 24 | 25 | def test_children_react(solara_test, page_session: playwright.sync_api.Page): 26 | def on_click(event_data): 27 | b.children = [ipyreact.Widget(_type="span", props={"className": "test-span"}, children=["direct child"])] 28 | 29 | b = ipyreact.Widget( 30 | _type="button", 31 | events={"onClick": on_click}, 32 | children=["Click me"], 33 | props={"className": "test-button"}, 34 | ) 35 | 36 | display(b) 37 | button = page_session.locator(".test-button") 38 | button.click() 39 | # direct child, not a grandchild (e.g. extra div around it) 40 | page_session.locator(".test-button > .test-span").wait_for() 41 | 42 | 43 | def test_children_ipywidgets(solara_test, page_session: playwright.sync_api.Page): 44 | def on_click(event_data): 45 | html = ipywidgets.HTML(value="not a direct child") 46 | html.add_class("test-html") 47 | b.children = [html] 48 | 49 | b = ipyreact.Widget( 50 | _type="button", 51 | events={"onClick": on_click}, 52 | children=["Click me"], 53 | props={"className": "test-button"}, 54 | ) 55 | 56 | display(b) 57 | button = page_session.locator(".test-button") 58 | button.click() 59 | # not per se a direct child 60 | page_session.locator(".test-button >> .test-html").wait_for() 61 | 62 | 63 | def test_update_children_after_create(solara_test, page_session: playwright.sync_api.Page): 64 | b = ipyreact.Widget( 65 | _type="button", 66 | children=["Initial"], 67 | props={"class": "test-button"}, 68 | ) 69 | b.children = ["Updated"] 70 | 71 | display(b) 72 | page_session.locator(".test-button >> text=Updated").wait_for() 73 | 74 | 75 | def test_update_props_after_create(solara_test, page_session: playwright.sync_api.Page): 76 | b = ipyreact.Widget( 77 | _type="button", 78 | children=["Button"], 79 | props={"class": "test-button-initial"}, 80 | ) 81 | b.props = {"class": "test-button-updated"} 82 | 83 | display(b) 84 | page_session.locator(".test-button-updated >> text=Button").wait_for() 85 | 86 | 87 | def test_error_module(solara_test, page_session: playwright.sync_api.Page): 88 | b = ipyreact.Widget( 89 | _type="Foo", 90 | _module="bar", 91 | children=["should not be shown"], 92 | ) 93 | display(b) 94 | page_session.locator("text=Unable to resolve specifier").wait_for() 95 | 96 | 97 | def test_error_type(solara_test, page_session: playwright.sync_api.Page): 98 | ipyreact.define_module( 99 | "my-module", 100 | """ 101 | import * as React from "react"; 102 | 103 | export function ClickButton({value, setValue}) { 104 | return React.createElement("button", { 105 | className: "counter-widget", 106 | onClick: () => setValue(value + 1), 107 | children: [`${value|| 0} clicks`], 108 | }) 109 | }; 110 | """, 111 | ) 112 | 113 | b = ipyreact.ValueWidget(_module="my-module", _type="ClickButtonMistyped") 114 | display(b) 115 | page_session.locator("text=no component ClickButtonMistyped found in module my-module").wait_for() 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@widgetti/jupyter-react", 3 | "version": "0.5.0", 4 | "description": "React for ipywidgets that just works", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "widgets" 10 | ], 11 | "files": [ 12 | "lib/**/*.js", 13 | "dist/*.js", 14 | "css/*.css" 15 | ], 16 | "homepage": "https://github.com/widgetti/ipyreact", 17 | "bugs": { 18 | "url": "https://github.com/widgetti/ipyreact/issues" 19 | }, 20 | "license": "BSD-3-Clause", 21 | "author": { 22 | "name": "Maarten A. Breddels", 23 | "email": "maartenbreddels@gmail.com" 24 | }, 25 | "main": "lib/index.js", 26 | "types": "./lib/index.d.ts", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/widgetti/ipyreact" 30 | }, 31 | "scripts": { 32 | "build": "yarn run build:lib && yarn run build:nbextension && yarn run build:labextension:dev", 33 | "build:prod": "yarn run build:lib && yarn run build:nbextension && yarn run build:labextension", 34 | "build:labextension": "jupyter labextension build .", 35 | "build:labextension:dev": "jupyter labextension build --development True .", 36 | "build:lib": "tsc", 37 | "build:nbextension": "webpack", 38 | "clean": "yarn run clean:lib && yarn run clean:nbextension && yarn run clean:labextension", 39 | "clean:lib": "rimraf lib", 40 | "clean:labextension": "rimraf ipyreact/labextension", 41 | "clean:nbextension": "rimraf ipyreact/nbextension/static/index.js", 42 | "prepack": "yarn run build:lib", 43 | "test": "jest", 44 | "watch": "npm-run-all -p watch:*", 45 | "watch:lib": "tsc -w", 46 | "watch:nbextension": "webpack --watch --mode=development", 47 | "watch:labextension": "jupyter labextension watch ." 48 | }, 49 | "dependencies": { 50 | "@babel/preset-typescript": "^7.21.0", 51 | "@babel/standalone": "^7.21.3", 52 | "@jupyter-widgets/base": "^1.1.10 || ^2 || ^3 || ^4 || ^5 || ^6", 53 | "@types/react": "^18.0.29", 54 | "@types/react-reconciler": "^0.28.8", 55 | "babel-plugin-import-map": "^1.0.0", 56 | "es-module-shims": "^2.0.0", 57 | "esbuild": "^0.17.14", 58 | "lodash": "^4.17.21", 59 | "raw-loader": "^4.0.2", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0", 62 | "react-reconciler": "^0.29.0", 63 | "sucrase": "^3.30.0" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.5.0", 67 | "@babel/preset-env": "^7.5.0", 68 | "@jupyter-widgets/base-manager": "^1.0.2", 69 | "@jupyterlab/builder": "^3.0.0", 70 | "@lumino/application": "^1.6.0", 71 | "@lumino/widgets": "^1.6.0", 72 | "@types/babel__standalone": "^7.1.4", 73 | "@types/jest": "^26.0.0", 74 | "@types/react-dom": "^18.0.11", 75 | "@types/webpack-env": "^1.13.6", 76 | "@typescript-eslint/eslint-plugin": "^3.6.0", 77 | "@typescript-eslint/parser": "^3.6.0", 78 | "acorn": "^7.2.0", 79 | "css-loader": "^3.2.0", 80 | "eslint": "^7.4.0", 81 | "eslint-config-prettier": "^6.11.0", 82 | "eslint-plugin-prettier": "^3.1.4", 83 | "fs-extra": "^7.0.0", 84 | "identity-obj-proxy": "^3.0.0", 85 | "jest": "^29.7.0", 86 | "mkdirp": "^0.5.1", 87 | "npm-run-all": "^4.1.3", 88 | "prettier": "^2.0.5", 89 | "rimraf": "^2.6.2", 90 | "source-map-loader": "^1.1.3", 91 | "style-loader": "^1.0.0", 92 | "ts-jest": "^29.2.5", 93 | "ts-loader": "^8.0.0", 94 | "typescript": "^5.7.2", 95 | "webpack": "^5.61.0", 96 | "webpack-cli": "^4.0.0" 97 | }, 98 | "jupyterlab": { 99 | "extension": "lib/plugin", 100 | "outputDir": "ipyreact/labextension/", 101 | "sharedPackages": { 102 | "@jupyter-widgets/base": { 103 | "bundled": false, 104 | "singleton": true 105 | }, 106 | "react": false, 107 | "react-dom": false 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "v*" 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: "16.x" 23 | 24 | - name: Linting 25 | if: ${{ matrix.os == 'ubuntu-latest' }} 26 | run: | 27 | yarn run lint:check 28 | 29 | - name: Set up Python 3.8 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: 3.8 33 | 34 | - name: Install dependencies 35 | run: pip install hatch wheel "jupyterlab<4" 36 | 37 | # - name: Change version for non-releases 38 | # if: ${{ ! startsWith(github.event.ref, 'refs/tags/v')}} 39 | # run: python .github/ci_version.py 40 | 41 | - name: Package js 42 | run: (npm ci && npm run build:prod && npm pack) 43 | 44 | - name: Package 45 | run: hatch build 46 | 47 | - name: Upload builds 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: ipyreact-dist 51 | path: | 52 | ./dist 53 | ./*.tgz 54 | 55 | test: 56 | needs: [build] 57 | runs-on: ubuntu-20.04 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - uses: actions/download-artifact@v4 67 | with: 68 | name: ipyreact-dist 69 | 70 | - name: Install Python 71 | uses: actions/setup-python@v5 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | 75 | - name: Install 76 | run: pip install `echo dist/*.whl`[unit-test] "jupyter_server<2" 77 | 78 | - name: Run unit tests 79 | run: pytest tests/unit 80 | 81 | ui-test: 82 | needs: [build] 83 | runs-on: ubuntu-20.04 84 | steps: 85 | - uses: actions/checkout@v4 86 | 87 | - uses: actions/download-artifact@v4 88 | with: 89 | name: ipyreact-dist 90 | 91 | - name: Install Python 92 | uses: actions/setup-python@v5 93 | with: 94 | python-version: 3.8 95 | 96 | - name: Install ipyreact 97 | run: pip install `echo dist/*.whl`[ui-test] "jupyter_server<2" 98 | 99 | - name: Install playwright browsers 100 | run: playwright install chromium 101 | 102 | - name: Run ui-tests 103 | run: pytest tests/ui/ --video=retain-on-failure 104 | 105 | - name: Upload Test artifacts 106 | if: always() 107 | uses: actions/upload-artifact@v4 108 | with: 109 | name: ipyreact-test-results-${{ github.run_number }} 110 | path: test-results 111 | 112 | release: 113 | if: startsWith(github.event.ref, 'refs/tags/v') 114 | needs: [test, ui-test] 115 | runs-on: ubuntu-20.04 116 | steps: 117 | - uses: actions/download-artifact@v4 118 | with: 119 | name: ipyreact-dist 120 | 121 | - name: Install node 122 | uses: actions/setup-node@v1 123 | with: 124 | node-version: "16.x" 125 | registry-url: "https://registry.npmjs.org" 126 | 127 | - name: Install Python 128 | uses: actions/setup-python@v5 129 | with: 130 | python-version: 3.8 131 | 132 | - name: Install dependencies 133 | run: | 134 | python -m pip install --upgrade pip 135 | pip install twine wheel jupyter-packaging jupyterlab 136 | 137 | - name: Publish the Python package 138 | env: 139 | TWINE_USERNAME: __token__ 140 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 141 | run: twine upload --skip-existing dist/*.whl dist/*.tar.gz 142 | 143 | - name: Publish the NPM package 144 | run: | 145 | echo $PRE_RELEASE 146 | if [[ $PRE_RELEASE == "true" ]]; then export TAG="next"; else export TAG="latest"; fi 147 | npm publish --tag ${TAG} --access public *.tgz 148 | env: 149 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 150 | PRE_RELEASE: ${{ github.event.release.prerelease }} 151 | -------------------------------------------------------------------------------- /ipyreact/widget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Maarten A. Breddels. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | """ 8 | TODO: Add module docstring 9 | """ 10 | 11 | import typing as t 12 | import warnings 13 | from pathlib import Path 14 | 15 | import anywidget 16 | from ipywidgets import ValueWidget as ValueWidgetClassic 17 | from ipywidgets import Widget, widget_serialization 18 | from traitlets import Any, Bool, Dict, Int, List, Unicode, default, observe 19 | 20 | from ._frontend import module_name, module_version 21 | 22 | HERE = Path(__file__).parent 23 | 24 | 25 | class Widget(anywidget.AnyWidget): 26 | """TODO: Add docstring here""" 27 | 28 | _model_name = Unicode("ReactModel").tag(sync=True) 29 | _model_module = Unicode(module_name).tag(sync=True) 30 | _model_module_version = Unicode(module_version).tag(sync=True) 31 | _view_name = Unicode("ReactView").tag(sync=True) 32 | _view_module = Unicode(module_name).tag(sync=True) 33 | _view_module_version = Unicode(module_version).tag(sync=True) 34 | props = Dict({}, allow_none=True).tag(sync=True, **widget_serialization) 35 | children = List(t.cast(t.List[t.Union[Widget, str]], [])).tag(sync=True, **widget_serialization) 36 | 37 | # this stays on the python side 38 | events = Dict({}) 39 | # this is send of the frontend (keys of events) 40 | _event_names = List(Unicode(), allow_none=True).tag(sync=True) 41 | _debug = Bool(False).tag(sync=True) 42 | _type = Unicode(None, allow_none=True).tag(sync=True) 43 | _dependencies = List(Unicode(), allow_none=True).tag(sync=True) 44 | _module = Unicode(None, allow_none=True).tag(sync=True) 45 | _react_version = Int(18).tag(sync=True) 46 | _cdn = Unicode("https://esm.sh/").tag 47 | 48 | # anywidget doesn't like if _esm isn't there 49 | _esm = "" 50 | 51 | def __init__(self, **kwargs) -> None: 52 | _esm = kwargs.pop("_esm", None) 53 | if _esm is not None: 54 | extra_traits = {} 55 | if isinstance(_esm, str): 56 | extra_traits["_esm"] = Unicode(str(_esm)).tag(sync=True) 57 | elif isinstance(_esm, Path): 58 | from anywidget._util import try_file_contents 59 | 60 | self._esm = try_file_contents(_esm) 61 | 62 | self.add_traits(**extra_traits) 63 | _ignore = ["on_msg", "on_displayed", "on_trait_change", "on_widget_constructed"] 64 | events = kwargs.pop("events", {}) 65 | for method_name in dir(self): 66 | if method_name.startswith("event_") and method_name not in _ignore: 67 | event_name = method_name[len("event_") :] 68 | method = getattr(self, method_name) 69 | if method_name not in events: 70 | events[event_name] = method 71 | _event_names = list(events) 72 | super().__init__(**{"_event_names": _event_names, "events": events, **kwargs}) 73 | self.on_msg(self._handle_event) 74 | 75 | def _handle_event(self, _, content, buffers): 76 | if "event_name" in content.keys(): 77 | event_name = content.get("event_name", "") 78 | data = content.get("data", {}) 79 | event_hander = self.events.get(event_name, None) 80 | if event_hander is None: 81 | return 82 | if "data" not in content: 83 | event_hander() 84 | else: 85 | if buffers: 86 | event_hander(data, buffers) 87 | else: 88 | event_hander(data) 89 | 90 | @observe("events") 91 | def _events(self, change): 92 | self.event_names = list(change["new"].keys()) 93 | 94 | @default("_dependencies") 95 | def _default_dependencies(self): 96 | import ipyreact.module 97 | 98 | return ipyreact.module.get_module_names() 99 | 100 | 101 | class ValueWidget(Widget, ValueWidgetClassic): 102 | # the ValueWidget from ipywidgets does not add sync=True to the value trait 103 | value = Any(help="The value of the widget.").tag(sync=True) 104 | 105 | 106 | # this is deprecated 107 | class ReactWidget(ValueWidget): 108 | _esm = HERE / Path("basic.tsx") 109 | 110 | def __init__(self, **kwargs) -> None: 111 | warnings.warn("ReactWidget is deprecated, use Widget or ValueWidget instead", DeprecationWarning) 112 | super().__init__(**kwargs) 113 | -------------------------------------------------------------------------------- /tests/ui/module_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import playwright.sync_api 4 | from solara import display 5 | 6 | HERE = Path(__file__).parent 7 | 8 | 9 | def test_module(ipywidgets_runner, page_session: playwright.sync_api.Page, assert_solara_snapshot): 10 | def kernel_code(): 11 | import ipyreact 12 | 13 | ipyreact.define_module( 14 | "my-module", 15 | """ 16 | import * as React from "react"; 17 | 18 | export function ClickButton({value, setValue}) { 19 | return React.createElement("button", { 20 | className: "counter-widget", 21 | onClick: () => setValue(value + 1), 22 | children: [`${value|| 0} clicks`], 23 | }) 24 | }; 25 | """, 26 | ) 27 | 28 | b = ipyreact.ValueWidget(_module="my-module", _type="ClickButton") 29 | display(b) 30 | 31 | ipywidgets_runner(kernel_code) 32 | counter = page_session.locator(".counter-widget") 33 | counter.click() 34 | page_session.locator("text=1 clicks").wait_for() 35 | 36 | 37 | # def test_threejs_fiber(ipywidgets_runner, page_session: playwright.sync_api.Page, assert_solara_snapshot): 38 | # bundle_path = HERE.parent.parent / "examples/threejs-fiber/threejs-fiber.bundle.js" 39 | # bundle_path_copy = bundle_path 40 | 41 | # def kernel_code(bundle_path=None): 42 | # from pathlib import Path 43 | 44 | # import ipyreact 45 | 46 | # if bundle_path is None: 47 | # # this only happens in solara 48 | # bundle_path = str(bundle_path_copy) # type: ignore 49 | 50 | # ipyreact.define_module("threejs-fiber", Path(bundle_path)) 51 | # # import ipyreact.importmap 52 | 53 | # # ipyreact.importmap._import_map_widget = None 54 | # # ipyreact.importmap._update_import_map() 55 | 56 | # class BoxWidget(ipyreact.Widget): 57 | # _esm = """ 58 | # import React, { useRef, useState } from "react" 59 | # import { Canvas, useFrame, useThree } from 'threejs-fiber' 60 | # import { OrbitControls } from "threejs-fiber"; 61 | 62 | # export default function Box({position, color}) { 63 | # const ref = useRef() 64 | # useFrame(() => (ref.current.rotation.x = ref.current.rotation.y += 0.01)) 65 | 66 | # return ( 67 | # 68 | # 69 | # 70 | # 71 | # ) 72 | # } 73 | 74 | # """ 75 | 76 | # import solara 77 | 78 | # @solara.component 79 | # def Box(position, color, props={}, events={}, children=[]): 80 | # return BoxWidget.element( 81 | # props={**props, **dict(color=color, position=position)}, 82 | # events=events, 83 | # children=children, 84 | # ) 85 | 86 | # @solara.component 87 | # def Canvas(props={}, events={}, children=[]): 88 | # return ipyreact.Widget.element( 89 | # props=props, 90 | # events=events, 91 | # children=children, 92 | # _type="Canvas", 93 | # _module="threejs-fiber", 94 | # ) 95 | 96 | # @solara.component 97 | # def OrbitControls(props={}, events={}, children=[]): 98 | # return ipyreact.Widget.element( 99 | # _type="OrbitControls", 100 | # _module="threejs-fiber", 101 | # props=props, 102 | # events=events, 103 | # children=children, 104 | # ) 105 | 106 | # @solara.component 107 | # def DirectionalLight(props={}, events={}, children=[]): 108 | # # starts with a lower case, should be available globally, so we don't need to pass 109 | # # _module="threejs-fiber" 110 | # return ipyreact.Widget.element(_type="directionalLight", props=props, events=events, children=children) 111 | 112 | # @solara.component 113 | # def Div(style={}, props={}, events={}, children=[]): 114 | # # we use a ipyreact based div to avoid an extra wrapper div which will affect layout 115 | # return ipyreact.Widget.element(_type="div", props={**props, **dict(style=style)}, children=children, events=events) 116 | 117 | # boxes = solara.reactive( 118 | # [ 119 | # ([-1, 0, 3], "#18a36e"), 120 | # ([1, 0, 3], "#f56f42"), 121 | # ] 122 | # ) 123 | 124 | # @solara.component 125 | # def Page(): 126 | # with Div(style={"height": "600px"}): 127 | # # a canvas fill the available space, so we add a parent div with height 128 | # with Canvas(): 129 | # for position, color in boxes.value: 130 | # Box(position=position, color=color) 131 | # OrbitControls() 132 | # DirectionalLight(props=dict(color="#ffffff", intensity=1, position=[-1, 2, 4])) 133 | 134 | # display(Page()) 135 | 136 | # ipywidgets_runner(kernel_code, locals=dict(bundle_path=str(bundle_path))) 137 | # page_session.locator(".jupyter-react-widget >> canvas").wait_for() 138 | -------------------------------------------------------------------------------- /examples/antd/antd.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "a01f370f", 6 | "metadata": {}, 7 | "source": [ 8 | "## Bundled ES modules\n", 9 | "\n", 10 | "While esm.sh is convenient to use, for production use, we recommend creating a standalone bundle. This will load faster and will not require a direct connection to esm.sh, which might not be available in airgapped or firewalled environments.\n", 11 | "\n", 12 | "We will not create a minimal bundle for https://ant.design/\n", 13 | "\n", 14 | "First create a simple file called `antd-minimal.js` that exports what we need.\n", 15 | "\n", 16 | "```javascript\n", 17 | "export {Button, Flex, Slider} from 'antd';\n", 18 | "```\n", 19 | "\n", 20 | "Next, we install the libraries:\n", 21 | "\n", 22 | "```bash\n", 23 | "$ npm install antd\n", 24 | "```\n", 25 | "\n", 26 | "And use ESBuild to turn this into a self-contained module/bundle, without react, since ipyreact provides that for us.\n", 27 | "\n", 28 | "```\n", 29 | "$ npx esbuild ./antd-minimal.js --bundle --outfile=./antd-minimal.esm.js --format=esm --external:react --external:react-dom --target=esnext\n", 30 | "```\n", 31 | "\n", 32 | "\n", 33 | "Now we can define the module with a custom name (we call it antd-minimal)." 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "ea16141d", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "%pip install -q ipyreact\n", 44 | "# This line is needed for JupyterLite" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "id": "80b55518", 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "import ipyreact\n", 55 | "from pathlib import Path\n", 56 | "\n", 57 | "ipyreact.define_module(\"antd-minimal\", Path(\"./antd-minimal.esm.js\"))" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "fe4844cf", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "def on_click(event_data):\n", 68 | " w.children = [\"Clicked\"]\n", 69 | "\n", 70 | "w = ipyreact.Widget(_module=\"antd-minimal\", _type=\"Button\", children=[\"Hi there\"], events={\"onClick\": on_click})\n", 71 | "w" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "id": "22570a65", 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "stack = ipyreact.Widget(_module=\"antd-minimal\", _type=\"Flex\",\n", 82 | " props={\"vertical\": True, \"style\": {\"padding\": \"24px\"}},\n", 83 | " children=[\n", 84 | " ipyreact.Widget(_module=\"antd-minimal\", _type=\"Button\", children=[\"Ant Design Button\"]),\n", 85 | " ipyreact.Widget(_module=\"antd-minimal\", _type=\"Slider\",\n", 86 | " props={\"defaultValue\": 3, \"min\": 0, \"max\": 11}),\n", 87 | "])\n", 88 | "stack" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "4d87b64e", 94 | "metadata": {}, 95 | "source": [ 96 | "Input components might need a little bit of custom code, and subclassing `ValueWidget`. It often means binding the value to the right prop of the input component (in this case the Slider takes the same name, `value`) and coupling the event handler (in this case `onChange`) to the `setValue` function." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "559b5234", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "import traitlets\n", 107 | "\n", 108 | "\n", 109 | "class Slider(ipyreact.ValueWidget):\n", 110 | " _esm = \"\"\"\n", 111 | " import * as React from \"react\";\n", 112 | " import {Slider} from \"antd-minimal\"\n", 113 | " \n", 114 | " export default ({value, setValue, ...rest}) => {\n", 115 | " return setValue(v)} {...rest}/>\n", 116 | " }\n", 117 | " \n", 118 | " \"\"\"\n", 119 | "s = Slider(value=2)\n", 120 | "s" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "id": "0fc7b9a4", 126 | "metadata": {}, 127 | "source": [ 128 | "*Note that it depends on the implementation of the event handler if the value is being passed directly, or a (synthetic) event with the data will be passed as argument. An typical example event handler could be `onChange={(event) => setValue(event.target.value)}`.*\n", 129 | "\n", 130 | "Now the slider widget is stateful, and we have bi-directional communication using the `.value` trait.\n", 131 | "For instance, we can read it:" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "id": "2c77a4b4", 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "s.value" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "id": "6d2642e1", 147 | "metadata": {}, 148 | "source": [ 149 | "Or write to it, and it will be reflected directly in the UI." 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": null, 155 | "id": "83b2686f", 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "s.value = 10" 160 | ] 161 | } 162 | ], 163 | "metadata": { 164 | "kernelspec": { 165 | "display_name": "Python 3 (ipykernel)", 166 | "language": "python", 167 | "name": "python3" 168 | }, 169 | "language_info": { 170 | "codemirror_mode": { 171 | "name": "ipython", 172 | "version": 3 173 | }, 174 | "file_extension": ".py", 175 | "mimetype": "text/x-python", 176 | "name": "python", 177 | "nbconvert_exporter": "python", 178 | "pygments_lexer": "ipython3", 179 | "version": "3.9.16" 180 | } 181 | }, 182 | "nbformat": 4, 183 | "nbformat_minor": 5 184 | } 185 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # ipyreact documentation build configuration file 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | 16 | # -- General configuration ------------------------------------------------ 17 | 18 | # If your documentation needs a minimal Sphinx version, state it here. 19 | # 20 | # needs_sphinx = '1.0' 21 | 22 | # Add any Sphinx extension module names here, as strings. They can be 23 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 24 | # ones. 25 | extensions = [ 26 | "sphinx.ext.autodoc", 27 | "sphinx.ext.viewcode", 28 | "sphinx.ext.intersphinx", 29 | "sphinx.ext.napoleon", 30 | "sphinx.ext.todo", 31 | "nbsphinx", 32 | "jupyter_sphinx", 33 | "nbsphinx_link", 34 | ] 35 | 36 | # Set the nbsphinx JS path to empty to avoid showing twice of the widgets 37 | nbsphinx_requirejs_path = "" 38 | nbsphinx_widgets_path = "" 39 | 40 | # Ensure our extension is available: 41 | import sys 42 | from os.path import dirname 43 | from os.path import join as pjoin 44 | 45 | docs = dirname(dirname(__file__)) 46 | root = dirname(docs) 47 | sys.path.insert(0, root) 48 | sys.path.insert(0, pjoin(docs, "sphinxext")) 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = ".rst" 58 | 59 | # The master toctree document. 60 | master_doc = "index" 61 | 62 | # General information about the project. 63 | project = "ipyreact" 64 | copyright = "2023, Maarten A. Breddels" 65 | author = "Maarten A. Breddels" 66 | 67 | # The version info for the project you're documenting, acts as replacement for 68 | # |version| and |release|, also used in various other places throughout the 69 | # built documents. 70 | # 71 | # The short X.Y version. 72 | 73 | 74 | # get version from python package: 75 | import os 76 | 77 | here = os.path.dirname(__file__) 78 | repo = os.path.join(here, "..", "..") 79 | _version_py = os.path.join(repo, "ipyreact", "_version.py") 80 | version_ns = {} 81 | with open(_version_py) as f: 82 | exec(f.read(), version_ns) 83 | 84 | # The short X.Y version. 85 | version = "%i.%i" % version_ns["version_info"][:2] 86 | # The full version, including alpha/beta/rc tags. 87 | release = version_ns["__version__"] 88 | 89 | # The language for content autogenerated by Sphinx. Refer to documentation 90 | # for a list of supported languages. 91 | # 92 | # This is also used if you do content translation via gettext catalogs. 93 | # Usually you set "language" from the command line for these cases. 94 | language = "en" 95 | 96 | # List of patterns, relative to source directory, that match files and 97 | # directories to ignore when looking for source files. 98 | # This patterns also effect to html_static_path and html_extra_path 99 | exclude_patterns = ["**.ipynb_checkpoints"] 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = "sphinx" 103 | 104 | # If true, `todo` and `todoList` produce output, else they produce nothing. 105 | todo_include_todos = False 106 | 107 | 108 | # -- Options for HTML output ---------------------------------------------- 109 | 110 | 111 | # Theme options are theme-specific and customize the look and feel of a theme 112 | # further. For a list of options available for each theme, see the 113 | # documentation. 114 | # 115 | # html_theme_options = {} 116 | 117 | # Add any paths that contain custom static files (such as style sheets) here, 118 | # relative to this directory. They are copied after the builtin static files, 119 | # so a file named "default.css" will overwrite the builtin "default.css". 120 | html_static_path = ["_static"] 121 | 122 | 123 | # -- Options for HTMLHelp output ------------------------------------------ 124 | 125 | # Output file base name for HTML help builder. 126 | htmlhelp_basename = "ipyreactdoc" 127 | 128 | 129 | # -- Options for LaTeX output --------------------------------------------- 130 | 131 | latex_elements = { 132 | # The paper size ('letterpaper' or 'a4paper'). 133 | # 134 | # 'papersize': 'letterpaper', 135 | # The font size ('10pt', '11pt' or '12pt'). 136 | # 137 | # 'pointsize': '10pt', 138 | # Additional stuff for the LaTeX preamble. 139 | # 140 | # 'preamble': '', 141 | # Latex figure (float) alignment 142 | # 143 | # 'figure_align': 'htbp', 144 | } 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, 148 | # author, documentclass [howto, manual, or own class]). 149 | latex_documents = [ 150 | (master_doc, "ipyreact.tex", "ipyreact Documentation", "Maarten A. Breddels", "manual"), 151 | ] 152 | 153 | 154 | # -- Options for manual page output --------------------------------------- 155 | 156 | # One entry per manual page. List of tuples 157 | # (source start file, name, description, authors, manual section). 158 | man_pages = [(master_doc, "ipyreact", "ipyreact Documentation", [author], 1)] 159 | 160 | 161 | # -- Options for Texinfo output ------------------------------------------- 162 | 163 | # Grouping the document tree into Texinfo files. List of tuples 164 | # (source start file, target name, title, author, 165 | # dir menu entry, description, category) 166 | texinfo_documents = [ 167 | ( 168 | master_doc, 169 | "ipyreact", 170 | "ipyreact Documentation", 171 | author, 172 | "ipyreact", 173 | "React for ipywidgets that just works", 174 | "Miscellaneous", 175 | ), 176 | ] 177 | 178 | 179 | # Example configuration for intersphinx: refer to the Python standard library. 180 | intersphinx_mapping = {"https://docs.python.org/": None} 181 | 182 | # Read The Docs 183 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from 184 | # docs.readthedocs.org 185 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 186 | 187 | if not on_rtd: # only import and set the theme if we're building docs locally 188 | import sphinx_rtd_theme 189 | 190 | html_theme = "sphinx_rtd_theme" 191 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 192 | 193 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 194 | 195 | 196 | # Uncomment this line if you have know exceptions in your included notebooks 197 | # that nbsphinx complains about: 198 | # 199 | nbsphinx_allow_errors = True # exception ipstruct.py ipython_genutils 200 | 201 | from sphinx.util import logging 202 | 203 | logger = logging.getLogger(__name__) 204 | 205 | 206 | def setup(app): 207 | def add_scripts(app): 208 | for fname in ["helper.js", "embed-bundle.js"]: 209 | if not os.path.exists(os.path.join(here, "_static", fname)): 210 | logger.warning("missing javascript file: %s" % fname) 211 | app.add_js_file(fname) 212 | 213 | app.connect("builder-inited", add_scripts) 214 | -------------------------------------------------------------------------------- /examples/threejs-fiber/threejs-fiber.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0ba18ea4", 6 | "metadata": {}, 7 | "source": [ 8 | "## Creating the ES module\n", 9 | "We will first create a single self contained ES module, since using threejs-fiber from esm.sh does not work flawlessly.\n", 10 | "\n", 11 | "First create a simple file called `threejs-fiber.js` that exports what we need.\n", 12 | "\n", 13 | "```javascript\n", 14 | "export * from '@react-three/fiber'\n", 15 | "export * from \"@react-three/drei\";\n", 16 | "```\n", 17 | "\n", 18 | "Next, we install the libraries:\n", 19 | "\n", 20 | "```bash\n", 21 | "$ npm install @react-three/drei@9.68.6 @react-three/fiber@8.13.0\n", 22 | "```\n", 23 | "\n", 24 | "And use ESBuild to turn this into a self-contained module/bundle, without react, since ipyreact provides that for us.\n", 25 | "\n", 26 | "```\n", 27 | "$ npx esbuild ./threejs-fiber.js --bundle --outfile=./threejs-fiber.bundle.js --format=esm --external:react --external:react-dom --external:react-reconciler --external:react-reconciler/constants --target=esnext\n", 28 | "```\n", 29 | "\n", 30 | "\n", 31 | "Now we can define the module with a custom name (we call it threejs-fiber)." 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "b04a717e", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "%pip install -q ipyreact\n", 42 | "# This line is needed for JupyterLite" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "60544de0", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "import ipyreact\n", 53 | "from pathlib import Path\n", 54 | "ipyreact.define_module(\"threejs-fiber\", Path(\"./threejs-fiber.bundle.js\"))" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "c495be0f", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "from traitlets import default\n", 65 | "\n", 66 | "\n", 67 | "class BoxWidget(ipyreact.Widget):\n", 68 | " _esm = \"\"\"\n", 69 | " import React, { useRef, useState } from \"react\"\n", 70 | " import { Canvas, useFrame, useThree } from 'threejs-fiber'\n", 71 | " import { OrbitControls } from \"threejs-fiber\";\n", 72 | "\n", 73 | " export default function Box({position, color}) {\n", 74 | " const ref = useRef()\n", 75 | " useFrame(() => (ref.current.rotation.x = ref.current.rotation.y += 0.01))\n", 76 | "\n", 77 | " return (\n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " )\n", 83 | " }\n", 84 | "\n", 85 | " \"\"\"" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "id": "401762c0", 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "id": "74751bd8", 100 | "metadata": { 101 | "scrolled": false 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "import random\n", 106 | "\n", 107 | "\n", 108 | "def random_color():\n", 109 | " # Generates a random hex color code\n", 110 | " return \"#\" + ''.join([random.choice('0123456789ABCDEF') for _ in range(6)])\n", 111 | "\n", 112 | "def add(_ignore=None):\n", 113 | " x = random.random() * 4 - 2\n", 114 | " z = random.random() * 4 - 1\n", 115 | " color = random_color() # Call the random_color function to get a random color\n", 116 | " box = BoxWidget(props=dict(position=[x, 0, z], color=color)) # Use the random color for the box\n", 117 | " canvas.children = [*canvas.children, box]\n", 118 | "\n", 119 | "canvas = ipyreact.Widget(_type=\"Canvas\", _module=\"threejs-fiber\",\n", 120 | " events=dict(onClick=add),\n", 121 | " children=[\n", 122 | " BoxWidget(props=dict(position=[-1, 0, 3], color=\"#18a36e\")),\n", 123 | " BoxWidget(props=dict(position=[1, 0, 3], color=\"#f56f42\")),\n", 124 | " ipyreact.Widget(_type=\"OrbitControls\", _module=\"threejs-fiber\"),\n", 125 | " # seems that if it starts with a small letter, it's globally available, and not exported\n", 126 | " # from the threejs-fiber module, therefore we do not pass _module=\"threejs-fiber\"\n", 127 | " ipyreact.Widget(_type=\"directionalLight\",\n", 128 | " props=dict(color=\"#ffffff\", intensity=1, position=[-1, 2, 4]))\n", 129 | " ]\n", 130 | ")\n", 131 | "\n", 132 | "# the canvas fills the parent, so wrap it in a div with the fixed height\n", 133 | "ipyreact.Widget(_type=\"div\", props=dict(style=dict(height=\"600px\")), children=[canvas])" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "b15124d9", 139 | "metadata": {}, 140 | "source": [ 141 | "# Using in solara\n", 142 | "\n", 143 | "*Note: this part does not work in JupyterLite*\n", 144 | "\n", 145 | "Although this shows the power of ipyreact, in how it composes, we can do better.\n", 146 | "\n", 147 | "The first problem is that it does not have a very nice API, it's very low level.\n", 148 | "\n", 149 | "The second problem is that although we now have a nice composable set of widgets, actually building a larger application by manually adding and removing widgets is tiresome, which is why we build [Solara](https://solara.dev).\n", 150 | "\n", 151 | "By following [the solara docs on how to use widgets](https://solara.dev/docs/howto/ipywidget-libraries) we can add wrapper component with a nicer API." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "e53d754b", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "import solara\n", 162 | "\n", 163 | "@solara.component\n", 164 | "def Box(position, color, props={}, events={}, children=[]):\n", 165 | " return BoxWidget.element(props={**props, **dict(color=color, position=position)}, events=events, children=children)\n", 166 | "\n", 167 | "\n", 168 | "@solara.component\n", 169 | "def Canvas(props={}, events={}, children=[]):\n", 170 | " return ipyreact.Widget.element(props=props, events=events, children=children, _type=\"Canvas\", _module=\"threejs-fiber\")\n", 171 | "\n", 172 | "\n", 173 | "@solara.component\n", 174 | "def OrbitControls(props={}, events={}, children=[]):\n", 175 | " return ipyreact.Widget.element(_type=\"OrbitControls\", _module=\"threejs-fiber\", props=props, events=events, children=children)\n", 176 | "\n", 177 | "\n", 178 | "@solara.component\n", 179 | "def DirectionalLight(props={}, events={}, children=[]):\n", 180 | " # starts with a lower case, should be available globally, so we don't need to pass\n", 181 | " # _module=\"threejs-fiber\"\n", 182 | " return ipyreact.Widget.element(_type=\"directionalLight\", props=props, events=events, children=children)\n", 183 | "\n", 184 | "\n", 185 | "@solara.component\n", 186 | "def Div(style={}, props={}, events={}, children=[]):\n", 187 | " # we use a ipyreact based div to avoid an extra wrapper div which will affect layout\n", 188 | " return ipyreact.Widget.element(_type=\"div\", props={**props, **dict(style=style)}, children=children, events=events)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "id": "c0550eed", 194 | "metadata": {}, 195 | "source": [ 196 | "Now we can build a dynamic application without having to worry about how to add and remove widgets, and populating our scene dynamically based on data (the reactive variable). On top of that we also have a nicer API that we can customize to our needs." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "id": "df54baab", 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "boxes = solara.reactive([\n", 207 | " ([-1, 0, 3], \"#18a36e\"),\n", 208 | " ([1, 0, 3], \"#f56f42\"),\n", 209 | "])\n", 210 | " \n", 211 | "def add(event_data=None):\n", 212 | " x = random.random() * 4 - 2\n", 213 | " z = random.random() * 4 - 1\n", 214 | " color = random_color() # Call the random_color function to get a random color\n", 215 | " boxes.value = [*boxes.value, ([x, 0, z], color)]\n", 216 | "\n", 217 | "\n", 218 | "def clear():\n", 219 | " boxes.value = boxes.value[:2]\n", 220 | "\n", 221 | "\n", 222 | "def add_10():\n", 223 | " for i in range(10):\n", 224 | " add()\n", 225 | " \n", 226 | "@solara.component\n", 227 | "def Page():\n", 228 | " with solara.Row():\n", 229 | " solara.Button(\"Clear\", on_click=clear)\n", 230 | " solara.Button(\"Add 10\", on_click=add_10)\n", 231 | " solara.Markdown(\"Click to add a new box\")\n", 232 | " with Div(style={\"height\": \"600px\"}):\n", 233 | " # a canvas fill the available space, so we add a parent div with height\n", 234 | " with Canvas(events={\"onClick\": add}):\n", 235 | " for position, color in boxes.value:\n", 236 | " Box(position=position, color=color)\n", 237 | " OrbitControls()\n", 238 | " DirectionalLight(props=dict(color=\"#ffffff\", intensity=1, position=[-1, 2, 4]))\n", 239 | "Page()" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "id": "e4823672", 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [] 249 | } 250 | ], 251 | "metadata": { 252 | "kernelspec": { 253 | "display_name": "Python 3 (ipykernel)", 254 | "language": "python", 255 | "name": "python3" 256 | }, 257 | "language_info": { 258 | "codemirror_mode": { 259 | "name": "ipython", 260 | "version": 3 261 | }, 262 | "file_extension": ".py", 263 | "mimetype": "text/x-python", 264 | "name": "python", 265 | "nbconvert_exporter": "python", 266 | "pygments_lexer": "ipython3", 267 | "version": "3.9.16" 268 | } 269 | }, 270 | "nbformat": 4, 271 | "nbformat_minor": 5 272 | } 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipyreact 2 | 3 | React for ipywidgets that just works. No webpack, no npm, no hassle. Just write jsx, tsx and python. 4 | 5 | Build on top of [AnyWidget](https://anywidget.dev/). 6 | 7 | ## Why 8 | 9 | Ipyreact adds composability, allowing you to add children to your widget, which will render the whole react tree in 10 | a single react context, without adding extra divs or creating a new react context. 11 | 12 | This allows wrapping libraries such as [Material UI](https://mui.com/), [Ant Design](https://ant.design/) and even 13 | [React-three-fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction). 14 | 15 | ## Tutorial 16 | 17 | This tutorial will walk you through the steps of building a complete ipywidget with react. 18 | 19 | [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=full_tutorial.ipynb) 20 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Ffull_tutorial.ipynb) 21 | 22 | Just click the JupyterLite or Binder link to start the interactive walkthrough. 23 | 24 | ## Goals 25 | 26 | - Take any [Material UI example](https://mui.com/material-ui/react-rating/), copy/paste the code, and it should work in Jupyter Notebook, Jupyter Lab, Voila, and more specifically, [Solara](https://github.com/widgetti/solara). 27 | - Wrap a library such as [Ant Design](https://ant.design/) giving the options to customize any JSON<->JavaScript Object (de)serialization, such as the [DatePicker](https://ant.design/components/date-picker) which uses a dayjs object internally, which cannot be serialized over the wire to Python. 28 | - Compose widgets together to form a single react tree, with the same react context (e.g. useContext). 29 | 30 | ## Examples 31 | 32 | ### Inline code 33 | 34 | ```python 35 | import ipyreact 36 | 37 | 38 | class ConfettiWidget(ipyreact.ValueWidget): 39 | _esm = """ 40 | import confetti from "canvas-confetti"; 41 | import * as React from "react"; 42 | 43 | export default function({value, setValue}) { 44 | return 47 | };""" 48 | ConfettiWidget() 49 | ``` 50 | 51 | ![initial-30-fps-compressed](https://user-images.githubusercontent.com/1765949/233469170-c659b670-07f5-4666-a201-80dea01ebabe.gif) 52 | 53 | (_NOTE: in the recording we used on_value, we now use setValue_) 54 | 55 | ### Hot reloading 56 | 57 | Create a tsx file: 58 | 59 | ```tsx 60 | // confetti.tsx 61 | import confetti from "canvas-confetti"; 62 | import * as React from "react"; 63 | 64 | export default function ({ value, setValue }) { 65 | return ( 66 | 69 | ); 70 | } 71 | ``` 72 | 73 | And use it in your python code: 74 | 75 | ```python 76 | import ipyreact 77 | import pathlib 78 | 79 | 80 | class ConfettiWidget(ipyreact.ValueWidget): 81 | _esm = pathlib.Path("confetti.tsx") 82 | 83 | ConfettiWidget() 84 | ``` 85 | 86 | Now edit, save, and see the changes in your browser/notebook. 87 | 88 | ![hot-reload-compressed](https://user-images.githubusercontent.com/1765949/233470113-b2aa9284-71b9-44f0-bd52-906a08b06e14.gif) 89 | 90 | (_NOTE: in the recording we used on_value, we now use setValue_) 91 | 92 | ### IPython magic 93 | 94 | First load the ipyreact extension: 95 | 96 | ```python 97 | %load_ext ipyreact 98 | ``` 99 | 100 | Then use the `%%react` magic to directly write jsx/tsx in your notebook: 101 | 102 | ```tsx 103 | %%react 104 | import confetti from "canvas-confetti"; 105 | import * as React from "react"; 106 | 107 | export default function({value, setValue}) { 108 | return 111 | }; 112 | ``` 113 | 114 | Access the underlying widget with the name `_last_react_widget` (e.g. `_last_react_widget.value` contains the number of clicks): 115 | 116 | ![magic-optimized](https://user-images.githubusercontent.com/1765949/233471041-62e807d6-c16d-4fc5-af5d-13c0acb2c677.gif) 117 | 118 | (_NOTE: in the recording we used on_value, we now use setValue_) 119 | 120 | ## Installation 121 | 122 | You can install using `pip`: 123 | 124 | ```bash 125 | pip install ipyreact 126 | ``` 127 | 128 | ## Usage 129 | 130 | ### Summary 131 | 132 | - The `ValueWidget` has an `value` trait, which is a `traitlets.Any` trait. Use this to pass data to your react component, or to get data back from your react component (since it inherits from ipywidgets.ValueWidget it 133 | can be used in combination with ipywidgets' [interact](https://ipywidgets.readthedocs.io/en/latest/examples/Using%20Interact.html)). 134 | - The `ipyreact.Widget` does not have the `value` trait. 135 | - All traits are added as props to your react component (e.g. `{value, setValue...}` pairs in the example above. 136 | - For every trait `ipyreact` automatically provides a `set` callback, which you can use to set the trait value from your react component (e.g. `setValue` in the example above). (_Note: we used `on_value` before, this is now deprecated_) 137 | - Props can de passed as `Widget(props={"title": "My title"})`, and contrary to a trait, will not add a `setTitle` callable to the props. 138 | - Children can be passed using `Widget(children=['text', or_widget])` supporting text, widgets, and un-interrupted rendering of ipyreact widgets. 139 | - Your code gets transpiled using [sucrase](https://github.com/alangpierce/sucrase) in the frontend, no bundler needed. 140 | - Your code should be written in ES modules. 141 | - Set `_debug=True` to get more debug information in the browser console. 142 | - Make sure you export a default function from your module (e.g. `export default function MyComponent() { ... }`). This is the component that will be rendered. 143 | - Pass `events={"onClick": handler}` to the constructor or add a method with the name `event_onClick(self, data=None)` to add a `onClick` callback to your props. 144 | 145 | ### HTML elements 146 | 147 | You do not need to provide the module code to create built-in HTML elements, ipyreact supports the same API as [React's createElement](https://react.dev/reference/react/createElement) 148 | allowing creation of buttons for instance. 149 | 150 | ```python 151 | import ipyreact 152 | ipyreact.Widget(_type="button", children=["click me"]) 153 | ``` 154 | 155 | Note that in addition to all native browser elements, also web components are supported. 156 | 157 | ### Children 158 | 159 | As shown in the above example, we also support children, which supports a list of strings (text), `ipyreact.Widget` widgets that will be rendered as an uninterrupted react tree, or 160 | any other `ipywidgets` 161 | 162 | ```python 163 | import ipyreact 164 | import ipywidgets as widgets 165 | ipyreact.Widget(_type="div", children=[ 166 | "normal text", 167 | ipyreact.Widget(_type="button", children=["nested react widgets"]), 168 | widgets.FloatSlider(description="regular ipywidgets") 169 | ]) 170 | ``` 171 | 172 | [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=children.ipynb) 173 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fchildren.ipynb) 174 | 175 | ### Events 176 | 177 | Events can be passed via the event argument. In this case `onClick` will be added as a prop to the button element. 178 | 179 | ```python 180 | import ipyreact 181 | ipyreact.Widget(_type="button", children=["click me"], events={"onClick": print}) 182 | ``` 183 | 184 | Subclasses can also add an `event_onClick` method, which will also add a `onClick` event handler to the props. 185 | 186 | [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=events.ipynb) 187 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fevents.ipynb) 188 | 189 | ### Importing external modules 190 | 191 | Writing JSX code without having to compile/bundle is great, but so is using external libraries. 192 | 193 | Ipyreact uses ES modules, which allows native importing of external libraries when written as an ES module. 194 | In the example below, we use https://esm.sh/ which exposes many JS libraries as ES modules that 195 | we can directly import. 196 | 197 | ```python 198 | import ipyreact 199 | 200 | ipyreact.ValueWidget( 201 | _esm=""" 202 | import confetti from "https://esm.sh/canvas-confetti@1.6.0"; 203 | import * as React from "react"; 204 | 205 | export default function({value, setValue}) { 206 | return 209 | }; 210 | """ 211 | ) 212 | ``` 213 | 214 | ### Import maps 215 | 216 | However, the above code now has a direct link to "https://esm.sh/canvas-confetti@1.6.0" which makes the code very specific to esm.sh. 217 | 218 | To address this, we also support [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to 219 | write code more independant of where the modules come from. 220 | You can provide an import map using `ipyreact.define_import_map`, which takes a dictionary of module names to urls or other modules. By default we support `react` and `react-dom` which is prebundled. 221 | 222 | Apart from `react`, the default we provide is: 223 | 224 | ```python 225 | define_import_map({ 226 | "@mui/material": "https://esm.sh/@mui/material@5.11.10?external=react,react-dom", 227 | "@mui/material/": "https://esm.sh/@mui/material@5.11.10&external=react,react-dom/", 228 | "@mui/icons-material/": "https://esm.sh/@mui/icons-material/?external=react,react-dom", 229 | "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", 230 | }) 231 | ``` 232 | 233 | _Note that it is important to add `external=react,react-dom` for ReactJS based libraries, otherwise [esm.sh](https://esm.sh/#using-import-maps) would import ReactJS again_. 234 | 235 | Which means we can now write our ConfettiButton as: 236 | 237 | ```python 238 | import ipyreact 239 | 240 | # note that this import_map is already part of the default 241 | ipyreact.define_import_map({ 242 | "canvas-confetti": "https://esm.sh/canvas-confetti@1.6.0", 243 | }) 244 | 245 | 246 | ipyreact.ValueWidget( 247 | _esm=""" 248 | import confetti from "canvas-confetti"; 249 | import * as React from "react"; 250 | 251 | export default function({value, setValue}) { 252 | return 255 | }; 256 | """ 257 | ) 258 | ``` 259 | 260 | And it also means we can copy paste _most_ of the examples from [mui](https://mui.com/) 261 | 262 | ```tsx 263 | %%react -n my_widget -d 264 | import {Button} from "@mui/material"; 265 | import confetti from "canvas-confetti"; 266 | import * as React from "react"; 267 | 268 | export default function({ value, setValue}) { 269 | console.log("value=", value); 270 | return ( 271 | 277 | ); 278 | } 279 | ``` 280 | 281 | We use the https://github.com/guybedford/es-module-shims shim to the browser page for the import maps functionality. 282 | This also means that although import maps can be configured per widget, they configuration of import maps is global. 283 | 284 | ### Bundled ESM modules 285 | 286 | ## Creating the ES module 287 | 288 | While esm.sh is convenient to use, for production use, we recommend creating a standalone bundle. This will load faster and will not require a direct connection to esm.sh, which might not be available in airgapped or firewalled environments. 289 | 290 | We will not create a minimal bundle for https://ant.design/ 291 | 292 | First create a simple file called `antd-minimal.js` that exports what we need. 293 | 294 | ```javascript 295 | export { Button, Flex, Slider } from "antd"; 296 | ``` 297 | 298 | Next, we install the libraries: 299 | 300 | ```bash 301 | $ npm install antd 302 | ``` 303 | 304 | And use ESBuild to turn this into a self-contained module/bundle, without react, since ipyreact provides that for us. 305 | 306 | ``` 307 | $ npx esbuild ./antd-minimal.js --bundle --outfile=./antd-minimal.esm.js --format=esm --external:react --external:react-dom --target=esnext 308 | ``` 309 | 310 | Now we can define the module with a custom name (we call it antd-minimal). 311 | 312 | ```python 313 | import ipyreact 314 | from pathlib import Path 315 | 316 | ipyreact.define_module("antd-minimal", Path("./antd-minimal.esm.js")) 317 | ``` 318 | 319 | We can now use the components from this module: 320 | 321 | ```python 322 | def on_click(event_data): 323 | w.children = ["Clicked"] 324 | 325 | w = ipyreact.Widget(_module="antd-minimal", _type="Button", children=["Hi there"], events={"onClick": on_click}) 326 | w 327 | ``` 328 | 329 | Or, composing multiple ones: 330 | 331 | ```python 332 | stack = ipyreact.Widget(_module="antd-minimal", _type="Flex", 333 | props={"vertical": True, "style": {"padding": "24px"}}, 334 | children=[ 335 | ipyreact.Widget(_module="antd-minimal", _type="Button", children=["Ant Design Button"]), 336 | ipyreact.Widget(_module="antd-minimal", _type="Slider", 337 | props={"defaultValue": 3, "min": 0, "max": 11}), 338 | ]) 339 | stack 340 | ``` 341 | 342 | Input components might need a little bit of custom code, and subclassing `ValueWidget`. It often means binding the value to the right prop of the input component (in this case the Slider takes the same name, `value`) and coupling the event handler (in this case `onChange`) to the `setValue` function. 343 | 344 | ```python 345 | import traitlets 346 | 347 | 348 | class Slider(ipyreact.ValueWidget): 349 | _esm = """ 350 | import * as React from "react"; 351 | import {Slider} from "antd-minimal" 352 | 353 | export default ({value, setValue, ...rest}) => { 354 | return setValue(v)} {...rest}/> 355 | } 356 | 357 | """ 358 | s = Slider(value=2) 359 | s 360 | ``` 361 | 362 | _Note that it depends on the implementation of the event handler if the value is being passed directly, or a (synthetic) event with the data will be passed as argument. An typical example event handler could be `onChange={(event) => setValue(event.target.value)}`._ 363 | 364 | Now the slider widget is stateful, and we have bi-directional communication using the `.value` trait. 365 | For instance, we can read it: 366 | 367 | ```python 368 | s.value 369 | ``` 370 | 371 | Or write to it, and it will be reflected directly in the UI. 372 | 373 | ```python 374 | s.value = 10 375 | ``` 376 | 377 | Test this out in the notebook: 378 | [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=antd/antd.ipynb) 379 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fantd%2Fantd.ipynb) 380 | 381 | ### Bundled ES modules for threejs 382 | 383 | See this notebook for a 3D WebGL threejs-fiber example 384 | 385 | [![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=threejs-fiber/threejs-fiber.ipynb) 386 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fthreejs-fiber%2Fthreejs-fiber.ipynb) 387 | 388 | ## Development Installation 389 | 390 | Create a dev environment: 391 | 392 | ```bash 393 | conda create -n ipyreact-dev -c conda-forge nodejs yarn python 'jupyterlab<4' 394 | conda activate ipyreact-dev 395 | ``` 396 | 397 | Install the python. This will also build the TS package. 398 | 399 | ```bash 400 | pip install -e ".[test, examples, dev]" 401 | pre-commit install 402 | ``` 403 | 404 | When developing your extensions, you need to manually enable your extensions with the 405 | notebook / lab frontend. For lab, this is done by the command: 406 | 407 | ``` 408 | jupyter labextension develop --overwrite . 409 | yarn run build 410 | ``` 411 | 412 | For classic notebook, you need to run: 413 | 414 | ``` 415 | jupyter nbextension install --sys-prefix --symlink --overwrite --py ipyreact 416 | jupyter nbextension enable --sys-prefix --py ipyreact 417 | ``` 418 | 419 | Note that the `--symlink` flag doesn't work on Windows, so you will here have to run 420 | the `install` command every time that you rebuild your extension. For certain installations 421 | you might also need another flag instead of `--sys-prefix`, but we won't cover the meaning 422 | of those flags here. 423 | 424 | ## Binary data transport 425 | 426 | Binary data such as NumPy arrays, or Arrow data can be efficiently transported to the frontend. 427 | Props support object that support the buffer interface. See [this test as an example](https://github.com/widgetti/ipyreact/tree/master/tests/ui/serialize_test.py). 428 | 429 | ### How to see your changes 430 | 431 | #### Typescript: 432 | 433 | If you use JupyterLab to develop then you can watch the source directory and run JupyterLab at the same time in different 434 | terminals to watch for changes in the extension's source and automatically rebuild the widget. 435 | 436 | ```bash 437 | # Watch the source directory in one terminal, automatically rebuilding when needed 438 | yarn run watch 439 | # Run JupyterLab in another terminal 440 | jupyter lab 441 | ``` 442 | 443 | After a change wait for the build to finish and then refresh your browser and the changes should take effect. 444 | 445 | #### Python: 446 | 447 | If you make a change to the python code then you will need to restart the notebook kernel to have it take effect. 448 | 449 | # FAQ 450 | 451 | ## Which version of React do you use. 452 | 453 | We currently only support React 18. Although we have some scaffolding in place to support different version, we do not have funding to support both. 454 | 455 | ## Why does ipyreact provides React? 456 | 457 | If several ReactJS components need to be composed into a single React app, they need to share the same React context. This makes it possible 458 | for features such as React's [useContext](https://react.dev/reference/react/useContext) to work across the whole React tree. 459 | If every library brings its own React, they cannot communicate using this. Also, every child would need to be nested in its own `
` 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 \n", 273 | " }};\"\"\"\n", 274 | "MyExampleWidget()" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "id": "12d2a8a2", 280 | "metadata": {}, 281 | "source": [ 282 | "## Parametrizing using props\n", 283 | "\n", 284 | "If you only want to set a value from the Python side, you can use the props to pass data to the component in the frontend.\n" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "id": "a2af801c", 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "import ipyreact\n", 295 | "from traitlets import Unicode\n", 296 | "\n", 297 | "class MyExampleWidget(ipyreact.Widget):\n", 298 | " _esm = \"\"\"\n", 299 | " import * as React from \"react\";\n", 300 | "\n", 301 | " export default function MyButton({ message }) {\n", 302 | " return ;\n", 303 | " };\"\"\"\n", 304 | "MyExampleWidget(props={\"message\": \"hi\"})" 305 | ] 306 | }, 307 | { 308 | "cell_type": "markdown", 309 | "id": "67118dd2", 310 | "metadata": {}, 311 | "source": [ 312 | "### Forwarding unused props and children\n", 313 | "However, with this, we lose the ability to set all the other props like in the example above. We can use the following pattern using [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#object_destructuring) by passing all unused props (called `rest` in this example) to the button element. Also, do not forget to pass the children!." 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": null, 319 | "id": "a26f1e3d", 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "import ipyreact\n", 324 | "from traitlets import Unicode\n", 325 | "\n", 326 | "# ⭐⭐⭐ This is good practice again ⭐⭐⭐\n", 327 | "class MyExampleWidget(ipyreact.Widget):\n", 328 | " _esm = \"\"\"\n", 329 | " import * as React from \"react\";\n", 330 | "\n", 331 | " export default function MyButton({ message, children, ...rest }) {\n", 332 | " return ;\n", 333 | " };\"\"\"\n", 334 | "MyExampleWidget(props={\"message\": \"hi\", \"title\": \"Behaves like a tooltip\"}, children=[' extra', ' children'])" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "id": "944363e3-92b1-4ee6-8e16-1a2f6879fefb", 340 | "metadata": {}, 341 | "source": [ 342 | "## Adding state\n", 343 | "\n", 344 | "Props cannot be changed by the component, they are considered pure input.\n", 345 | "\n", 346 | "When you need the component to control state, you can add a trait ([see traitlets](https://traitlets.readthedocs.io/en/stable/using_traitlets.html)) to your widget class will (with `.tag(sync=True)` to make it sync to the frontend). For every trait added, you will receive a value and a setter to our props in the frontend.\n", 347 | "\n", 348 | "For instance, if you add a trait called `message`, you will have a `message` and `setMessage` in your props." 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "execution_count": null, 354 | "id": "0e27dc18-82f2-4598-9117-9e0cfec49f99", 355 | "metadata": { 356 | "tags": [] 357 | }, 358 | "outputs": [], 359 | "source": [ 360 | "import ipyreact\n", 361 | "from traitlets import Unicode\n", 362 | "\n", 363 | "class MyExampleWidget(ipyreact.Widget):\n", 364 | " message = Unicode(\"Click me\").tag(sync=True)\n", 365 | " _esm = \"\"\"\n", 366 | " import * as React from \"react\";\n", 367 | "\n", 368 | " export default function MyButton({ message, setMessage }) {\n", 369 | " return ;\n", 370 | " };\"\"\"\n", 371 | "w = MyExampleWidget()\n", 372 | "w" 373 | ] 374 | }, 375 | { 376 | "cell_type": "markdown", 377 | "id": "5dad7f59", 378 | "metadata": {}, 379 | "source": [ 380 | "Every time the component calls `setMessage` the component will rerender itself with the new value for `message`, but will also synchronize the value to the Python side. \n", 381 | "\n", 382 | "If we change the value from the Python side, the value gets send to the frontend, and the component will render with the new `message` value." 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": null, 388 | "id": "5bc13be9", 389 | "metadata": {}, 390 | "outputs": [], 391 | "source": [ 392 | "w.message = \"Set from Python 🐍\"" 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": null, 398 | "id": "e2fb6dcb-4477-4ff5-a7d0-5e0f209d83cf", 399 | "metadata": { 400 | "tags": [] 401 | }, 402 | "outputs": [], 403 | "source": [ 404 | "# we can use this traitlets also as parameters\n", 405 | "MyExampleWidget(message=\"Different initial value\")" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "id": "5a4436e2", 411 | "metadata": {}, 412 | "source": [ 413 | "It's great that python will throw an error when the wrong type is given!" 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "execution_count": null, 419 | "id": "4dba603f-8052-4d8d-845d-d28029e17c35", 420 | "metadata": { 421 | "tags": [] 422 | }, 423 | "outputs": [], 424 | "source": [ 425 | "# Note that traits can be type checked, this will result in an error because message is not a string (it is an int)\n", 426 | "# w.message = 1" 427 | ] 428 | }, 429 | { 430 | "cell_type": "markdown", 431 | "id": "30979edc", 432 | "metadata": {}, 433 | "source": [ 434 | "### ValueWidget\n", 435 | "\n", 436 | "Since it is very common that a component controls a single value (e.g. any input component, such as text input, a slider etc) we made a special subclass of `ipyreact.Widget` called `ipyreact.ValueWidget` that already contains a `value` trait.\n", 437 | "\n", 438 | "In many cases you do not even need to create a subclass, but can directly use the class to create an instance." 439 | ] 440 | }, 441 | { 442 | "cell_type": "code", 443 | "execution_count": null, 444 | "id": "2fbfdcf2", 445 | "metadata": {}, 446 | "outputs": [], 447 | "source": [ 448 | "import ipyreact\n", 449 | "from traitlets import Unicode\n", 450 | "\n", 451 | "\n", 452 | "# Although we can subclass, we don't need to in this case\n", 453 | "# class MyExampleValueWidget(ipyreact.ValueWidget):\n", 454 | "# # we get a value trait for free\n", 455 | "# _esm = \"\"\"\n", 456 | "# import * as React from \"react\";\n", 457 | "\n", 458 | "# export default function MyButton({ value, setValue }) {\n", 459 | "# return ;\n", 460 | "# };\"\"\"\n", 461 | "# MyExampleValueWidget(value=\"Similar, but using the value/ValueWidget\")\n", 462 | "\n", 463 | "\n", 464 | "# We can simply create an instance of ValueWidget\n", 465 | "\n", 466 | "ipyreact.ValueWidget(value=\"Similar, but using the value/ValueWidget\",\n", 467 | " _esm=\"\"\"\n", 468 | " import * as React from \"react\";\n", 469 | "\n", 470 | " export default function MyButton({ value, setValue }) {\n", 471 | " return ;\n", 472 | " };\"\"\"\n", 473 | ")" 474 | ] 475 | }, 476 | { 477 | "cell_type": "markdown", 478 | "id": "adb7a394", 479 | "metadata": {}, 480 | "source": [ 481 | "The upside of using the `ValueWidget` is that it is a subclass of `ipywidgets.ValueWidget` and therefore can be used in [interact](https://ipywidgets.readthedocs.io/en/latest/examples/Using%20Interact.html).\n", 482 | "\n", 483 | "Having a standard name (`value`) for the trait can be useful, but might loss semantics in your specific case.\n", 484 | "\n", 485 | "Note that you may be tempted to add in many traits, since it makes it easier to modify the state of the component. But be aware that for ever trait added, your props get both a `foo` and `setFoo`. Make sure you do not accidently pass the `setFoo` to your child elements, as they might not support it (e.g. button has no setFoo attribute).\n" 486 | ] 487 | }, 488 | { 489 | "cell_type": "markdown", 490 | "id": "cee4af9a", 491 | "metadata": {}, 492 | "source": [ 493 | "## Adding events\n", 494 | "\n", 495 | "Apart from the traits, the props trait, and the children, we also support events.\n", 496 | "\n", 497 | "The events dict is a mapping from event name to an event handler. Event names can be native browser events, such as onClick on native elements (e.g. button), but they can also be custom events.\n", 498 | "\n", 499 | "### Native events\n", 500 | "\n", 501 | "Native browser events are always of the form `on`, for instance, the [click](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) event will map to the `onClick` event name." 502 | ] 503 | }, 504 | { 505 | "cell_type": "code", 506 | "execution_count": null, 507 | "id": "269558f5", 508 | "metadata": {}, 509 | "outputs": [], 510 | "source": [ 511 | "def on_click(event_data):\n", 512 | " w.children = [\"Clicked ⭐\"]\n", 513 | " \n", 514 | "w = ipyreact.Widget(_type=\"button\",\n", 515 | " children=[\"Click me\"],\n", 516 | " props={\n", 517 | " \"title\": \"Behaves like a tooltip\",\n", 518 | " \"style\": {\"border\": \"5px solid orange\"},\n", 519 | " \"class\": \"mybutton\"\n", 520 | " },\n", 521 | " events={\"onClick\": on_click})\n", 522 | "w" 523 | ] 524 | }, 525 | { 526 | "cell_type": "markdown", 527 | "id": "232e5e9c", 528 | "metadata": {}, 529 | "source": [ 530 | "### Custom events\n", 531 | "\n", 532 | "If you are creating your own component, you are free to name events anything you'd like. Note that event handlers can optionally take arguments." 533 | ] 534 | }, 535 | { 536 | "cell_type": "code", 537 | "execution_count": null, 538 | "id": "43016676", 539 | "metadata": {}, 540 | "outputs": [], 541 | "source": [ 542 | "def on_my_click(new_label):\n", 543 | " w.children = [new_label]\n", 544 | " \n", 545 | " \n", 546 | "w = ipyreact.Widget(children=[\"Click me\"],\n", 547 | " events={\"onMyClick\": on_my_click},\n", 548 | " _esm=\"\"\"\n", 549 | " import * as React from \"react\";\n", 550 | "\n", 551 | " export default function MyButton({ onMyClick, children }) {\n", 552 | " return ;\n", 553 | " };\n", 554 | " \"\"\"\n", 555 | ")\n", 556 | "w" 557 | ] 558 | }, 559 | { 560 | "cell_type": "markdown", 561 | "id": "f2736a01", 562 | "metadata": {}, 563 | "source": [ 564 | "It is also possible to add methods with a subclass, prefixed with `event_` that will automatically be available in the props as well." 565 | ] 566 | }, 567 | { 568 | "cell_type": "code", 569 | "execution_count": null, 570 | "id": "ae44863a", 571 | "metadata": {}, 572 | "outputs": [], 573 | "source": [ 574 | "class MyButton(ipyreact.Widget):\n", 575 | " _esm = \"\"\"\n", 576 | " import * as React from \"react\";\n", 577 | "\n", 578 | " export default function MyButton({ onMyClick, children }) {\n", 579 | " return ;\n", 580 | " };\n", 581 | " \"\"\"\n", 582 | "\n", 583 | " # the method name should match the name in the props\n", 584 | " def event_onMyClick(self, new_label):\n", 585 | " w.children = [new_label]\n", 586 | "\n", 587 | "w = MyButton(children=[\"Click me\"])\n", 588 | "w" 589 | ] 590 | }, 591 | { 592 | "cell_type": "markdown", 593 | "id": "f6b30c43", 594 | "metadata": {}, 595 | "source": [ 596 | "## Traitlet events / observe\n", 597 | "\n", 598 | "Since traits can be [observed for changes](https://traitlets.readthedocs.io/en/stable/using_traitlets.html#observe) we can also add an event handler to state changes (instead of the event handler solution is the previous example).\n", 599 | "\n", 600 | "Both solutions can be valid. Sometimes events go together with a state change, and observing a state change then makes sense. In cases where a pure event is emitted, that does not directly lead to a state change, this solution might not be the right one.\n", 601 | "\n", 602 | "The example below does combine an event with a state change, and we therefore use the `@observe` decorator to handle further state changes." 603 | ] 604 | }, 605 | { 606 | "cell_type": "code", 607 | "execution_count": null, 608 | "id": "24f42e00", 609 | "metadata": {}, 610 | "outputs": [], 611 | "source": [ 612 | "from traitlets import Any, observe\n", 613 | "from traitlets import Int, Any\n", 614 | "import ipyreact\n", 615 | "\n", 616 | "def is_prime_number(n):\n", 617 | " for i in range(2, n):\n", 618 | " if n % i == 0:\n", 619 | " return False\n", 620 | " return True\n", 621 | "\n", 622 | "\n", 623 | "class PrimePythonWidget(ipyreact.Widget):\n", 624 | " message = Any(\"Click to test the next number\").tag(sync=True)\n", 625 | " number = Int(0).tag(sync=True)\n", 626 | "\n", 627 | " @observe(\"number\")\n", 628 | " def _observe_count(self, change):\n", 629 | " if is_prime_number(self.number):\n", 630 | " self.message = \"Yes ✅ it is a prime number\"\n", 631 | " else:\n", 632 | " self.message = \"No ❌, not a primer number\"\n", 633 | " # alternatively: \n", 634 | " # self.props = {**self.props, message: ....}\n", 635 | "\n", 636 | " _esm = \"\"\"\n", 637 | " import * as React from \"react\";\n", 638 | "\n", 639 | " // NOTE: we add setMessage, even though we do not use it, to avoid forwarding\n", 640 | " // it to button\n", 641 | " export default function({setNumber, number, message, setMessage, ...rest}) {\n", 642 | " return
\n", 643 | " \n", 646 | "
\n", 647 | " {message}\n", 648 | "
\n", 649 | " };\"\"\"\n", 650 | "\n", 651 | "\n", 652 | "primepy = PrimePythonWidget(props={\"class\": \"mybutton\"})\n", 653 | "primepy" 654 | ] 655 | }, 656 | { 657 | "cell_type": "markdown", 658 | "id": "448a0e28", 659 | "metadata": {}, 660 | "source": [ 661 | "Note that in this case, we have chosen to add `message` as a trait, instead of sending the `message` via the `props` trait. Since we also combine this with forwarding the rest of the props to the button, we *have* to take out the `setMessage` callback. If we do not, React will complain that the button element does not support the `setMessage` attribute." 662 | ] 663 | }, 664 | { 665 | "cell_type": "markdown", 666 | "id": "7476173b", 667 | "metadata": {}, 668 | "source": [ 669 | "## Components in files\n", 670 | "Having the JavaScript components in python string variables is good for the beginning. \n", 671 | "That way the project is compact and there is no need of file switching. \n", 672 | "\n", 673 | "As examples are getting longer, the JavaScript components can be written in separate files. \n", 674 | "That way, you will also get JavaScript syntax hilighting. " 675 | ] 676 | }, 677 | { 678 | "cell_type": "code", 679 | "execution_count": null, 680 | "id": "2e1ce310", 681 | "metadata": {}, 682 | "outputs": [], 683 | "source": [ 684 | "import ipyreact\n", 685 | "import pathlib\n", 686 | "\n", 687 | "class WidgetFromFile(ipyreact.Widget):\n", 688 | " _esm = pathlib.Path(\"my_component.tsx\").read_text()\n", 689 | "\n", 690 | "WidgetFromFile()" 691 | ] 692 | }, 693 | { 694 | "cell_type": "markdown", 695 | "id": "23f1401c", 696 | "metadata": {}, 697 | "source": [ 698 | "If you don't want to re-run the python code after making changes to the file that contains the component, you can see changes happening immediately thanks to **hot-reloading**.\n", 699 | "This requires `pip install watchdog`. \n", 700 | "Next, you replace the line \n", 701 | "`_esm = pathlib.Path(\"my_component.tsx\").read_text()` \n", 702 | "with \n", 703 | "`_esm = pathlib.Path(\"my_component.tsx\")` \n", 704 | "\n", 705 | "Now open `my_component.tsx`, change \"Hello World\" to \"Hi there\", and you will see that the changes are reflected immediately." 706 | ] 707 | }, 708 | { 709 | "cell_type": "code", 710 | "execution_count": null, 711 | "id": "d7bd0c3c", 712 | "metadata": {}, 713 | "outputs": [], 714 | "source": [ 715 | "import ipyreact\n", 716 | "import pathlib\n", 717 | "\n", 718 | "class WidgetFromFile(ipyreact.Widget):\n", 719 | " _esm = pathlib.Path(\"my_component.tsx\") # <- this will not work in JupyterLite\n", 720 | "\n", 721 | "WidgetFromFile()" 722 | ] 723 | }, 724 | { 725 | "cell_type": "markdown", 726 | "id": "b34a44f8", 727 | "metadata": {}, 728 | "source": [ 729 | "### Importing external modules\n", 730 | "\n", 731 | "Writing JSX code without having to compile/bundle is great, but so is using external libraries.\n", 732 | "\n", 733 | "Ipyreact uses ES modules, which allows native importing of external libraries when written as an ES module.\n", 734 | "In the example below, we use https://esm.sh/ which exposes many JS libraries as ES modules that\n", 735 | "we can directly import." 736 | ] 737 | }, 738 | { 739 | "cell_type": "code", 740 | "execution_count": null, 741 | "id": "381b5a21", 742 | "metadata": {}, 743 | "outputs": [], 744 | "source": [ 745 | "import ipyreact\n", 746 | "\n", 747 | "ipyreact.ValueWidget(\n", 748 | " _esm=\"\"\"\n", 749 | " import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n", 750 | " import * as React from \"react\";\n", 751 | "\n", 752 | " export default function({value, setValue}) {\n", 753 | " return \n", 756 | " };\n", 757 | " \"\"\"\n", 758 | ")" 759 | ] 760 | }, 761 | { 762 | "cell_type": "markdown", 763 | "id": "366d143d", 764 | "metadata": {}, 765 | "source": [ 766 | "However, the above code now has a direct link to \"https://esm.sh/canvas-confetti@1.6.0\" which makes the code very specific to esm.sh.\n", 767 | "\n", 768 | "To address this, we also support [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to \n", 769 | "write code more independant of where the modules come from.\n", 770 | "You can provide an import map using `ipyreact.define_import_map`, which takes a dictionary of module names to urls or other modules. By default we support `react` and `react-dom` which is prebundled.\n", 771 | "\n", 772 | "Apart from `react`, the default we provide is:\n", 773 | "\n", 774 | "```python\n", 775 | "define_import_map({\n", 776 | " \"@mui/material\": \"https://esm.sh/@mui/material@5.11.10?external=react,react-dom\",\n", 777 | " \"@mui/material/\": \"https://esm.sh/@mui/material@5.11.10&external=react,react-dom/\",\n", 778 | " \"@mui/icons-material/\": \"https://esm.sh/@mui/icons-material/?external=react,react-dom\",\n", 779 | " \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n", 780 | "})\n", 781 | "\n", 782 | "```\n", 783 | "\n", 784 | "_Note that it is important to add `external=react,react-dom` for ReactJS based libraries, otherwise [esm.sh](https://esm.sh/#using-import-maps) would import ReactJS again_.\n", 785 | "\n", 786 | "Which means we can now write our ConfettiButton as:\n" 787 | ] 788 | }, 789 | { 790 | "cell_type": "code", 791 | "execution_count": null, 792 | "id": "9081c0cd", 793 | "metadata": {}, 794 | "outputs": [], 795 | "source": [ 796 | "import ipyreact\n", 797 | "\n", 798 | "# note that this import_map is already part of the default\n", 799 | "ipyreact.define_import_map({\n", 800 | " \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n", 801 | "})\n", 802 | "\n", 803 | "\n", 804 | "ipyreact.ValueWidget(\n", 805 | " _esm=\"\"\"\n", 806 | " import confetti from \"canvas-confetti\";\n", 807 | " import * as React from \"react\";\n", 808 | "\n", 809 | " export default function({value, setValue}) {\n", 810 | " return \n", 813 | " };\n", 814 | " \"\"\"\n", 815 | ")" 816 | ] 817 | }, 818 | { 819 | "cell_type": "markdown", 820 | "id": "24138653", 821 | "metadata": {}, 822 | "source": [ 823 | "## Advanced\n", 824 | "\n", 825 | " * [Bundled ES modules (ant design example)](./antd/antd.ipynb)\n", 826 | " * [Bundled ES modules for threejs (3D WebGL threejs-fiber example)](./threejs-fiber/threejs-fiber.ipynb)" 827 | ] 828 | }, 829 | { 830 | "cell_type": "markdown", 831 | "id": "82c9fbe3-6112-4dae-b817-e1a12343b407", 832 | "metadata": {}, 833 | "source": [ 834 | "## Optional\n", 835 | "### Autocomplete\n", 836 | "\n", 837 | "one more thing: \n", 838 | "Having **autocompletion in IDEs** is awesome! \n", 839 | "traitlets don't have that by default, but adding a `signature_has_traits` decorator will do the job!" 840 | ] 841 | }, 842 | { 843 | "cell_type": "code", 844 | "execution_count": null, 845 | "id": "89793f7c-1398-4f11-ab75-b73ffacd9b43", 846 | "metadata": { 847 | "tags": [] 848 | }, 849 | "outputs": [], 850 | "source": [ 851 | "import ipyreact\n", 852 | "from traitlets import Any, Unicode, Int, observe, signature_has_traits\n", 853 | "\n", 854 | "@signature_has_traits\n", 855 | "class MyExampleWidget(ipyreact.Widget):\n", 856 | " my_width = Int(23).tag(sync=True)\n", 857 | " _esm = \"\"\"\n", 858 | " import * as React from \"react\";\n", 859 | "\n", 860 | " export default function MyButton({ my_width }) {\n", 861 | " return (\n", 862 | " \n", 869 | " {\" \"}\n", 870 | " Width of {my_width} px{\" \"}\n", 871 | " \n", 872 | " );\n", 873 | " }\"\"\"\n", 874 | "MyExampleWidget(my_width=300)" 875 | ] 876 | }, 877 | { 878 | "cell_type": "markdown", 879 | "id": "38cfce36", 880 | "metadata": {}, 881 | "source": [ 882 | "And this screenshots shows that autocompletion works now: \n", 883 | "" 884 | ] 885 | }, 886 | { 887 | "cell_type": "markdown", 888 | "id": "bae77944-e0dd-49c4-bd27-b401d7bbef1b", 889 | "metadata": {}, 890 | "source": [ 891 | "Now we want to **print a message at class initialization** that says \"Button was initialized with width 300px!\" \n", 892 | "That is possible with the following code pattern using calling the `super` method." 893 | ] 894 | }, 895 | { 896 | "cell_type": "code", 897 | "execution_count": null, 898 | "id": "124771ca-8271-4b5e-9f5d-ee792442665b", 899 | "metadata": { 900 | "tags": [] 901 | }, 902 | "outputs": [], 903 | "source": [ 904 | "import ipyreact\n", 905 | "from traitlets import Int, signature_has_traits\n", 906 | "\n", 907 | "# 🪄🪄🪄 this is an advanced example, feel free to skip 🪄🪄🪄\n", 908 | "\n", 909 | "@signature_has_traits\n", 910 | "class MyExampleWidget(ipyreact.Widget):\n", 911 | " def __init__(self, **kwargs):\n", 912 | " super().__init__(**kwargs)\n", 913 | " self.print_welcome_message()\n", 914 | "\n", 915 | " def print_welcome_message(self):\n", 916 | " print(f\"Button was initilized with width of {self.my_width}px \")\n", 917 | "\n", 918 | " my_width = Int(23).tag(sync=True)\n", 919 | "\n", 920 | " _esm = \"\"\"\n", 921 | " import * as React from \"react\";\n", 922 | "\n", 923 | " export default function MyButton({ my_width }) {\n", 924 | " return (\n", 925 | " \n", 932 | " {\" \"}\n", 933 | " Width of {my_width} px{\" \"}\n", 934 | " \n", 935 | " );\n", 936 | " }\"\"\"\n", 937 | "\n", 938 | "MyExampleWidget(my_width=200)" 939 | ] 940 | } 941 | ], 942 | "metadata": { 943 | "kernelspec": { 944 | "display_name": "Python 3 (ipykernel)", 945 | "language": "python", 946 | "name": "python3" 947 | }, 948 | "language_info": { 949 | "codemirror_mode": { 950 | "name": "ipython", 951 | "version": 3 952 | }, 953 | "file_extension": ".py", 954 | "mimetype": "text/x-python", 955 | "name": "python", 956 | "nbconvert_exporter": "python", 957 | "pygments_lexer": "ipython3", 958 | "version": "3.9.16" 959 | } 960 | }, 961 | "nbformat": 4, 962 | "nbformat_minor": 5 963 | } 964 | --------------------------------------------------------------------------------