` tag.
+
+The `ImageButton` now works as any other widget. Lets try the `.from_param` method to create a `ImageButton` from a `param` class.
+
+```{pyodide}
+class MyClass(param.Parameterized):
+
+ clicks = param.Integer(default=0)
+
+ value = param.Event()
+
+ @param.depends("value", watch=True)
+ def _handle_value(self):
+ if self.value:
+ self.clicks += 1
+
+my_instance = MyClass()
+button2 = ImageButton.from_param(my_instance.param.value)
+pn.Column(button2, my_instance.param.clicks).servable()
+```
+
+When you click the image button you should see the number of clicks increase.
diff --git a/doc/how_to/custom_components/esm/dataframe.md b/doc/how_to/custom_components/esm/dataframe.md
new file mode 100644
index 0000000000..a5156aca78
--- /dev/null
+++ b/doc/how_to/custom_components/esm/dataframe.md
@@ -0,0 +1,135 @@
+# Rendering DataFrames using ESM components
+
+In this guide we will show you how to implement ESM components with a `DataFrame` parameter.
+
+## Creating a `JSComponent`
+
+In this example we will show you how to create a custom DataFrame Pane. The example will be based on the [GridJS table](https://gridjs.io/).
+
+```{pyodide}
+import random
+import pandas as pd
+import param
+import panel as pn
+
+from panel.custom import JSComponent
+
+class GridJS(JSComponent):
+
+ object = param.DataFrame()
+
+ _extension_name = 'gridjs'
+
+ _esm = """
+ import * as gridjs from "https://esm.sh/gridjs@6.2.0"
+
+ function get_config(model) {
+ const data = model.object
+ const columns = Object.keys(data).filter(key => key !== "index");
+ const rows = []
+ for (let index=0; index < data["index"].shape[0]; index++) {
+ const row = columns.map(key => data[key][index])
+ rows.push(row)
+ }
+ return {columns: columns, data: rows, resizable: true, sort: true}
+ }
+
+ export function render({model, el}) {
+ console.log(model.object)
+ const config = get_config(model)
+ console.log(config)
+ const grid = new gridjs.Grid(config).render(el)
+ model.on('object', () => grid.updateConfig(get_config(model)).forceRender())
+ }
+ """
+
+ __css__ = [
+ "https://unpkg.com/gridjs/dist/theme/mermaid.min.css"
+ ]
+
+
+def data(event):
+ return pd.DataFrame([
+ ["John", "john@example.com", "(353) 01 222 3333", random.uniform(0, 1)],
+ ["Mark", "mark@gmail.com", "(01) 22 888 4444", random.uniform(0, 1)],
+ ["Eoin", "eoin@gmail.com", "0097 22 654 00033", random.uniform(0, 1)],
+ ["Sarah", "sarahcdd@gmail.com", "+322 876 1233", random.uniform(0, 1)],
+ ["Afshin", "afshin@mail.com", "(353) 22 87 8356", random.uniform(0, 1)]
+ ], columns= ["Name", "Email", "Phone Number", "Random"])
+
+update_button = pn.widgets.Button(name="UPDATE", button_type="primary")
+
+grid = GridJS(object=pn.bind(data, update_button), sizing_mode="stretch_width")
+
+pn.Column(update_button, grid).servable()
+```
+
+The main challenge of creating this component is understanding the structure of `data.value` and how it can be converted to a format (`config`) that `gridjs.Grid` accepts.
+
+To help you understand what the `data.value` and `config` values looks like, I've logged them to the *browser console* using `console.log`.
+
+![DataFrame in the console](../../../_static/reactive-html-dataframe-in-console.png)
+
+## Creating a `ReactComponent`
+
+Now let's see how to implement the same thing using the [GridJS React integration](https://gridjs.io/docs/integrations/react/).
+
+```{pyodide}
+import random
+import pandas as pd
+import param
+import panel as pn
+
+from panel.custom import PaneBase, ReactComponent
+
+class GridJS(ReactComponent):
+
+ object = param.DataFrame()
+
+ _extension_name = 'gridjs'
+
+ _esm = """
+ import { useEffect, useState } from "react"
+ import { Grid } from "https://esm.sh/gridjs-react@6.1.1"
+
+ function get_config(data) {
+ const columns = Object.keys(data).filter(key => key !== "index");
+ const rows = []
+ for (let index=0; index < data["index"].shape[0]; index++) {
+ const row = columns.map(key => data[key][index])
+ rows.push(row)
+ }
+ return {columns: columns, data: rows, resizable: true, sort: true}
+ }
+
+ export function render({model, el}) {
+ const [data] = model.useState("object")
+ const [config, setConfig] = useState(get_config(data))
+ useEffect(() => {
+ const newConfig = get_config(data);
+ setConfig(newConfig);
+ }, [data])
+ return
+ }
+ """
+
+ __css__ = [
+ "https://unpkg.com/gridjs/dist/theme/mermaid.min.css"
+ ]
+
+
+def data(event):
+ return pd.DataFrame([
+ ["John", "john@example.com", "(353) 01 222 3333", random.uniform(0, 1)],
+ ["Mark", "mark@gmail.com", "(01) 22 888 4444", random.uniform(0, 1)],
+ ["Eoin", "eoin@gmail.com", "0097 22 654 00033", random.uniform(0, 1)],
+ ["Sarah", "sarahcdd@gmail.com", "+322 876 1233", random.uniform(0, 1)],
+ ["Afshin", "afshin@mail.com", "(353) 22 87 8356", random.uniform(0, 1)]
+ ], columns= ["Name", "Email", "Phone Number", "Random"])
+
+update_button = pn.widgets.Button(name="UPDATE", button_type="primary")
+
+grid = GridJS(object=pn.bind(data, update_button), sizing_mode="stretch_width")
+
+pn.Column(update_button, grid).servable()
+```
diff --git a/doc/how_to/custom_components/examples/esm_canvas.md b/doc/how_to/custom_components/examples/esm_canvas.md
new file mode 100644
index 0000000000..335baa7883
--- /dev/null
+++ b/doc/how_to/custom_components/examples/esm_canvas.md
@@ -0,0 +1,90 @@
+# Build a Custom Canvas Component
+
+```{pyodide}
+import param
+import panel as pn
+
+from panel.custom import JSComponent
+
+pn.extension()
+
+class Canvas(JSComponent):
+
+ color = param.Color(default='#000000')
+
+ line_width = param.Number(default=1, bounds=(0.1, 10))
+
+ uri = param.String()
+
+ _esm = """
+export function render({model, el}){
+ // Create canvas
+ const canvas = document.createElement('canvas');
+ canvas.style.border = '1px solid';
+ canvas.width = model.width;
+ canvas.height = model.height;
+ const ctx = canvas.getContext("2d")
+
+ // Set up drawing handlers
+ let start = null
+ canvas.addEventListener('mousedown', (event) => {
+ start = event
+ ctx.beginPath()
+ ctx.moveTo(start.offsetX, start.offsetY)
+ })
+ canvas.addEventListener('mousemove', (event) => {
+ if (start == null)
+ return
+ ctx.lineTo(event.offsetX, event.offsetY)
+ ctx.stroke()
+ })
+ canvas.addEventListener('mouseup', () => {
+ start = null
+ })
+
+ // Update styles
+ model.on(['color', 'line_width'], () => {
+ ctx.lineWidth = model.line_width;
+ ctx.strokeStyle = model.color;
+ })
+
+ // Create clear button
+ const clearButton = document.createElement('button');
+ clearButton.textContent = 'Clear';
+ clearButton.addEventListener('click', () => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
+ model.uri = ""
+ })
+ // Create save button
+ const saveButton = document.createElement('button');
+ saveButton.textContent = 'Save';
+ saveButton.addEventListener('click', () => {
+ model.uri = canvas.toDataURL();
+ })
+ // Append elements to the parent element
+ el.appendChild(canvas);
+ el.appendChild(clearButton);
+ el.appendChild(saveButton);
+}
+"""
+
+canvas = Canvas(height=400, width=400)
+png_view = pn.pane.HTML(
+ pn.rx("
").format(uri=canvas.param.uri),
+ height=400
+)
+
+pn.Column(
+ '# Drag on canvas to draw\n To export the drawing to a png click save.',
+ pn.Param(
+ canvas.param,
+ default_layout=pn.Row,
+ parameters=['color', 'line_width'],
+ show_name=False
+ ),
+ pn.Row(
+ canvas,
+ png_view
+ ),
+).servable()
+```
diff --git a/doc/how_to/custom_components/examples/esm_leaflet.md b/doc/how_to/custom_components/examples/esm_leaflet.md
new file mode 100644
index 0000000000..e45fcd3c7f
--- /dev/null
+++ b/doc/how_to/custom_components/examples/esm_leaflet.md
@@ -0,0 +1,124 @@
+# Build a Custom Leaflet Component
+
+The custom `LeafletHeatMap` component demonstrates a number of concepts. Let us start by defining the component:
+
+```{pyodide}
+import param
+import pandas as pd
+import panel as pn
+import numpy as np
+
+from panel.custom import JSComponent
+
+class LeafletHeatMap(JSComponent):
+
+ attribution = param.String(doc="Tile source attribution.")
+
+ blur = param.Integer(default=18, bounds=(5, 50), doc="Amount of blur to apply to heatmap")
+
+ center = param.XYCoordinates(default=(0, 0), doc="The center of the map.")
+
+ data = param.DataFrame(doc="The heatmap data to plot, should have 'x', 'y' and 'value' columns.")
+
+ tile_url = param.String(doc="Tile source URL with {x}, {y} and {z} parameter")
+
+ min_alpha = param.Number(default=0.2, bounds=(0, 1), doc="Minimum alpha of the heatmap")
+
+ radius = param.Integer(default=25, bounds=(5, 50), doc="The radius of heatmap values on the map")
+
+ x = param.String(default='longitude', doc="Column in the data with longitude coordinates")
+
+ y = param.String(default='latitude', doc="Column in the data with latitude coordinates")
+
+ value = param.String(doc="Column in the data with the data values")
+
+ zoom = param.Integer(default=13, bounds=(0, 21), doc="The plots zoom-level")
+
+ _esm = """
+ import L from "https://esm.sh/leaflet@1.7.1"
+ import * as Lheat from "https://esm.sh/leaflet.heat@0.2.0"
+
+ function get_records(model) {
+ const records = []
+ for (let i=0; i
{ model.zoom = map.getZoom() })
+
+ const tileLayer = L.tileLayer(model.tile_url, {
+ attribution: model.attribution,
+ maxZoom: 21,
+ tileSize: 512,
+ zoomOffset: -1,
+ }).addTo(map)
+
+ model.on("after_render", () => {
+ console.log(Lheat)
+ map.invalidateSize()
+ const data = get_records(model)
+ const heatLayer = L.heatLayer(
+ data, {
+ blur: model.blur,
+ radius: model.radius,
+ max: 10,
+ minOpacity: model.min_alpha
+ }).addTo(map)
+
+ model.on(['blur', 'min_alpha', 'radius'], () => {
+ heatLayer.setOptions({
+ blur: model.blur,
+ minOpacity: model.min_alpha,
+ radius: model.radius,
+ })
+ })
+ model.on('change:data', () => heatLayer.setLatLngs(get_records(model)))
+ })
+ }"""
+
+ _stylesheets = ['https://unpkg.com/leaflet@1.7.1/dist/leaflet.css']
+
+pn.extension(template='bootstrap')
+```
+
+Some of the concepts this component demonstrates:
+
+- Loading of external libraries, specifically leaflet.js and the leaflet.heat plugin.
+- Adding event listeners with `model.on`
+- Delaying rendering by defining an `after_render` lifecycle hook.
+- Loading of an external stylesheet by including it in the list of `_stylesheets`.
+
+Now let's try this component:
+
+```{pyodide}
+url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv"
+
+earthquakes = pd.read_csv(url)
+
+heatmap = LeafletHeatMap(
+ attribution='Map data © OpenStreetMap contributors',
+ data=earthquakes[['longitude', 'latitude', 'mag']],
+ min_height=500,
+ tile_url='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.jpg',
+ radius=30,
+ sizing_mode='stretch_both',
+ value='mag',
+ zoom=2,
+)
+
+description=pn.pane.Markdown(f'## Earthquakes between {earthquakes.time.min()} and {earthquakes.time.max()}\n\n[Data Source]({url})', sizing_mode="stretch_width")
+
+pn.Column(
+ description,
+ pn.Row(
+ heatmap.controls(['blur', 'min_alpha', 'radius', 'zoom']).servable(target='sidebar'),
+ heatmap.servable(),
+ sizing_mode='stretch_both'
+ ),
+ sizing_mode='stretch_both'
+)
+```
diff --git a/doc/how_to/custom_components/examples/esm_material_ui.md b/doc/how_to/custom_components/examples/esm_material_ui.md
new file mode 100644
index 0000000000..c85fe8b651
--- /dev/null
+++ b/doc/how_to/custom_components/examples/esm_material_ui.md
@@ -0,0 +1,109 @@
+# Wrapping Material UI components
+
+:::{note}
+The `MaterialBase` component is defined before the call to `pn.extension` to allow us to load the `_extension_name` and thereby initialize the required JS and CSS resources. Ordinarily the component would be defined in an external module.
+:::
+
+```{pyodide}
+import param
+import panel as pn
+
+from panel.custom import ReactComponent
+
+class MaterialComponent(ReactComponent):
+
+ _importmap = {
+ "imports": {
+ "@mui/material/": "https://esm.sh/@mui/material@5.11.14/",
+ }
+ }
+
+pn.extension(template='material')
+```
+
+This example demonstrates how to wrap Material UI components using `ReactComponent`.
+
+```{pyodide}
+
+class Button(MaterialComponent):
+
+ disabled = param.Boolean(default=False)
+
+ label = param.String(default='')
+
+ variant = param.Selector(objects=["contained", "outlined", "text"])
+
+ _esm = """
+ import Button from '@mui/material/Button';
+
+ export function render({ model }) {
+ const [label] = model.useState("label")
+ const [variant] = model.useState("variant")
+ const [disabled] = model.useState("disabled")
+ return (
+
+ )
+ }
+ """
+
+class Rating(MaterialComponent):
+
+ value = param.Number(default=0, bounds=(0, 5))
+
+ _esm = """
+ import Rating from '@mui/material/Rating'
+
+ export function render({model}) {
+ const [value, setValue] = model.useState("value")
+ return (
+ setValue(newValue) }
+ />
+ )
+ }
+ """
+
+class DiscreteSlider(MaterialComponent):
+
+ marks = param.List(default=[
+ {'value': 0, 'label': '0°C'},
+ {'value': 20, 'label': '20°C'},
+ {'value': 37, 'label': '37°C'},
+ {'value': 100, 'label': '100°C'},
+ ])
+
+ value = param.Number(default=20)
+
+ _esm = """
+ import Box from '@mui/material/Box';
+ import Slider from '@mui/material/Slider';
+
+ export function render({ model }) {
+ const [value, setValue] = model.useState("value")
+ const [marks] = model.useState("marks")
+ return (
+
+ setValue(e.target.value)}
+ step={null}
+ valueLabelDisplay="auto"
+ />
+
+ );
+ }
+ """
+
+button = Button()
+rating = Rating(value=3)
+slider = DiscreteSlider()
+
+pn.Row(
+ pn.Column(button.controls(['disabled', 'label', 'variant']), button),
+ pn.Column(rating.controls(['value']), rating),
+ pn.Column(slider.controls(['value']), slider),
+).servable()
+```
diff --git a/doc/how_to/custom_components/index.md b/doc/how_to/custom_components/index.md
index a3d6c3f36b..95e5626b89 100644
--- a/doc/how_to/custom_components/index.md
+++ b/doc/how_to/custom_components/index.md
@@ -18,12 +18,120 @@ How to build custom components that are combinations of existing components.
::::
+### Examples
+
+::::{grid} 1 2 2 3
+:gutter: 1 1 1 2
+
+:::{grid-item-card} Build a Plot Viewer
+:img-top: https://assets.holoviz.org/panel/how_to/custom_components/plot_viewer.png
+:link: examples/plot_viewer
+:link-type: doc
+
+Build a custom component wrapping a bokeh plot and some widgets using the `Viewer` pattern.
+:::
+
+:::{grid-item-card} Build a Table Viewer
+:img-top: https://assets.holoviz.org/panel/how_to/custom_components/table_viewer.png
+:link: examples/table_viewer
+:link-type: doc
+
+Build a custom component wrapping a table and some widgets using the `Viewer` pattern.
+:::
+
+::::
+
```{toctree}
:titlesonly:
:hidden:
:maxdepth: 2
custom_viewer
+examples/plot_viewer
+examples/table_viewer
+```
+
+## ESM Components
+
+Build custom components in Javascript using so called ESM components, which allow you to write components that automatically sync parameter state between Python and JS. ESM components can be written in pure JS, using React or using the AnyWidget specification.
+
+::::{grid} 1 2 2 3
+:gutter: 1 1 1 2
+
+:::{grid-item-card} {octicon}`pencil;2.5em;sd-mr-1 sd-animate-grow50` Add callbacks to ESM components
+:link: esm/callbacks
+:link-type: doc
+
+How to add both JS and Python based callbacks to ESM components.
+:::
+
+:::{grid-item-card} {octicon}`pencil;2.5em;sd-mr-1 sd-animate-grow50` Create Custom Widgets
+:link: esm/custom_widgets
+:link-type: doc
+
+How to create a custom widget using ESM components
+:::
+
+:::{grid-item-card} {octicon}`columns;2.5em;sd-mr-1 sd-animate-grow50` Create Custom Layouts
+:link: esm/custom_layout
+:link-type: doc
+
+How to create a custom layout using ESM components
+:::
+
+:::{grid-item-card} {octicon}`table;2.5em;sd-mr-1 sd-animate-grow50` Render a `DataFrame`
+:link: esm/dataframe
+:link-type: doc
+
+How to create `JSComponent`s and `ReactComponent`s that render data in a DataFrame.
+:::
+
+::::
+
+### Examples
+
+::::{grid} 1 2 2 3
+:gutter: 1 1 1 2
+
+:::{grid-item-card} Canvas `JSComponent`
+:img-top: https://assets.holoviz.org/panel/how_to/custom_components/canvas_draw.png
+:link: examples/esm_canvas
+:link-type: doc
+
+Build a custom component to draw on an HTML canvas based on `JSComponent`.
+:::
+
+:::{grid-item-card} Leaflet.js `JSComponent`
+:img-top: https://assets.holoviz.org/panel/how_to/custom_components/leaflet.png
+:link: examples/esm_leaflet
+:link-type: doc
+
+Build a custom component wrapping leaflet.js using `JSComponent`.
+:::
+
+:::{grid-item-card} Material UI `ReactComponent`
+:img-top: https://assets.holoviz.org/panel/how_to/custom_components/material_ui.png
+:link: examples/esm_material_ui
+:link-type: doc
+
+Build custom components wrapping Material UI using `ReactComponent`.
+:::
+
+::::
+
+```{toctree}
+:titlesonly:
+:hidden:
+:maxdepth: 2
+
+esm/callbacks
+esm/custom_widgets
+esm/custom_layout
+esm/dataframe
+examples/esm_canvas
+examples/esm_leaflet
+examples/esm_material_ui
+
```
## `ReactiveHTML` Components
@@ -84,41 +192,11 @@ How to create components using `ReactiveHTML` and a DataFrame parameter
::::
-```{toctree}
-:titlesonly:
-:hidden:
-:maxdepth: 2
-
-reactive_html/reactive_html_layout
-reactive_html/reactive_html_styling
-reactive_html/reactive_html_panes
-reactive_html/reactive_html_indicators
-reactive_html/reactive_html_callbacks
-reactive_html/reactive_html_widgets
-reactive_html/reactive_html_dataframe
-```
-
-## Examples
+### Examples
::::{grid} 1 2 2 3
:gutter: 1 1 1 2
-:::{grid-item-card} Build a Plot Viewer
-:img-top: https://assets.holoviz.org/panel/how_to/custom_components/plot_viewer.png
-:link: examples/plot_viewer
-:link-type: doc
-
-Build a custom component wrapping a bokeh plot and some widgets using the `Viewer` pattern.
-:::
-
-:::{grid-item-card} Build a Table Viewer
-:img-top: https://assets.holoviz.org/panel/how_to/custom_components/table_viewer.png
-:link: examples/table_viewer
-:link-type: doc
-
-Build a custom component wrapping a table and some widgets using the `Viewer` pattern.
-:::
-
:::{grid-item-card} Build a Canvas component
:img-top: https://assets.holoviz.org/panel/how_to/custom_components/canvas_draw.png
:link: examples/canvas_draw
@@ -158,8 +236,13 @@ Build custom component wrapping a Vue.js app using `ReactiveHTML`.
:hidden:
:maxdepth: 2
-examples/plot_viewer
-examples/table_viewer
+reactive_html/reactive_html_layout
+reactive_html/reactive_html_styling
+reactive_html/reactive_html_panes
+reactive_html/reactive_html_indicators
+reactive_html/reactive_html_callbacks
+reactive_html/reactive_html_widgets
+reactive_html/reactive_html_dataframe
examples/canvas_draw
examples/leaflet
examples/material_ui
diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_callbacks.md b/doc/how_to/custom_components/reactive_html/reactive_html_callbacks.md
index 579874dede..8a637146f9 100644
--- a/doc/how_to/custom_components/reactive_html/reactive_html_callbacks.md
+++ b/doc/how_to/custom_components/reactive_html/reactive_html_callbacks.md
@@ -4,8 +4,7 @@ In this guide we will show you how to add callbacks to your `ReactiveHTML` compo
## Slideshow with Python callback
-This example shows you how to create a `SlideShow` component that uses a Python *callback*
-function to update the `SlideShow` image when its clicked:
+This example shows you how to create a `Slideshow` component that uses a Python *callback* function to update the `Slideshow` image when its clicked:
```{pyodide}
import param
@@ -24,16 +23,14 @@ class Slideshow(ReactiveHTML):
def _img_click(self, event):
self.index += 1
-Slideshow(width=500, height=200).servable()
+Slideshow().servable()
```
-This approach lets you quickly build custom HTML components with complex interactivity.
-However if you do not need any complex computations in Python you can also construct a pure JS equivalent:
+This approach lets you quickly build custom HTML components with complex interactivity. However if you do not need any complex computations in Python you can also construct a pure JS equivalent:
## Slideshow with Javascript Callback
-This example shows you how to create a `SlideShow` component that uses a Javascript *callback*
-function to update the `SlideShow` image when its clicked:
+This example shows you how to create a `SlideShow` component that uses a Javascript *callback* function to update the `SlideShow` image when its clicked:
```{pyodide}
import param
@@ -51,9 +48,7 @@ class JSSlideshow(ReactiveHTML):
_scripts = {'click': 'data.index += 1'}
-JSSlideshow(width=800, height=300).servable()
+JSSlideshow().servable()
```
-By using Javascript callbacks instead of Python callbacks you can achieve higher performance,
-components that can be *js linked* and components that will also work when your app is saved to
-static html.
+By using Javascript callbacks instead of Python callbacks you can achieve higher performance, components that can be *js linked* and components that will also work when your app is saved to static html.
diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_dataframe.md b/doc/how_to/custom_components/reactive_html/reactive_html_dataframe.md
index afbcfc701c..9ad84869e5 100644
--- a/doc/how_to/custom_components/reactive_html/reactive_html_dataframe.md
+++ b/doc/how_to/custom_components/reactive_html/reactive_html_dataframe.md
@@ -1,12 +1,10 @@
# DataFrames and ReactiveHTML
-In this guide we will show you how to implement `ReactiveHTML` components with a
-`DataFrame` parameter.
+In this guide we will show you how to implement `ReactiveHTML` components with a `DataFrame` parameter.
## Creating a Custom DataFrame Pane
-In this example we will show you how to create a custom DataFrame Pane. The example will be based on
-the [GridJS table](https://gridjs.io/).
+In this example we will show you how to create a custom DataFrame Pane. The example will be based on the [GridJS table](https://gridjs.io/).
```{pyodide}
import random
@@ -14,8 +12,10 @@ import pandas as pd
import param
import panel as pn
+from panel.custom import ReactiveHTML
+
+class GridJS(ReactiveHTML):
-class GridJS(pn.reactive.ReactiveHTML):
value = param.DataFrame()
_template = ''
@@ -24,25 +24,24 @@ class GridJS(pn.reactive.ReactiveHTML):
_scripts = {
"render": """
-console.log(data.value)
-state.config= ()=>{
- const columns = Object.keys(data.value).filter(key => key !== "index");
- var rows= []
- for (let index=0;index{
- return data.value[key][index]
- })
- rows.push(row)
- }
- return {columns: columns, data: rows, resizable: true, sort: true }
-}
-config = state.config()
-console.log(config)
-state.grid = new gridjs.Grid(config).render(wrapper);
-""", "value": """
-config = state.config()
-state.grid.updateConfig(config).forceRender()
-"""
+ console.log(data.value)
+ state.config = () => {
+ const columns = Object.keys(data.value).filter(key => key !== "index");
+ const rows = []
+ for (let index=0; index < data.value["index"].shape[0]; index++) {
+ const row = columns.map(key => data.value[key][index])
+ rows.push(row)
+ }
+ return {columns: columns, data: rows, resizable: true, sort: true}
+ }
+ const config = state.config()
+ console.log(config)
+ state.grid = new gridjs.Grid(config).render(wrapper);
+ """,
+ "value": """
+ config = state.config()
+ state.grid.updateConfig(config).forceRender()
+ """
}
__css__ = [
@@ -53,6 +52,7 @@ state.grid.updateConfig(config).forceRender()
"https://unpkg.com/gridjs/dist/gridjs.umd.js"
]
+
def data(event):
return pd.DataFrame([
["John", "john@example.com", "(353) 01 222 3333", random.uniform(0, 1)],
@@ -60,18 +60,17 @@ def data(event):
["Eoin", "eoin@gmail.com", "0097 22 654 00033", random.uniform(0, 1)],
["Sarah", "sarahcdd@gmail.com", "+322 876 1233", random.uniform(0, 1)],
["Afshin", "afshin@mail.com", "(353) 22 87 8356", random.uniform(0, 1)]
- ],
- columns= ["Name", "Email", "Phone Number", "Random"]
- )
+ ], columns= ["Name", "Email", "Phone Number", "Random"])
+
update_button = pn.widgets.Button(name="UPDATE", button_type="primary")
+
grid = GridJS(value=pn.bind(data, update_button), sizing_mode="stretch_width")
+
pn.Column(update_button, grid).servable()
```
-The main challenge of creating this component is understanding the structure of `data.value` and
-how it can be converted to a format (`config`) that `gridjs.Grid` accepts.
+The main challenge of creating this component is understanding the structure of `data.value` and how it can be converted to a format (`config`) that `gridjs.Grid` accepts.
-To help you understand what the `data.value` and `config` values looks like, I've logged them to
-the *browser console* using `console.log`.
+To help you understand what the `data.value` and `config` values looks like, I've logged them to the *browser console* using `console.log`.
![DataFrame in the console](../../../_static/reactive-html-dataframe-in-console.png)
diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_layout.md b/doc/how_to/custom_components/reactive_html/reactive_html_layout.md
index a5f518378f..cabe791a24 100644
--- a/doc/how_to/custom_components/reactive_html/reactive_html_layout.md
+++ b/doc/how_to/custom_components/reactive_html/reactive_html_layout.md
@@ -10,16 +10,19 @@ You can layout a single object as follows.
import panel as pn
import param
+from panel.custom import Child, ReactiveHTML
+
pn.extension()
-class LayoutSingleObject(pn.reactive.ReactiveHTML):
- object = param.Parameter(allow_refs=False)
+class LayoutSingleObject(ReactiveHTML):
+
+ object = Child(allow_refs=False)
_template = """
-
Temperature
-
A measurement from the sensor
-
${object}
+
Temperature
+
A measurement from the sensor
+
${object}
"""
@@ -39,18 +42,11 @@ LayoutSingleObject(
```
:::{note}
-
-Please note
-
- We define the HTML layout in the `_template` attribute.
- We can refer to the parameter `object` in the `_template` via the *template parameter* `${object}`.
- - We must give the `div` element holding the `${object}` an `id`. If we do not, then an exception
- will be raised. The `id` can be any value, for example `id="my-object"`.
-- We call our *object* parameter `object` to be consistent with our built in layouts. But the
-parameter can be called anything. For example `value`, `dial` or `temperature`.
-- We add the `border` in the `styles` parameter so that we can better see how the `_template` layes
-out inside the `ReactiveHTML` component. This can be very useful for development.
-
+ - We must give the `div` element holding the `${object}` an `id`. If we do not, then an exception will be raised. The `id` can be any value, for example `id="my-object"`.
+- We call our *object* parameter `object` to be consistent with our built in layouts. But the parameter can be called anything. For example `value`, `dial` or `temperature`.
+- We add the `border` in the `styles` parameter so that we can better see how the `_template` layes out inside the `ReactiveHTML` component. This can be very useful for development.
:::
## Layout multiple parameters
@@ -59,11 +55,13 @@ out inside the `ReactiveHTML` component. This can be very useful for development
import panel as pn
import param
+from panel.custom import Child, ReactiveHTML
+
pn.extension()
-class LayoutMultipleValues(pn.reactive.ReactiveHTML):
- object1 = param.Parameter(allow_refs=False)
- object2 = param.Parameter(allow_refs=False)
+class LayoutMultipleValues(ReactiveHTML):
+ object1 = Child()
+ object2 = Child()
_template = """
@@ -84,9 +82,7 @@ layout.servable()
You might notice that the values of `object1` and `object2` looks like they have been
rendered as markdown! That is correct.
-Before inserting the value of a parameter in the
-`_template`, Panel transforms the value using `pn.panel`. And for a string value `pn.panel` returns
-a `Markdown` pane.
+Before inserting the value of a parameter in the `_template`, Panel transforms the value using `pn.panel`. And for a string value `pn.panel` returns a `Markdown` pane.
Let's verify this.
@@ -106,32 +102,33 @@ LayoutMultipleValues(
## Layout as literal `str` values
-If you want to show the *literal* `str` value of your parameter instead of the `pn.panel` return
-value you can configure that via the `_child_config` attribute.
+If you want to show the *literal* `str` value of your parameter instead of the `pn.panel` return value you can configure that via the `_child_config` attribute.
```{pyodide}
import panel as pn
import param
+from panel.custom import ReactiveHTML
+
pn.extension()
-class LayoutLiteralValues(pn.reactive.ReactiveHTML):
- object1 = param.Parameter()
- object2 = param.Parameter()
+class LayoutLiteralValues(ReactiveHTML):
+ object1 = param.String()
+ object2 = param.String()
_child_config = {"object1": "literal", "object2": "literal"}
_template = """
-
Object 1
-
${object1}
-
Object 2
-
${object2}
+
Object 1
+
${object1}
+
Object 2
+
${object2}
-"""
+ """
layout = LayoutLiteralValues(
object1="This is the **value** of `object1`", object2="This is the **value** of `object2`",
@@ -154,10 +151,13 @@ If you want to want to layout a dynamic `List` of objects you can use a *for loo
import panel as pn
import param
+from panel.custom import Children, ReactiveHTML
+
pn.extension()
-class LayoutOfList(pn.reactive.ReactiveHTML):
- objects = param.List()
+class LayoutOfList(ReactiveHTML):
+
+ objects = Children()
_template = """
@@ -170,11 +170,10 @@ class LayoutOfList(pn.reactive.ReactiveHTML):
"""
LayoutOfList(objects=[
- "I **love** beat boxing",
- "https://upload.wikimedia.org/wikipedia/commons/d/d3/Beatboxset1_pepouni.ogg",
- "Yes I do!"],
- styles={"border": "2px solid lightgray"},
-).servable()
+ "I **love** beat boxing",
+ "https://upload.wikimedia.org/wikipedia/commons/d/d3/Beatboxset1_pepouni.ogg",
+ "Yes I do!"
+], styles={"border": "2px solid lightgray"}).servable()
```
The component will trigger a rerendering if you update the `List` value.
@@ -196,25 +195,27 @@ You can optionally
## Create a list like layout
-If you want to create a *list like* layout similar to `Column` and `Row`, you can
-combine `NamedListLike` and `ReactiveHTML`.
+If you want to create a *list like* layout similar to `Column` and `Row`, you can combine `ListLike` and `ReactiveHTML`.
```{pyodide}
import panel as pn
import param
+from panel.custom import ReactiveHTML
+from panel.layout.base import ListLike
+
pn.extension()
-class ListLikeLayout(pn.layout.base.NamedListLike, pn.reactive.ReactiveHTML):
+class ListLikeLayout(ListLike, ReactiveHTML):
objects = param.List()
_template = """
- {% for object in objects %}
-
Object {{ loop.index0 }}
-
${object}
-
- {% endfor %}
+ {% for object in objects %}
+
Object {{ loop.index0 }}
+
${object}
+
+ {% endfor %}
"""
@@ -232,7 +233,7 @@ expect.
:::{note}
-You must list `NamedListLike, ReactiveHTML` in exactly that order when you define the class! The other
+You must list `ListLike, ReactiveHTML` in exactly that order when you define the class! The other
way around `ReactiveHTML, NamedListLike` will not work.
::::
@@ -245,37 +246,35 @@ If you want to layout a dictionary, you can use a for loop on the `.items()`.
import panel as pn
import param
+from panel.custom import ReactiveHTML
+
pn.extension()
-class LayoutOfDict(pn.reactive.ReactiveHTML):
+class LayoutOfDict(ReactiveHTML):
object = param.Dict()
_template = """
- {% for key, value in object.items() %}
-
{{ loop.index0 }}. {{ key }}
-
${value}
-
- {% endfor %}
+ {% for key, value in object.items() %}
+
{{ loop.index0 }}. {{ key }}
+
${value}
+
+ {% endfor %}
-"""
+ """
LayoutOfDict(object={
"Intro": "I **love** beat boxing",
"Example": "https://upload.wikimedia.org/wikipedia/commons/d/d3/Beatboxset1_pepouni.ogg",
"*Outro*": "Yes I do!"
-},
- styles={"border": "2px solid lightgray"},
-).servable()
+}, styles={"border": "2px solid lightgray"}).servable()
```
:::{note}
Please note
-- We can insert the `key` as a literal value only using `{{ key }}`. Inserting it as a template
-variable `${key}` will not work.
-- We must not give the HTML element containing `{{ key }}` an `id`. If we do, an exception will be
-raised.
+- We can insert the `key` as a literal value only using `{{ key }}`. Inserting it as a template variable `${key}` will not work.
+- We must not give the HTML element containing `{{ key }}` an `id`. If we do, an exception will be raised.
:::
diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_panes.md b/doc/how_to/custom_components/reactive_html/reactive_html_panes.md
index b3828ddc0a..272d845bd4 100644
--- a/doc/how_to/custom_components/reactive_html/reactive_html_panes.md
+++ b/doc/how_to/custom_components/reactive_html/reactive_html_panes.md
@@ -4,32 +4,34 @@ In this guide we will show you how to efficiently implement `ReactiveHTML` panes
## Creating a ChartJS Pane
-This example will show you the basics of creating a
-[ChartJS](https://www.chartjs.org/docs/latest/) pane.
+This example will show you the basics of creating a [ChartJS](https://www.chartjs.org/docs/latest/) pane.
```{pyodide}
import panel as pn
import param
+from panel.custom import PaneBase, ReactiveHTML
+
+class ChatJSComponent(ReactiveHTML):
-class ChatJSComponent(pn.reactive.ReactiveHTML):
object = param.Dict()
- _template = (
- '
'
- )
+ _template = """
+
+ """
+
_scripts = {
- "after_layout": """
- self.object()
-""",
+ "after_layout": "if (state.chart == null) { self.object() }",
+ "remove": """
+ state.chart.destroy();
+ state.chart = null;
+ """,
"object": """
-if (state.chart){
- state.chart.destroy();
- state.chart = null;
-}
-state.chart = new Chart(canvas_el.getContext('2d'), data.object);
-""",
+ if (state.chart) { self.remove() }
+ state.chart = new Chart(canvas_el.getContext('2d'), data.object);
+ """,
}
+
__javascript__ = [
"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"
]
@@ -65,11 +67,7 @@ grid = ChatJSComponent(
pn.Column(chart_type, grid).servable()
```
-Initially I tried creating the chart in the `render` function, but the chart did not display.
-I found out via Google Search and experimentation that the chart needs to be created later in the
-`after_layout` function. If you get stuck
-share your question and minimum, reproducible code example on
-[Discourse](https://discourse.holoviz.org/).
+Note that the chart is not created inside the `after_layout` callback since ChartJS requires the layout to be fully initialized before render. Dealing with layout issues like this sometimes requires a bit of iteration, if you get stuck, share your question and minimum, reproducible code example on [Discourse](https://discourse.holoviz.org/).
## Creating a Cytoscape Pane
@@ -78,9 +76,12 @@ This example will show you how to build a more advanced [CytoscapeJS](https://js
```{pyodide}
import param
import panel as pn
-from panel.reactive import ReactiveHTML
+
+from panel.custom import ReactiveHTML
+
class Cytoscape(ReactiveHTML):
+
object = param.List()
layout = param.Selector(default="cose", objects=["breadthfirst", "circle", "concentric", "cose", "grid", "preset", "random"])
@@ -94,44 +95,38 @@ class Cytoscape(ReactiveHTML):
selected_nodes = param.List()
selected_edges = param.List()
- _template = """
-
- """
+ _template = '
'
+
__javascript__ = ['https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.23.0/cytoscape.umd.js']
_scripts = {
- 'render': """
- self.create()
- """,
- "create": """
- if (state.cy == undefined){
- state.cy = cytoscape({
- container: cy,
- layout: {name: data.layout},
- elements: data.object,
- zoom: data.zoom,
- pan: data.pan,
- });
- state.cy.on('select unselect', function (evt) {
- data.selected_nodes = state.cy.elements('node:selected').map((el)=>{return el.id()})
- data.selected_edges = state.cy.elements('edge:selected').map((el)=>{return el.id()})
- });
- self.style()
- const mainEle = document.querySelector("body")
- mainEle.addEventListener("scrollend", (event) => {state.cy.resize().fit()})
- };
+ "render": """
+ if (state.cy == undefined) {
+ state.cy = cytoscape({
+ container: cy,
+ layout: {name: data.layout},
+ elements: data.object,
+ zoom: data.zoom,
+ pan: data.pan,
+ });
+ state.cy.on('select unselect', function (evt) {
+ data.selected_nodes = state.cy.elements('node:selected').map(el => el.id())
+ data.selected_edges = state.cy.elements('edge:selected').map(el => el.id())
+ });
+ self.style()
+ const mainEle = document.querySelector("body")
+ mainEle.addEventListener("scrollend", (event) => {state.cy.resize().fit()})
+ };
""",
- 'remove': """
- state.cy.destroy()
- delete state.cy
+ "remove": """
+ state.cy.destroy()
+ delete state.cy
""",
"object": "state.cy.json({elements: data.object});state.cy.resize().fit()",
- 'layout': "state.cy.layout({name: data.layout}).run()",
+ "layout": "state.cy.layout({name: data.layout}).run()",
"zoom": "state.cy.zoom(data.zoom)",
"pan": "state.cy.pan(data.pan)",
- "style": """
-state.cy.style().resetToDefault().append(data.style).update()
-""",
+ "style": "state.cy.style().resetToDefault().append(data.style).update()",
}
_extension_name = 'cytoscape'
@@ -146,9 +141,6 @@ pn.Row(
).servable()
```
-Please notice that we `resize` and `fit` the graph on `scrollend`.
-This is a *hack* needed to make the graph show up and fit nicely to the screen.
+Please notice that we `resize` and `fit` the graph on `scrollend`. This is a *hack* needed to make the graph show up and fit nicely to the screen.
-Hacks like these are sometimes needed and requires a bit of experience to find. If you get stuck
-share your question and minimum, reproducible code example on
-[Discourse](https://discourse.holoviz.org/).
+Hacks like these are sometimes needed and requires a bit of experience to find. If you get stuck share your question and minimum, reproducible code example on [Discourse](https://discourse.holoviz.org/).
diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_styling.md b/doc/how_to/custom_components/reactive_html/reactive_html_styling.md
index 9fb8339c55..d9d0d52647 100644
--- a/doc/how_to/custom_components/reactive_html/reactive_html_styling.md
+++ b/doc/how_to/custom_components/reactive_html/reactive_html_styling.md
@@ -11,12 +11,13 @@ an HTML and CSS starting point that you can fine tune.
```{pyodide}
import param
import panel as pn
-from panel.reactive import ReactiveHTML
+
+from panel.custom import Child, ReactiveHTML
pn.extension()
class SensorLayout(ReactiveHTML):
- object = param.Parameter(allow_refs=False)
+ object = Child(allow_refs=False)
_template = """
diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_widgets.md b/doc/how_to/custom_components/reactive_html/reactive_html_widgets.md
index 527d447a6c..78e2adf9d8 100644
--- a/doc/how_to/custom_components/reactive_html/reactive_html_widgets.md
+++ b/doc/how_to/custom_components/reactive_html/reactive_html_widgets.md
@@ -10,15 +10,18 @@ This example we will show you to create an `ImageButton`.
import panel as pn
import param
-from panel.reactive import ReactiveHTML
+from panel.custom import ReactiveHTML, WidgetBase
pn.extension()
-class ImageButton(ReactiveHTML):
+class ImageButton(ReactiveHTML, WidgetBase):
clicks = param.Integer(default=0)
+
image = param.String()
+ value = param.Event()
+
_template = """\