Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ReactiveHTML docs #5448

Merged
merged 29 commits into from Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
391 changes: 0 additions & 391 deletions doc/explanation/components/components_custom.md

This file was deleted.

717 changes: 717 additions & 0 deletions doc/explanation/components/reactive_html_components.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions doc/explanation/index.md
Expand Up @@ -23,18 +23,18 @@ Learn the pros and cons of Panel's different APIs.
::::{grid} 1 2 2 3
:gutter: 1 1 1 2

:::{grid-item-card} {octicon}`rows;2.5em;sd-mr-1 sd-animate-grow50` Components overview
:::{grid-item-card} {octicon}`rows;2.5em;sd-mr-1 sd-animate-grow50` Built in components
:link: components/components_overview
:link-type: doc

Deepen your understanding about Panel's visible objects and layout types.
Deepen your understanding about Panel's built in components.
:::

:::{grid-item-card} {octicon}`plus-circle;2.5em;sd-mr-1 sd-animate-grow50` Custom components
:link: components/components_custom
:::{grid-item-card} {octicon}`plus-circle;2.5em;sd-mr-1 sd-animate-grow50` ReactiveHTML components
:link: components/reactive_html_components
:link-type: doc

Deepen your understanding about building custom Panel components.
Deepen your understanding about custom `ReactiveHTML` components
:::

::::
Expand Down
179 changes: 1 addition & 178 deletions doc/how_to/custom_components/custom_reactiveHTML.md
@@ -1,182 +1,5 @@
# Build Components from Scratch

This guide addresses how to build custom Panel components from scratch.

```{admonition} Prerequisites
1. As a how-to guide, the intent is to provide recipes for specific problems without a lot of discussion. However, this is an advanced topic so if you get stuck, please read the associated [Explanation > Building Custom Components](../../explanation/components/components_custom) for further explanation.
```

---

The `ReactiveHTML` class provides bi-directional syncing of arbitrary HTML attributes and DOM properties with parameters on the subclass. The key part of the subclass is the `_template` variable. This is the HTML template that gets rendered and declares how to link parameters on the class to HTML attributes.

## Callback Example

Let's declare a `Slideshow` component which subscribes to `click` events on an `<img>` element and advances the image `index` on each click:

```{pyodide}
import panel as pn
import param

from panel.reactive import ReactiveHTML

pn.extension()

class Slideshow(ReactiveHTML):

index = param.Integer(default=0)

_template = '<img id="slideshow" src="https://picsum.photos/800/300?image=${index}" onclick="${_img_click}"></img>'

def _img_click(self, event):
self.index += 1

print('run the code block above, then click on the image below')

Slideshow(width=500, height=200)
```

As we can see this approach lets us quickly build custom HTML components with complex interactivity. However if we do not need any complex computations in Python we can also construct a pure JS equivalent:

```{pyodide}
class JSSlideshow(ReactiveHTML):

index = param.Integer(default=0)

_template = """<img id="slideshow" src="https://picsum.photos/800/300?image=${index}" onclick="${script('click')}"></img>"""

_scripts = {'click': 'data.index += 1'}

JSSlideshow(width=800, height=300)
```

## Child Template Example

If we want to provide a template for the children of an HTML node we have to use Jinja2 syntax to loop over the parameter. The component will insert the loop variable `option` into each of the tags:

```{pyodide}
class Select(ReactiveHTML):

options = param.List(doc="Options to choose from.")

value = param.String(doc="Current selected option")

_template = """
<select id="select" value="${value}" style="width: ${model.width}px">
{% for option in options %}
<option id="option">${option}</option>
{% endfor %}
</select>
"""

_dom_events = {'select': ['change']}

select = Select(options=['A', 'B', 'C'])
select
```

The loop body can declare any number of HTML tags to add for each child object, e.g. to add labels or icons, however the child object (like the `{{option}}` or `${option}`) must always be wrapped by an HTML element (e.g. `<option>`) which must declare an `id`. Depending on your use case you can wrap each child in any HTML element you require, allowing complex nested components to be declared. Note that the example above inserted the `options` as child objects but since they are strings we could use literals instead:

```html
<select id="select" value="${value}" style="width: ${model.width}px">
{% for option in options %}
<option id="option-{{ loop.index0 }}">{{ option }}</option>
{% endfor %}
</select>
```

When using child literals we have to ensure that each `<option>` DOM node has a unique ID manually by inserting the `loop.index0` value (which would otherwise be added automatically).

