├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .nojekyll ├── LICENSE ├── README.md ├── content ├── anywidget.ipynb ├── index.css └── index.js └── requirements.txt /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 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@v4 17 | - name: Setup Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.11' 21 | - name: Install the dependencies 22 | run: | 23 | python -m pip install -r requirements.txt 24 | - name: Build the JupyterLite site 25 | run: | 26 | cp README.md content 27 | jupyter lite build --contents content --output-dir dist 28 | - name: Upload artifact 29 | uses: actions/upload-pages-artifact@v3 30 | with: 31 | path: ./dist 32 | 33 | deploy: 34 | needs: build 35 | if: github.ref == 'refs/heads/main' 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@v4 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | .yarn-packages/ 5 | *.egg-info/ 6 | .ipynb_checkpoints 7 | *.tsbuildinfo 8 | 9 | # Created by https://www.gitignore.io/api/python 10 | # Edit at https://www.gitignore.io/?templates=python 11 | 12 | ### Python ### 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | pip-wheel-metadata/ 36 | share/python-wheels/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | .spyproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # Mr Developer 94 | .mr.developer.cfg 95 | .project 96 | .pydevproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | .dmypy.json 104 | dmypy.json 105 | 106 | # Pyre type checker 107 | .pyre/ 108 | 109 | # OS X stuff 110 | *.DS_Store 111 | 112 | # End of https://www.gitignore.io/api/python 113 | 114 | # jupyterlite 115 | *.doit.db 116 | _output 117 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c), Jupyter Widgets Contrib 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anywidget Lite 2 | 3 | [![lite-badge](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://jupyter-widgets-contrib.github.io/anywidget-lite/lab/index.html?path=anywidget.ipynb) 4 | 5 | Prototype your Jupyter Widget in the browser with anywidget and JupyterLite 💡 6 | 7 | https://github.com/user-attachments/assets/6e35915d-cc57-45da-9b3d-eae8ca58d744 8 | 9 | ## Usage 10 | 11 | Open the following link in your browser and start coding: 12 | 13 | **https://jupyter-widgets-contrib.github.io/anywidget-lite/lab/index.html?path=anywidget.ipynb** 14 | 15 | ## Sharing notebooks 16 | 17 | This JupyterLite project comes with a "Share" button powered by nblink. 18 | nblink is a jupyterlite extension that compresses your notebook into a url that allows anyone to open a copy just from a link. 19 | More about nblink here https://github.com/dwootton/nblink 20 | -------------------------------------------------------------------------------- /content/anywidget.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Create a Jupyter Widget with anywidget\n", 8 | "\n", 9 | "*This notebook is heavily inspired by the \"Getting Started\" guide on the anywidget documentation: https://anywidget.dev/en/getting-started/*\n", 10 | "\n", 11 | "## What is anywidget?\n", 12 | "\n", 13 | "**anywidget** is a Python library that simplifies creating and publishing\n", 14 | "custom [Jupyter Widgets](https://ipywidgets.readthedocs.io/en/latest/).\n", 15 | "No messy build configuration or complicated cookiecutter templates.\n", 16 | "\n", 17 | "It is **not** a new interactive widgets framework, but rather\n", 18 | "an abstraction for creating custom Jupyter Widgets using modern web standards.\n", 19 | "\n", 20 | "## Key features\n", 21 | "\n", 22 | "- 🛠️ Create widgets **without complicated cookiecutter templates**\n", 23 | "- 📚 **Publish to PyPI** like any other Python package\n", 24 | "- 🤖 Prototype **within** `.ipynb` or `.py` files\n", 25 | "- 🚀 Run in **Jupyter**, **JupyterLab**, **Google Colab**, **VSCode**, and more\n", 26 | "- ⚡ Develop with **instant HMR**, like modern web frameworks\n" 27 | ] 28 | }, 29 | { 30 | "cell_type": "markdown", 31 | "metadata": {}, 32 | "source": [ 33 | "First you need to install `anywidget`:" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": { 40 | "trusted": true 41 | }, 42 | "outputs": [], 43 | "source": [ 44 | "%pip install anywidget ipywidgets==8.1.2" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "Then you can define a new widget and provide the `count`, `_esm` and `_css` attributes." 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": { 58 | "trusted": true 59 | }, 60 | "outputs": [], 61 | "source": [ 62 | "import anywidget\n", 63 | "import traitlets\n", 64 | "\n", 65 | "class CounterWidget(anywidget.AnyWidget):\n", 66 | " _esm = \"\"\"\n", 67 | " function render({ model, el }) {\n", 68 | " let getCount = () => model.get(\"count\");\n", 69 | " let button = document.createElement(\"button\");\n", 70 | " button.classList.add(\"counter-button\");\n", 71 | " button.innerHTML = `count is ${getCount()}`;\n", 72 | " button.addEventListener(\"click\", () => {\n", 73 | " model.set(\"count\", getCount() + 1);\n", 74 | " model.save_changes();\n", 75 | " });\n", 76 | " model.on(\"change:count\", () => {\n", 77 | " button.innerHTML = `count is ${getCount()}`;\n", 78 | " });\n", 79 | " el.appendChild(button);\n", 80 | " }\n", 81 | "\texport default { render };\n", 82 | " \"\"\"\n", 83 | " _css=\"\"\"\n", 84 | " .counter-button { background-color: #ea580c; }\n", 85 | " .counter-button:hover { background-color: #9a3412; }\n", 86 | " \"\"\"\n", 87 | " count = traitlets.Int(0).tag(sync=True)\n", 88 | "\n", 89 | "counter = CounterWidget()\n", 90 | "counter.count = 42\n", 91 | "counter" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "- `count` is a stateful property for that both the client JavaScript and Python have access to.\n", 99 | " Shared state is defined via [traitlets](https://traitlets.readthedocs.io/en/stable/) with the `sync=True`\n", 100 | " keyword argument.\n", 101 | "\n", 102 | "- `_esm` specifies a **required** [ECMAScript module](https://nodejs.org/api/esm.html) for the widget.\n", 103 | " It defines and exports `render`, a function for rendering and initializes dynamic updates for the custom widget.\n", 104 | "\n", 105 | "- `_css` specifies an **optional** CSS stylesheet to load for the widget. It can be a full URL or plain text. Styles are loaded\n", 106 | " in the global scope if using this feature, so take care to avoid naming conflicts.\n", 107 | "\n", 108 | " Feel free to modify some of the code above and re-execute the cells to see the changes 🪄" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "## Progressive Development\n", 116 | "\n", 117 | "As your widgets grow in complexity, it is recommended to separate your\n", 118 | "front-end code from your Python code. Just move the `_esm` and `_css`\n", 119 | "definitions to separate files and reference them via path." 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": { 126 | "trusted": true 127 | }, 128 | "outputs": [], 129 | "source": [ 130 | "from pathlib import Path\n", 131 | "\n", 132 | "class CounterWidget(anywidget.AnyWidget):\n", 133 | " _esm = Path('/drive/index.js')\n", 134 | " _css= Path('/drive/index.css')\n", 135 | " count = traitlets.Int(0).tag(sync=True)\n", 136 | "\n", 137 | "counter = CounterWidget()\n", 138 | "counter.count = 42\n", 139 | "counter" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "**Note**: since this particular notebook is meant to be used in JupyterLite, we specify `/drive` as the prefix for finding the `.js` and `.css` files. `/drive` is the location of the underlying (in-browser) file system where JupyterLite expects to find the files, so they can be displayed in the file browser.\n", 147 | "\n", 148 | "You can now open `index.js` and `index.css` in JupyterLab and edit the files directly. After making changes, you will have to recreate the widget so they are applied." 149 | ] 150 | } 151 | ], 152 | "metadata": { 153 | "kernelspec": { 154 | "display_name": "Python (Pyodide)", 155 | "language": "python", 156 | "name": "python" 157 | }, 158 | "language_info": { 159 | "codemirror_mode": { 160 | "name": "python", 161 | "version": 3 162 | }, 163 | "file_extension": ".py", 164 | "mimetype": "text/x-python", 165 | "name": "python", 166 | "nbconvert_exporter": "python", 167 | "pygments_lexer": "ipython3", 168 | "version": "3.8" 169 | } 170 | }, 171 | "nbformat": 4, 172 | "nbformat_minor": 4 173 | } 174 | -------------------------------------------------------------------------------- /content/index.css: -------------------------------------------------------------------------------- 1 | .counter-button { background-color: #ea580c; } 2 | .counter-button:hover { background-color: #9a3412; } 3 | -------------------------------------------------------------------------------- /content/index.js: -------------------------------------------------------------------------------- 1 | function render({ model, el }) { 2 | let getCount = () => model.get("count"); 3 | let button = document.createElement("button"); 4 | button.classList.add("counter-button"); 5 | button.innerHTML = `count is ${getCount()}`; 6 | button.addEventListener("click", () => { 7 | model.set("count", getCount() + 1); 8 | model.save_changes(); 9 | }); 10 | model.on("change:count", () => { 11 | button.innerHTML = `count is ${getCount()}`; 12 | }); 13 | el.appendChild(button); 14 | } 15 | 16 | export default { render }; 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anywidget 2 | jupyterlab-widgets==3.0.13 3 | jupyterlab~=4.3.6 4 | jupyterlite-core==0.5.1 5 | jupyterlite-pyodide-kernel==0.5.2 6 | notebook~=7.3.3 7 | nblink 8 | --------------------------------------------------------------------------------