Last active
January 23, 2026 15:05
-
-
Save bollwyvl/ab6603e89423ff993e63bb4ec1a10582 to your computer and use it in GitHub Desktop.
Using Jupyter Widgets with Arbitrary JavaScript and HTML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "metadata": { | |
| "kernelspec": { | |
| "display_name": "Python (Pyodide)", | |
| "language": "python", | |
| "name": "python" | |
| }, | |
| "language_info": { | |
| "codemirror_mode": { | |
| "name": "python", | |
| "version": 3 | |
| }, | |
| "file_extension": ".py", | |
| "mimetype": "text/x-python", | |
| "name": "python", | |
| "nbconvert_exporter": "python", | |
| "pygments_lexer": "ipython3", | |
| "version": "3.8" | |
| } | |
| }, | |
| "nbformat_minor": 5, | |
| "nbformat": 4, | |
| "cells": [ | |
| { | |
| "id": "f5e123c1-df85-4c06-b9e9-ed166259fbc5", | |
| "cell_type": "markdown", | |
| "source": "# Using Jupyter Widgets with Arbitrary JavaScript and HTML\n\nThe legacy JavaScript API for the Jupyter notebook presented many problems: it relied on page-level constants, specific to the client implementation.\n\nThis approach shows using Jupyter Widgets in the kernal and DOM as a JSON-based backhaul.", | |
| "metadata": {} | |
| }, | |
| { | |
| "id": "a99bc8c8-a7fb-4bae-a954-1c0852063195", | |
| "cell_type": "code", | |
| "source": "%pip install ipywidgets\nfrom datetime import datetime\nfrom ipywidgets import Textarea, DatetimePicker, VBox, HBox, Text, HTML\nimport json", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [], | |
| "execution_count": 116 | |
| }, | |
| { | |
| "id": "e7b74666-bcec-4da7-b583-a832d9587588", | |
| "cell_type": "markdown", | |
| "source": "## Build the JSON bus widgets", | |
| "metadata": {} | |
| }, | |
| { | |
| "id": "2141fbad-8f31-42c9-9ef0-3c207de4990e", | |
| "cell_type": "code", | |
| "source": "from_js, to_js = [Textarea(description=f\"{d} JS\") for d in [\"from\", \"to\"]]\nfrom_js.add_class(\"my-from-js\")\nto_js.add_class(\"my-to-js\")\nHBox([from_js, to_js])", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [ | |
| { | |
| "execution_count": 117, | |
| "output_type": "execute_result", | |
| "data": { | |
| "text/plain": "HBox(children=(Textarea(value='', description='from JS', _dom_classes=('my-from-js',)), Textarea(value='', des…", | |
| "application/vnd.jupyter.widget-view+json": { | |
| "version_major": 2, | |
| "version_minor": 0, | |
| "model_id": "3cc94426dfa44741a6ae3b67497d08cd" | |
| } | |
| }, | |
| "metadata": {} | |
| } | |
| ], | |
| "execution_count": 117 | |
| }, | |
| { | |
| "id": "15bd634e-cab8-46e0-9c45-1c6fe378d978", | |
| "cell_type": "markdown", | |
| "source": "## Build some \"real\" widgets\n\nThese demonstrate some rich inputs and outputs.", | |
| "metadata": {} | |
| }, | |
| { | |
| "id": "9b495b9f-a764-408e-8d4f-4ce2d7be5033", | |
| "cell_type": "code", | |
| "source": "last_click = DatetimePicker(description=\"last click\")\nbutton_label = Text(\"click me\", description=\"button label\")", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [], | |
| "execution_count": 118 | |
| }, | |
| { | |
| "id": "11c85c89-cc7b-4b10-9ecd-02b4118ee396", | |
| "cell_type": "markdown", | |
| "source": "## Connect events\n\nThese will use JSON as the intermediate, instead of raw Python/JS equivalents of arbitrary code execution.", | |
| "metadata": {} | |
| }, | |
| { | |
| "id": "3b5a1c2d-669d-4027-8262-b0295ac5ff5b", | |
| "cell_type": "code", | |
| "source": "def on_from_js(*_):\n new = json.loads(from_js.value or '{}')\n new_click = new.get(\"last_click\")\n last_click.value = datetime.fromisoformat(new_click) if new_click else None\n\ndef send_js(*_):\n to_js.value = json.dumps({\"button_label\": button_label.value})\n\nfrom_js.observe(on_from_js, \"value\")\nbutton_label.observe(send_js, \"value\")", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [], | |
| "execution_count": 119 | |
| }, | |
| { | |
| "id": "e64f78d5-1f64-4779-9c0b-50fddcceb512", | |
| "cell_type": "markdown", | |
| "source": "## Show custom HTML\n\nThis uses the `%%html` magic, but anything that generates a rich HTML output could work.", | |
| "metadata": {} | |
| }, | |
| { | |
| "id": "96c235e2-202a-42f2-bada-97928b00d7d7", | |
| "cell_type": "code", | |
| "source": "%%html\n<button class=\"my-button\">click me</button>\n<script>\n;(function (){\n function sendPy(data) {\n fromJs = document.querySelector('.my-from-js textarea');\n fromJs.value = JSON.stringify(data);\n fromJs.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));\n }\n function onToJs() {\n let toJs = document.querySelector('.my-to-js textarea');\n let data = JSON.parse(toJs.value);\n button.textContent = data[\"button_label\"];\n }\n let button = document.querySelector('.my-button');\n button.addEventListener(\"click\", () => sendPy({last_click: (new Date()).toISOString()}));\n // polling is not great, but here we are\n setInterval(onToJs, 1000);\n console.log('loaded');\n}).call(this);\n</script>", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [ | |
| { | |
| "output_type": "display_data", | |
| "data": { | |
| "text/plain": "<IPython.core.display.HTML object>", | |
| "text/html": "<button class=\"my-button\">click me</button>\n<script>\n;(function (){\n function sendPy(data) {\n fromJs = document.querySelector('.my-from-js textarea');\n fromJs.value = JSON.stringify(data);\n fromJs.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));\n }\n function onToJs() {\n let toJs = document.querySelector('.my-to-js textarea');\n let data = JSON.parse(toJs.value);\n button.textContent = data[\"button_label\"];\n }\n let button = document.querySelector('.my-button');\n button.addEventListener(\"click\", () => sendPy({last_click: (new Date()).toISOString()}));\n // polling is not great, but here we are\n setInterval(onToJs, 1000);\n console.log('loaded');\n}).call(this);\n</script>\n" | |
| }, | |
| "metadata": {} | |
| } | |
| ], | |
| "execution_count": 120 | |
| }, | |
| { | |
| "id": "de8dcbab-521e-440d-b35c-6e46c724e60e", | |
| "cell_type": "code", | |
| "source": "HBox([button_label, to_js, from_js, last_click])", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [ | |
| { | |
| "execution_count": 121, | |
| "output_type": "execute_result", | |
| "data": { | |
| "text/plain": "HBox(children=(Text(value='click me', description='button label'), Textarea(value='', description='to JS', _do…", | |
| "application/vnd.jupyter.widget-view+json": { | |
| "version_major": 2, | |
| "version_minor": 0, | |
| "model_id": "d9981a2fb32d4792ac6ed3d273cfd4c8" | |
| } | |
| }, | |
| "metadata": {} | |
| } | |
| ], | |
| "execution_count": 121 | |
| }, | |
| { | |
| "id": "c291b717-e4be-47bf-8de3-6cb6b6fbba1e", | |
| "cell_type": "markdown", | |
| "source": "The \"bus\" widgets need to be actually be added to the DOM, but can be visually hidden.", | |
| "metadata": {} | |
| }, | |
| { | |
| "id": "dd5423b2-aa5d-47fd-9bbb-9333cd7c1172", | |
| "cell_type": "code", | |
| "source": "to_js.layout.display = from_js.layout.display = \"none\"", | |
| "metadata": { | |
| "trusted": true | |
| }, | |
| "outputs": [], | |
| "execution_count": 122 | |
| }, | |
| { | |
| "id": "47ac5b8b-3631-4df7-9337-b826522cc05f", | |
| "cell_type": "markdown", | |
| "source": "## Alternatives\n\n- for very complex HTML/CSS, using an `iframe` may work better with a [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) approach\n- some cases can use [`ipylab`](https://github.com/jtpio/ipylab) to further integrate with Jupyter clients, such as:\n - building new UI panels\n - using application-level commands and icons", | |
| "metadata": {} | |
| } | |
| ] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment