{
 "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",
    "        <button onClick={() => setValue(value + 1)}>\n",
    "          count is {value}\n",
    "        </button>\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
}
