Skip to content

Commit

Permalink
Custom components User Guide reorg (#4412)
Browse files Browse the repository at this point in the history
* create how to on combining components

* create how to and background on custom components

* add section on external dependencies to background

* move custom components into new advanced section
  • Loading branch information
droumis authored Feb 8, 2023
1 parent 06ae142 commit 6f3f1bd
Show file tree
Hide file tree
Showing 7 changed files with 723 additions and 6 deletions.
3 changes: 3 additions & 0 deletions doc/background/comms/comms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Panel Communications

WIP
386 changes: 386 additions & 0 deletions doc/background/components/components_custom.md

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions doc/background/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ Beyond the [Getting Started > Core Concepts](../getting_started/core_concepts.md
Deepen your understanding about Panel's visible objects and layouts types.
:::

:::{grid-item-card} {octicon}`rows;2.5em;sd-mr-1` Custom Components
:link: components/components_custom
:link-type: doc

Learn about building custom Panel components.
:::

:::{grid-item-card} {octicon}`workflow;2.5em;sd-mr-1` APIs
:link: api/api
:link-type: doc

Explore the reasoning for each of Panel's APIs.
:::

:::{grid-item-card} {octicon}`multi-select;2.5em;sd-mr-1` Param
:::{grid-item-card} {octicon}`multi-select;2.5em;sd-mr-1` Panel and Param
:link: param/param
:link-type: doc

Discover why Panel utilizes the Param library.
Discover why and how Panel utilizes the Param library.
:::

:::{grid-item-card} {octicon}`sliders;2.5em;sd-mr-1` Widget Abbreviations for `Interact`
Expand All @@ -42,11 +49,11 @@ Learn about the abbreviations used to create widgets with Panel `interact`.
Compare Panel with similar libraries.
:::

:::{grid-item-card} {octicon}`arrow-both;2.5em;sd-mr-1` Panel in Jupyter, Standalone servers, or exported files
:link: comms
:::{grid-item-card} {octicon}`arrow-both;2.5em;sd-mr-1` Panel Communications
:link: comms/comms
:link-type: doc

An overview explaining how Panel communicates between Python and Javascript in different contexts, such as Jupyter, server environments and in exported files.
Learn how Panel communicates between Python and Javascript in different contexts.
:::

:::{grid-item-card} {octicon}`repo-template;2.5em;sd-mr-1` Templates
Expand Down
178 changes: 178 additions & 0 deletions doc/how_to/custom_components/custom_reactiveHTML.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Build Components from Scratch

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

---

As a how-to guide, the intent of this page is to provide recipes to solve a specific real-world problem without a lot of discussion. However, this is an advanced topic so if you get stuck, please read the associated [Background > Building Custom Components](../../background/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 param
from panel.reactive import ReactiveHTML
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 [Background > Building Custom Components](../../background/components/components_custom) for further explanation.
- Learn how to load external dependencies for your custom components in [Background > Building Custom Components > External Dependencies](../../background/components/components_custom#external-dependencies)
114 changes: 114 additions & 0 deletions doc/how_to/custom_components/custom_viewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Combine Existing Components

This guide addresses how to build custom components that are combinations of existing components.

The simplest way to extend Panel is to implement a so called `Viewer` component that can wrap multiple existing Panel components into an easily reusable unit that behaves like a native Panel component.

Let's create a composite `EditableRange` component made up of two `FloatInput` widgets. First, we will create the widgets:

``` {code-block} python
:emphasize-lines: 13-14
import param
import panel as pn
from panel.viewable import Viewer
pn.extension() # for notebook
class EditableRange(Viewer):
value = param.Range(doc="A numeric range.")
width = param.Integer(default=300)
def __init__(self, **params):
self._start_input = pn.widgets.FloatInput()
self._end_input = pn.widgets.FloatInput(align='end')
super().__init__(**params)
self._layout = pn.Row(self._start_input, self._end_input)
```

Then, we set up callbacks to sync the parameters on the underlying widgets with the parameters on the `Viewer` component.

``` {code-block} python
:emphasize-lines: 17, 19-29
import param
import panel as pn
from panel.viewable import Viewer
pn.extension() # for notebook
class EditableRange(Viewer):
value = param.Range(doc="A numeric range.")
width = param.Integer(default=300)
def __init__(self, **params):
self._start_input = pn.widgets.FloatInput()
self._end_input = pn.widgets.FloatInput(align='end')
super().__init__(**params)
self._layout = pn.Row(self._start_input, self._end_input)
self._sync_widgets()
@param.depends('value', 'width', watch=True)
def _sync_widgets(self):
self._start_input.name = self.name
self._start_input.value = self.value[0]
self._end_input.value = self.value[1]
self._start_input.width = self.width//2
self._end_input.width = self.width//2
@param.depends('_start_input.value', '_end_input.value', watch=True)
def _sync_params(self):
self.value = (self._start_input.value, self._end_input.value)
)
```

Finally, we'll implement the required ``__panel__`` method, which returns the Panel layout to be rendered. Panel will call this method when displaying the component.

```{pyodide}
import param
import panel as pn
from panel.viewable import Viewer
pn.extension() # for notebook
class EditableRange(Viewer):
value = param.Range(doc="A numeric range.")
width = param.Integer(default=300)
def __init__(self, **params):
self._start_input = pn.widgets.FloatInput()
self._end_input = pn.widgets.FloatInput(align='end')
super().__init__(**params)
self._layout = pn.Row(self._start_input, self._end_input)
self._sync_widgets()
def __panel__(self):
return self._layout
@param.depends('value', 'width', watch=True)
def _sync_widgets(self):
self._start_input.name = self.name
self._start_input.value = self.value[0]
self._end_input.value = self.value[1]
self._start_input.width = self.width//2
self._end_input.width = self.width//2
@param.depends('_start_input.value', '_end_input.value', watch=True)
def _sync_params(self):
self.value = (self._start_input.value, self._end_input.value)
range_widget = EditableRange(name='Range', value=(0, 10))
pn.Column(
'#### This is a custom widget',
range_widget
)
```

## Related Resources
- To create custom components from scratch, check out [How To > Build Components from Scratch](./custom_reactiveHTML.md) and read the associated [Background > Building Custom Components](../../background/components/components_custom) for further explanation.
22 changes: 22 additions & 0 deletions doc/how_to/custom_components/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Build Custom Components

Extend Panel with custom components.

::::{grid} 1 2 2 3
:gutter: 1 1 1 2

:::{grid-item-card} {octicon}`git-merge;2.5em;sd-mr-1` Combine Existing Components
:link: custom_viewer
:link-type: doc

How to build custom components that are combinations of existing components.
:::

:::{grid-item-card} {octicon}`plus-circle;2.5em;sd-mr-1` Build Components from Scratch
:link: custom_reactiveHTML
:link-type: doc

How to build custom components from scratch.
:::

::::
9 changes: 8 additions & 1 deletion doc/how_to/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# How-to Guide
# How-to Guides

The Panel How-to Guides provide step by step recipes for solving essential problems and tasks. They are more advanced than the Getting Started material and assume some knowledge of how Panel works.

Expand Down Expand Up @@ -135,6 +135,13 @@ Discover how to profile and debug your application using the admin dashboard and
Discover how to set up unit tests, UI tests and load testing to ensure your applications are (and stay) robust and scalable.
:::

:::{grid-item-card} {octicon}`plus-circle;2.5em;sd-mr-1` Build Custom Components
:link: custom_components/index
:link-type: doc

Discover how to extend Panel by building custom components.
:::

::::


Expand Down

0 comments on commit 6f3f1bd

Please sign in to comment.