├── .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 | [](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 |
--------------------------------------------------------------------------------