Widgets with ReactiveHTML#
In this guide we will show you how to efficiently implement custom widgets using ReactiveHTML
to get input from the user.
SVG Input#
This example will show you have to turn a SVG image into a clickable and hoverable input widget.
This can for example be used to make a technical drawing interactive.
import panel as pn
import param
from panel.custom import ReactiveHTML, WidgetBase
pn.extension()
class SVGInput(ReactiveHTML, WidgetBase):
value = param.String(default="")
clicks = param.Integer()
click = param.String()
hover = param.String()
_child_config = {"value": "literal"}
_template = """\
<div id="container" class="pn-container"
onclick="${script('click_handler')}" onmouseover="${script('mouseover_handler')}"
>
{{ value }}
</div>
"""
_stylesheets = ["""
.pn-container { height: 100%; width: 100%; position:relative;}
.pn-container svg {position: relative; height:100%; width:100%}
"""]
_scripts = {
"click_handler": """
const name = state.event.target.getAttribute("data-name")
if (name != null) {
data.click = name
data.clicks += 1
}
""",
"mouseover_handler": """
const name = state.event.target.getAttribute("data-name")
data.hover = name || ""
"""
}
SVG = """
<svg viewBox="0 35 300 300">
<rect data-name="foot" x="130" y="270" width="40" height="30" fill="brown"/>
<polygon data-name="bottom" points="150,150 230,270 70,270" fill="#234236"/>
<polygon data-name="middle" points="150,110 210,210 90,210" fill="#0C5C4C"/>
<polygon data-name="top" points="150,70 190,150 110,150" fill="#38755B"/>
</svg>
"""
button = SVGInput(
value=SVG,
styles={"border": "2px solid lightgray"},
height=400, sizing_mode="stretch_width", max_width=1000
)
pn.Column(button, button.param.clicks, button.param.click, button.param.hover).servable()
If you want to use your own SVG value
, you must make sure that
the
viewBox
is set on the SVGthe
data-name
is set on the SVG child elements, that you want to make clickable and hoverable.
In this example its tempting to call the onclick
event handler click
instead of click_handler
.
But if you do, the clicks
value will be incremented twice because a parameter called click
also exists and when its value changes, the associated click
script will be executed!
Select Widget#
Lets see how to create a basic Select
widget.
import panel as pn
import param
from panel.custom import ReactiveHTML, WidgetBase
pn.extension()
class Select(ReactiveHTML, WidgetBase):
options = param.List(doc="Options to choose from.")
value = param.String(default="", doc="Current selected option")
_template = """
<select id="select_el" class="pn-container style" value="${value}">
{% for option in options %}
<option id="option_el">{{option}}</option>
{% endfor %}
</select>
"""
stylesheets=["""
.pn-container {height: 100%;width: 100%;position:relative;}
.style {border: 2px dashed lightgray;border-radius:20px}
"""]
_dom_events = {'select_el': ['change']}
select = Select(
value="B",
options=['A', 'B', 'C'], height=50, width=300,
)
pn.Column(select, select.param.value).servable()
Note how we used a {% for … %}` loop to loop over the options.
In this example we inserted the options as literal str
values via {{option}}
.
Note that the example above inserted the options
as child objects but since they are strings we could use literals instead:
<select id="select_el" class="pn-container style" value="${value}" >
{% for option in options %}
<option id="option_el">{{ option }}</option>
{% endfor %}
</select>
Drawable Canvas#
Next we will build a more complex widget to draw on a canvas with a configurable line width, color and the ability to clear and save the resulting drawing.
import panel as pn
import param
from panel.custom import ReactiveHTML, WidgetBase
pn.extension()
class Canvas(ReactiveHTML, WidgetBase):
value = param.String(default="")
color = param.Color(default="#000000")
line_width = param.Number(default=1, bounds=(0.1, 10))
save = param.Event()
clear = param.Event()
_template = """
<canvas
id="canvas_el" style="border: 1px solid;"
width="${model.width}" height="${model.height}"
onmousedown="${script('start')}" onmousemove="${script('draw')}" onmouseup="${script('end')}"
></canvas>
"""
_scripts = {
"render": "state.ctx = canvas_el.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",
"save": "data.value = canvas_el.toDataURL()",
"clear": """
state.ctx.clearRect(0, 0, canvas_el.width, canvas_el.height)
data.value = ""
""",
"line_width": "state.ctx.lineWidth = data.line_width",
"color": "state.ctx.strokeStyle = data.color",
}
canvas = Canvas(width=300, height=300,)
def png_element(value):
if not value:
return "<p style='padding:10px;'>Click <em>Save</em> to show the image here.<p>"
return f"<img src='{value}'></img>"
png_view = pn.pane.HTML(
pn.bind(png_element, canvas),
width=canvas.width,
height=canvas.height+2,
margin=(0, 10),
styles={"border": "1px solid black"},
)
pn.Column(
"# Drag on the left canvas to draw\n To export the drawing to a `png` image click *Save*.",
pn.Row(
pn.Param(
canvas.param,
parameters=['color', 'line_width', 'save', 'clear'],
show_name=False
),
canvas,
png_view,
),
).servable()
This example invokes scripts in 3 ways:
'render'
is called on initialization'start'
,'draw'
and'end'
are explicitly invoked using the${script(...)}
syntax in inline callbacks'line_width'
,'color'
,'save'
and'clear'
are invoked when the parameters change
It also makes extensive use of the available objects in the namespace:
'render'
: Accesses thecanvas_el
DOM node by name and saves it to thestate
object to easily access thecanvas_el
in subsequent scripts'start'
,'draw'
: Use theevent
object provided by theonmousedown
andonmousemove
inline callbacks'save'
,'clear'
,'line_width'
,'color'
: Use thedata
object to get and set the current state of thevalue