{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "031c5d57-b722-4999-88ac-686ac83d3ef1", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "pn.extension()" ] }, { "cell_type": "markdown", "id": "9908a714-692d-4513-aca8-b251a627cae4", "metadata": {}, "source": [ "Panel's `AnyWidgetComponent` class simplifies the creation of custom Panel components using the [AnyWidget Front End Module (AFM) specification](https://anywidget.dev/en/afm/) maintained by [`AnyWidget`](https://anywidget.dev/).\n", "\n", "This allows the Panel, Jupyter and other communities to collaborate and share JavaScript code for widgets." ] }, { "cell_type": "code", "execution_count": null, "id": "93ab8716-5052-4a89-83b4-dd78576816ce", "metadata": {}, "outputs": [], "source": [ "import param\n", "import panel as pn\n", "from panel.custom import AnyWidgetComponent\n", "\n", "pn.extension()\n", "\n", "class CounterWidget(AnyWidgetComponent):\n", " _esm = \"\"\"\n", " function render({ model, el }) {\n", " let count = () => model.get(\"value\");\n", " let btn = document.createElement(\"button\");\n", " btn.innerHTML = `count is ${count()}`;\n", " btn.addEventListener(\"click\", () => {\n", " model.set(\"value\", count() + 1);\n", " model.save_changes();\n", " });\n", " model.on(\"change:value\", () => {\n", " btn.innerHTML = `count is ${count()}`;\n", " });\n", " el.appendChild(btn);\n", " }\n", " export default { render };\n", " \"\"\"\n", " value = param.Integer()\n", "\n", "CounterWidget().servable()" ] }, { "cell_type": "markdown", "id": "1a37ed44-8c89-40d6-9c01-b22c5a4c4d0a", "metadata": {}, "source": [ ":::{note}\n", "Panel's `AnyWidgetComponent` supports using the [`AnyWidget`](https://anywidget.dev/) API on the JavaScript side and the [`param`](https://param.holoviz.org/) parameters API on the Python side.\n", "\n", "If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md).\n", ":::\n", "\n", "\n", "## API\n", "\n", "### AnyWidgetComponent Attributes\n", "\n", "- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `default` object or function that returns an object. The object should contain a `render` function and optionally an `initialize` function. In a development environment such as a notebook or when using `--dev` flag, the module will automatically reload upon saving changes.\n", "- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved.\n", "- **`_stylesheets`** (list[str] | list[PurePath]): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments. It works similarly to the `AnyWidget` `_css` attribute.\n", "\n", ":::note\n", "You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file it is referenced in.\n", ":::\n", "\n", "#### `render` Function\n", "\n", "The `_esm` `default` object must contain a `render` function. It accepts the following parameters:\n", "\n", "- **`model`**: Represents the parameters of the component and provides methods to `.get` values, `.set` values, and `.save_changes`. In addition to the AnyWidgets methods, Panel uniquely provides the `get_child` method to enable rendering of child models.\n", "- **`el`**: The parent HTML element to which HTML elements are appended.\n", "\n", "For more details, see [`AnyWidget`](https://anywidget.dev/).\n", "\n", "## Usage\n", "\n", "### Styling with CSS\n", "\n", "Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML." ] }, { "cell_type": "code", "execution_count": null, "id": "16d63729-efec-4033-8c3e-12295b3910e6", "metadata": {}, "outputs": [], "source": [ "import param\n", "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent\n", "\n", "pn.extension()\n", "\n", "class CounterWidget(AnyWidgetComponent):\n", " _esm = \"\"\"\n", " function render({ model, el }) {\n", " let count = () => model.get(\"value\");\n", " let btn = document.createElement(\"button\");\n", " btn.innerHTML = `count is ${count()}`;\n", " btn.addEventListener(\"click\", () => {\n", " model.set(\"value\", count() + 1);\n", " model.save_changes();\n", " });\n", " model.on(\"change:value\", () => {\n", " btn.innerHTML = `count is ${count()}`;\n", " });\n", " el.appendChild(btn);\n", " }\n", " export default { render };\n", " \"\"\"\n", " _stylesheets = [\n", " \"\"\"\n", " button { color: white; font-size: 1.75rem; background-color: #ea580c; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; }\n", " button:hover { background-color: #9a3412; }\n", " \"\"\"\n", " ]\n", " value = param.Integer()\n", "\n", "CounterWidget().servable()" ] }, { "cell_type": "markdown", "id": "7619dad4-4dfe-43a1-aac1-32c57ddffc58", "metadata": {}, "source": [ ":::{note}\n", "\n", "The `AnyWidget` will automatically add the CSS class `counter-widget` to the `el`.\n", "\n", "The `AnyWidgetComponent` does not add this class, but you can do it yourself via `el.classList.add(\"counter-widget\");`.\n", "\n", ":::\n", "\n", "\n", "### Dependency Imports\n", "\n", "JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/)." ] }, { "cell_type": "code", "execution_count": null, "id": "e5bd2900-61d6-4112-8522-6a0239bf6d1f", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent\n", "\n", "\n", "class ConfettiButton(AnyWidgetComponent):\n", "\n", " _esm = \"\"\"\n", " import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n", "\n", " function render({ el }) {\n", " let btn = document.createElement(\"button\");\n", " btn.innerHTML = \"Click Me\";\n", " btn.addEventListener(\"click\", () => {\n", " confetti();\n", " });\n", " el.appendChild(btn);\n", " }\n", " export default { render }\n", " \"\"\"\n", "\n", "ConfettiButton().servable()\n" ] }, { "cell_type": "markdown", "id": "cc5c58de-544d-4cac-b103-b6db1e4dc139", "metadata": {}, "source": [ "Use the `_importmap` attribute for more concise module references." ] }, { "cell_type": "code", "execution_count": null, "id": "c20cda5c-7176-4d3d-8b56-01acad7aa924", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent\n", "\n", "\n", "class ConfettiButton(AnyWidgetComponent):\n", "\n", " _importmap = {\n", " \"imports\": {\n", " \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n", " }\n", " }\n", "\n", " _esm = \"\"\"\n", " import confetti from \"canvas-confetti\";\n", "\n", " function render({ el }) {\n", " let btn = document.createElement(\"button\");\n", " btn.innerHTML = \"Click Me\";\n", " btn.addEventListener(\"click\", () => {\n", " confetti();\n", " });\n", " el.appendChild(btn);\n", " }\n", " export default { render }\n", " \"\"\"\n", "\n", "ConfettiButton().servable()" ] }, { "cell_type": "markdown", "id": "c1d8d880-3b55-4eb4-998c-3fb265b47322", "metadata": {}, "source": [ "See the [import map documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more information about the import map format.\n", "\n", "### External Files\n", "\n", "You can load JavaScript and CSS from files by providing the paths to these files.\n", "\n", "Create the file **counter_button.py**.\n", "\n", "```python\n", "from pathlib import Path\n", "\n", "import param\n", "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent\n", "\n", "pn.extension()\n", "\n", "class CounterButton(AnyWidgetComponent):\n", "\n", " value = param.Integer()\n", "\n", " _esm = Path(\"counter_button.js\")\n", " _stylesheets = [Path(\"counter_button.css\")]\n", "\n", "CounterButton().servable()\n", "```\n", "\n", "Now create the file **counter_button.js**.\n", "\n", "```javascript\n", "function render({ model, el }) {\n", " let value = () => model.get(\"value\");\n", " let btn = document.createElement(\"button\");\n", " btn.innerHTML = `count is ${value()}`;\n", " btn.addEventListener(\"click\", () => {\n", " model.set('value', value() + 1);\n", " model.save_changes();\n", " });\n", " model.on(\"change:value\", () => {\n", " btn.innerHTML = `count is ${value()}`;\n", " });\n", " el.appendChild(btn);\n", "}\n", "export default { render }\n", "```\n", "\n", "Now create the file **counter_button.css**.\n", "\n", "```css\n", "button {\n", " background: #0072B5;\n", " color: white;\n", " border: none;\n", " padding: 10px;\n", " border-radius: 4px;\n", "}\n", "button:hover {\n", " background: #4099da;\n", "}\n", "```\n", "\n", "Serve the app with `panel serve counter_button.py --autoreload`.\n", "\n", "You can now edit the JavaScript or CSS file, and the changes will be automatically reloaded.\n", "\n", "- Try changing the `innerHTML` from `count is ${value()}` to `COUNT IS ${value()}` and observe the update. Note that you must update `innerHTML` in two places.\n", "- Try changing the background color from `#0072B5` to `#008080`." ] }, { "cell_type": "markdown", "id": "6f5b0f26-b5ec-455b-91fb-4032f34086ee", "metadata": {}, "source": [ "## Displaying A Single Child\n", "\n", "You can display Python objects by defining a `Child` parameter. Please note that this feature is **currently not supported by `AnyWidget`**.\n", "\n", "Lets start with the simplest example:" ] }, { "cell_type": "code", "execution_count": null, "id": "db65cc94-6e23-460a-8796-e10b55c88300", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent, Child\n", "\n", "pn.extension()\n", "\n", "\n", "class Example(AnyWidgetComponent):\n", "\n", " child = Child()\n", "\n", " _esm = \"\"\"\n", " function render({ model, el }) {\n", " const button = document.createElement(\"button\");\n", " button.append(model.get_child(\"child\"))\n", " el.appendChild(button);\n", " }\n", "\n", " export default { render };\n", " \"\"\"\n", "\n", "\n", "Example(child=pn.panel(\"A **Markdown** pane!\")).servable()" ] }, { "cell_type": "markdown", "id": "401345bd-5a53-4461-96c9-959b2ea41904", "metadata": {}, "source": [ "If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`:" ] }, { "cell_type": "code", "execution_count": null, "id": "49c4c66f-9b4f-4461-ac3c-4f9ad2dbd1a7", "metadata": {}, "outputs": [], "source": [ "Example(child=\"A **Markdown** pane!\").servable()" ] }, { "cell_type": "markdown", "id": "5ef1c471-2ed8-48ef-a478-3873f7d613fd", "metadata": {}, "source": [ "If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument." ] }, { "cell_type": "code", "execution_count": null, "id": "2e7c1209-2b86-433a-8dd2-ca351ac02979", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent, Child\n", "\n", "\n", "class Example(AnyWidgetComponent):\n", "\n", " child = Child(class_=pn.pane.Markdown)\n", "\n", " _esm = \"\"\"\n", " function render({ model, el }) {\n", " const button = document.createElement(\"button\");\n", " button.append(model.get_child(\"child\"))\n", " el.appendChild(button);\n", " }\n", "\n", " export default { render };\n", " \"\"\"\n", "\n", "\n", "Example(child=pn.panel(\"A **Markdown** pane!\")).servable()" ] }, { "cell_type": "markdown", "id": "2e7da88d-aaa6-41ba-8a33-a0f93e0eb302", "metadata": {}, "source": [ "The `class_` argument also supports a tuple of types:\n", "\n", "```python\n", " child = Child(class_=(pn.pane.Markdown, pn.pane.HTML))\n", "```" ] }, { "cell_type": "markdown", "id": "53ce78f8-b4f5-4ff6-afe0-563efa0d3fc7", "metadata": {}, "source": [ "## Displaying a List of Children\n", "\n", "You can also display a `List` of `Viewable` objects using the `Children` parameter type:" ] }, { "cell_type": "code", "execution_count": null, "id": "634d84e4-e372-4cff-b40e-7e16df179765", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "from panel.custom import AnyWidgetComponent, Children\n", "\n", "pn.extension()\n", "\n", "\n", "class Example(AnyWidgetComponent):\n", "\n", " objects = Children()\n", "\n", " _esm = \"\"\"\n", " function render({ model, el }) {\n", " const div = document.createElement('div')\n", " div.append(...model.get_child(\"objects\"))\n", " el.appendChild(div);\n", " }\n", "\n", " export default { render };\n", " \"\"\"\n", "\n", "\n", "Example(\n", " objects=[pn.panel(\"A **Markdown** pane!\"), pn.widgets.Button(name=\"Click me!\"), {\"text\": \"I'm shown as a JSON Pane\"}]\n", ").servable()" ] }, { "cell_type": "markdown", "id": "748776ca-925d-44dd-b503-3823252165c9", "metadata": {}, "source": [ ":::note\n", "You can change the `item_type` to a specific subtype of `Viewable` or a tuple of\n", "`Viewable` subtypes.\n", ":::" ] }, { "cell_type": "markdown", "id": "91a8cb50-c38d-4d8a-b992-7bf013926a73", "metadata": {}, "source": [ "### React\n", "\n", "You can use React with `AnyWidget` as shown below." ] }, { "cell_type": "code", "execution_count": null, "id": "1d2b5ea2-18cd-47fa-a639-7535f5c1652d", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "import param\n", "\n", "from panel.custom import AnyWidgetComponent\n", "\n", "\n", "class CounterButton(AnyWidgetComponent):\n", "\n", " value = param.Integer()\n", "\n", " _importmap = {\n", " \"imports\": {\n", " \"@anywidget/react\": \"https://esm.sh/@anywidget/react\",\n", " \"react\": \"https://esm.sh/react\",\n", " }\n", " }\n", "\n", " _esm = \"\"\"\n", " import * as React from \"react\"; /* mandatory import */\n", " import { createRender, useModelState } from \"@anywidget/react\";\n", "\n", " const render = createRender(() => {\n", " const [value, setValue] = useModelState(\"value\");\n", " return (\n", " \n", " );\n", " });\n", " export default { render }\n", " \"\"\"\n", "\n", "CounterButton().servable()" ] }, { "cell_type": "markdown", "id": "a8e7f361-5df5-4fe0-b81a-a50d6680f0f9", "metadata": {}, "source": [ ":::{note}\n", "You will notice that Panel's `AnyWidgetComponent` can be used with React and [JSX](https://react.dev/learn/writing-markup-with-jsx) without any build tools. Instead of build tools, Panel uses [Sucrase](https://sucrase.io/) to transpile the JSX code to JavaScript on the client side.\n", ":::\n", "\n", "## References\n", "\n", "### Tutorials\n", "\n", "- [Build Custom Components](../../how_to/custom_components/esm/custom_layout.md)\n", "\n", "### How-To Guides\n", "\n", "- [Convert `AnyWidget` widgets](../../how_to/migrate/anywidget/index.md)\n", "\n", "### Reference Guides\n", "\n", "- [`AnyWidgetComponent`](AnyWidgetComponent.ipynb)\n", "- [`JSComponent`](JSComponent.ipynb)\n", "- [`ReactComponent`](ReactComponent.ipynb)" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 }