## Javascript Events Example

Next we will build a more complex example using pure Javascript events to draw on a canvas with configurable line width, color and the ability to clear and save the resulting drawing.

```{pyodide}
import panel as pn

class Canvas(ReactiveHTML):

color = param.Color(default='#000000')

line_width = param.Number(default=1, bounds=(0.1, 10))

uri = param.String()

_template = """
<canvas
id="canvas"
style="border: 1px solid;"
width="${model.width}"
height="${model.height}"
onmousedown="${script('start')}"
onmousemove="${script('draw')}"
onmouseup="${script('end')}"
>
</canvas>
<button id="clear" onclick='${script("clear")}'>Clear</button>
<button id="save" onclick='${script("save")}'>Save</button>
"""

_scripts = {
'render': """
state.ctx = canvas.getContext("2d")
""",
'start': """
state.start = event
state.ctx.beginPath()
state.ctx.moveTo(state.start.offsetX, state.start.offsetY)
""",
'draw': """
if (state.start == null)
return
state.ctx.lineTo(event.offsetX, event.offsetY)
state.ctx.stroke()
""",
'end': """
delete state.start
""",
'clear': """
state.ctx.clearRect(0, 0, canvas.width, canvas.height);
""",
'save': """
data.uri = canvas.toDataURL();
""",
'line_width': """
state.ctx.lineWidth = data.line_width;
""",
'color': """
state.ctx.strokeStyle = data.color;
"""
}

canvas = Canvas(width=300, height=300)

# We create a separate HTML element which syncs with the uri parameter of the Canvas
png_view = pn.pane.HTML()
canvas.jslink(png_view, code={'uri': "target.text = `<img src='${source.uri}'></img>`"})

pn.Column(
'# Drag on canvas to draw\n To export the drawing to a png click save.',
pn.Row(
canvas.controls(['color', 'line_width']),
canvas,
png_view
)
)
```

This example leverages all three ways a script is invoked:

1. `'render'` is called on initialization
2. `'start'`, `'draw'` and `'end'` are explicitly invoked using the `${script(...)}` syntax in inline callbacks
3. `'line_width'` and `'color'` are invoked when the parameters change (i.e. when a widget is updated)

It also makes extensive use of the available objects in the namespace:

- `'render'`: Uses the `state` object to easily access the canvas rendering context in subsequent callbacks and accesses the `canvas` DOM node by name.
- `'start'`, `'draw'`: Use the `event` object provided by the `onmousedown` and `onmousemove` inline callbacks
- `'save'`, `'line_width'`, `'color'`: Use the `data` object to get and set the current state of the parameter values


## Related Resources

- Read the associated [Explanation > Building Custom Components](../../explanation/components/components_custom) for further explanation, including how to load external dependencies for your custom components.
38 changes: 3 additions & 35 deletions doc/how_to/custom_components/index.md
Expand Up @@ -16,7 +16,7 @@ How to build custom components that are combinations of existing components.
:link: custom_reactiveHTML
:link-type: doc

How to build custom components from scratch.
How to build custom components with HTML, CSS, Javascript and `ReactiveHTML` and no Javascript tooling.
:::

::::
Expand All @@ -26,52 +26,20 @@ How to build custom components from scratch.
::::{grid} 1 2 2 3
:gutter: 1 1 1 2

:::{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
:link-type: doc

Build a custom component to draw on an HTML canvas based on `ReactiveHTML`.
:::

:::{grid-item-card} Wrap Leaflet.js
:img-top: https://assets.holoviz.org/panel/how_to/custom_components/leaflet.png
:link: examples/leaflet
:link-type: doc

Build a custom component wrapping leaflet.js using `ReactiveHTML`.
:::

:::{grid-item-card} Wrap Material UI
:img-top: https://assets.holoviz.org/panel/how_to/custom_components/material_ui.png
:link: examples/material_ui
:link-type: doc

Build custom components wrapping material UI using `ReactiveHTML`.
:::

:::{grid-item-card} Wrap a Vue.js component
:img-top: https://assets.holoviz.org/panel/how_to/custom_components/vue.png
:link: examples/vue
:link-type: doc

Build custom component wrapping a Vue.js app using `ReactiveHTML`.
:::

:::{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 custom component wrapping a bokeh plot and some widgets using the `Viewer` pattern.
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 custom component wrapping a table and some widgets using the `Viewer` pattern.
Build a custom component wrapping a table and some widgets using the `Viewer` pattern.
:::

::::
Expand Down