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
viewBoxis set on the SVGthe
data-nameis 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_elDOM node by name and saves it to thestateobject to easily access thecanvas_elin subsequent scripts'start','draw': Use theeventobject provided by theonmousedownandonmousemoveinline callbacks'save','clear','line_width','color': Use thedataobject to get and set the current state of thevalue