Create Custom Widgets using ESM Components#

In this guide we will show you how to efficiently implement custom widgets using JSComponent, ReactComponent and AnyWidgetComponent to get input from the user.

Image Button#

This example we will show you to create an ImageButton.

import panel as pn
import param

from panel.custom import JSComponent
from panel.widgets import WidgetBase

pn.extension()

class ImageButton(JSComponent, WidgetBase):

    clicks = param.Integer(default=0)
    image = param.String()
    value = param.Event()

    _esm = """
export function render({ model }) {
    const button = document.createElement('button');
    button.id = 'button';
    button.className = 'pn-container center-content';

    const img = document.createElement('img');
    img.id = 'image';
    img.className = 'image-size';
    img.src = model.image;

    button.appendChild(img);

    button.addEventListener('click', () => {
        model.clicks += 1;
    });
    return button
}
"""

    _stylesheets = ["""
.pn-container {
    height: 100%;
    width: 100%;
}
.center-content {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1em;
}
.image-size {
    width: 100%;
    max-height: 100%;
    object-fit: contain;
}
"""]

    @param.depends('clicks')
    def _trigger_value(self):
       self.param.trigger('value')

button = ImageButton(
    image="https://panel.holoviz.org/_static/logo_stacked.png",
    styles={"border": "2px solid lightgray"},
    width=400, height=200
)
pn.Column(button, button.param.clicks,).servable()
import panel as pn
import param

from panel.custom import ReactComponent
from panel.widgets import WidgetBase

pn.extension()

class ImageButton(ReactComponent, WidgetBase):

    clicks = param.Integer(default=0)
    image = param.String()
    value = param.Event()

    _esm = """
export function render({ model }) {
    const [clicks, setClicks] = model.useState("clicks");
    const [image] = model.useState("image");

    return (
    <button onClick={e => setClicks(clicks+1)} className="pn-container center-content">
        <img src={image} className="image-size" src={ image }/>
    </button>
    )
}
"""

    _stylesheets = ["""
.pn-container {
    height: 100%;
    width: 100%;
}
.center-content {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1em;
}
.image-size {
    width: 100%;
    max-height: 100%;
    object-fit: contain;
}
"""]

    @param.depends('clicks')
    def _trigger_value(self):
       self.param.trigger('value')

button = ImageButton(
    image="https://panel.holoviz.org/_static/logo_stacked.png",
    styles={"border": "2px solid lightgray"},
    width=400
)
pn.Column(button, button.param.clicks).servable()
import panel as pn
import param

from panel.custom import AnyWidgetComponent
from panel.widgets import WidgetBase

pn.extension()

class ImageButton(AnyWidgetComponent, WidgetBase):

    clicks = param.Integer(default=0)
    image = param.String()
    value = param.Event()

    _esm = """
function render({ model, el }) {
    const button = document.createElement('button');
    button.id = 'button';
    button.className = 'pn-container center-content';

    const img = document.createElement('img');
    img.id = 'image';
    img.className = 'image-size';
    img.src = model.get("image");

    button.appendChild(img);

    button.addEventListener('click', () => {
        model.set("clicks", model.get("clicks")+1);
        model.save_changes();
    });
    el.appendChild(button);
}
export default { render }
"""

    _stylesheets = ["""
.pn-container {
    height: 100%;
    width: 100%;
}
.center-content {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1em;
}
.image-size {
    width: 100%;
    max-height: 100%;
    object-fit: contain;
}
"""]

    @param.depends('clicks')
    def _trigger_value(self):
       self.param.trigger('value')

button = ImageButton(
    image="https://panel.holoviz.org/_static/logo_stacked.png",
    styles={"border": "2px solid lightgray"},
    width=400, height=200
)

pn.Column(button, button.param.clicks).servable()

If you don’t want the button styling, you can change the <button> tag to a <div> tag.

The ImageButton now works as any other widget. Lets try the .from_param method to create an ImageButton from a `Parameter:

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, image="https://panel.holoviz.org/_static/logo_stacked.png",)
pn.Column(button2, my_instance.param.clicks).servable()

When you click the image button you should see the number of clicks increase.