Can i use/create a modal/dialog in Panel?

I need to use a modal in my app.
Is there any existing solution?
if not, can you guide me on using HTML pane to do it?

Hi @ItamarShDev

How to solve this depends on what you want to do with the model.

Should it contain

  • Text
  • Markdown
  • Html
  • Panel Buttons
  • Panel layouts, panes or widgets?

I believe you would have to do one of the following.

I would try the first or second option depending on your use case.

And please add a Feature Request to Panel on Github describing your use case. That will help.

Hi @ItamarShDev

Did you find a solution? And which?

I think I will be experimenting a bit with this today as I need to learn about it too.

Regarding whether it’s in Panel 0.10 I’m not sure. I can see that there is a statement in that direction https://github.com/holoviz/panel/pull/1421#issuecomment-653582948 though.

Hi @ItamarShDev.

I’ve created an example based on a custom Panel Template.

Check it out live at https://awesome-panel.org/dialog_template

You can find the most recent code here https://github.com/MarcSkovMadsen/awesome-panel/tree/master/application/pages/dialog_template.

I include the current code below for completeness


template.html

{% extends base %}

{% block postamble %}
<!-- https://shoelace.style/ -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.19/dist/shoelace/shoelace.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.19/dist/shoelace/shoelace.esm.js"></script>
<style>
    body, .bk-root .bk, .bk-root .bk:before, .bk-root .bk:after {
        font-family: var(--sl-input-font-family);
    }
</style>
{% endblock %}

<!-- goes in body -->
{% block contents %}
{{ embed(roots.header) }}
<div id="main" style="margin: 25px">
    {{ embed(roots.main) }}
    <br/>
    <!-- https://shoelace.style/components/dialog -->
    <sl-dialog label="{{ dialog_label }}" class="dialog-overview" style="--width: 550px;--height: 500px;">
        {{ embed(roots.dialog) }}
    <sl-button slot="footer" type="primary">Close</sl-button>
    </sl-dialog>

    <sl-button>Open Dialog</sl-button>
</div>

<script>
(() => {
    const dialog = document.querySelector('.dialog-overview');
    const openButton = dialog.nextElementSibling;
    const closeButton = dialog.querySelector('sl-button[slot="footer"]');

    openButton.addEventListener('click', () => dialog.show());
    closeButton.addEventListener('click', () => dialog.hide());
})();
</script>

{% endblock %}

template.py

"""Custom Panel Template With a Dialog"""
import pathlib

import panel as pn
import param

TEMPLATE = (pathlib.Path(__file__).parent / "template.html").read_text()


class TemplateWithDialog(pn.template.Template):
    """Custom Panel Template With a Dialog"""

    dialog_open = param.Action()
    dialog_close = param.Action()

    TEMPLATE = (pathlib.Path(__file__).parent / "template.html").read_text()

    def __init__(self, header, main, dialog, dialog_label: str):
        super().__init__(
            template=TEMPLATE,
        )

        self.add_panel("header", header)
        self.add_panel("main", main)
        self.add_panel("dialog", dialog)
        self.add_variable("dialog_label", dialog_label)

app.py

"""Panel application show casing a Custom Panel Template With a Dialog"""
import hvplot.pandas  # pylint: disable=unused-import
import panel as pn
from bokeh.sampledata import sea_surface_temperature as sst

from .template import TemplateWithDialog


def _get_sea_surface_temperature_plot():
    if "dialog_template_plot" not in pn.state.cache:
        pn.state.cache["dialog_template_plot"] = sst.sea_surface_temperature.hvplot.kde().opts(
            height=300, width=500
        )
    return pn.state.cache["dialog_template_plot"]


def view():
    """Returns a Panel application show casing a Custom Panel Template With a Dialog"""
    top_panel = pn.Row(
            pn.pane.PNG(
                "https://panel.holoviz.org/_static/logo_horizontal.png",
                height=50,
                margin=10,
                link_url="https://panel.holoviz.org",
                sizing_mode="stretch_width",
            ),
            background="black",
            sizing_mode="stretch_width",
        )
    main_panel = pn.Column(
        "This is a Panel application with a dialog",
        "Provided by awesome-panel.org",
        sizing_mode="stretch_width",
    )
    dialog_panel = pn.Column(_get_sea_surface_temperature_plot(), sizing_mode="fixed")

    template = TemplateWithDialog(
        header=top_panel,
        main=main_panel,
        dialog=dialog_panel,
        dialog_label="HvPlot - Sea surface temperature kde",
    )

    return template
5 Likes

Very nice example! I’ve been wondering how to do that.

1 Like

We should learn more about custom templates @Jhsmit :+1:

With a component library like shoelace, fast, material Mwc, sap ui 5 etc you can actually create lots of awesome and modern layouts for you panel app.

Amazing! can we communicate with that example?

For example, get yes/no answer, get inputs values and such?

1 Like

Yes.

You can place any panel components inside the pn.Column of the dialog_panel. So replace the plot with markdown, checkboxes, radioboxes etc.

1 Like

[quote=“Marc, post:4, topic:1207”]

Hello Guys,
Do these do anything? Can we get feedback from shoelace when the ‘close’ button is pressed?

 dialog_open = param.Action()
 dialog_close = param.Action()

Thanks.

1 Like

I would think so. But how i actually dont know.

Thanks for sharing this. It tried using it at my end. The only trouble is that it is transparent and I see the text behind appearing on the dialog box. I know may be it can be controlled using CSS or the like, but I am not so experienced. I must add that it is happening only within the Jupyter Notebook. When served, the app is fine, just as you showed.

Thanks,
Sam

Hi @sam_panel,

The templates provided by panel have now a modal list-like attribute you can populate (as you can already do with the sidebar or the header) with items. They can be controlled from Python with .open_modal() and .close_modal(). Here is an example from the current dev docs:

import time

import panel as pn
import numpy as np
import holoviews as hv

pn.extension(sizing_mode='stretch_width')

bootstrap = pn.template.BootstrapTemplate(title='Bootstrap Template')

xs = np.linspace(0, np.pi)
freq = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2)
phase = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi)

@pn.depends(freq=freq, phase=phase)
def sine(freq, phase):
    return hv.Curve((xs, np.sin(xs*freq+phase))).opts(
        responsive=True, min_height=400)

@pn.depends(freq=freq, phase=phase)
def cosine(freq, phase):
    return hv.Curve((xs, np.cos(xs*freq+phase))).opts(
        responsive=True, min_height=400)

bootstrap.sidebar.append(freq)
bootstrap.sidebar.append(phase)

bootstrap.main.append(
    pn.Row(
        pn.Card(hv.DynamicMap(sine), title='Sine'),
        pn.Card(hv.DynamicMap(cosine), title='Cosine')
    )
)

# Callback that will be called when the About button is clicked
def about_callback(event):
    bootstrap.open_modal()
    time.sleep(10)
    bootstrap.close_modal()

# Create, link and add the button to the sidebar
btn = pn.widgets.Button(name="About")
btn.on_click(about_callback)
bootstrap.sidebar.append(btn)

# Add some content to the modal
bootstrap.modal.append("# About...")

bootstrap.servable()
3 Likes

Thanks @maximlt for the code and the pointers to the modal attribute. I am glad that things are so richly provided all within Panel.

Sam

2 Likes

Hi @maximlt

I wanted to know whether it is possible to update the content in the modal, e.g., with different buttons popping up different figures?

Here is an attempt from me, but is unsuccessful:

import panel as pn
import numpy as np
import holoviews as hv

pn.extension(sizing_mode='stretch_width')

freq = 2
phase=2*np.pi

xs = np.linspace(0, np.pi)
ys = np.tanh(xs*freq+phase)
zs = np.sin(xs*freq + phase)


fig1 = hv.Curve((xs, ys))
fig2 = hv.Curve((xs, zs))

app_bootstrap = pn.template.BootstrapTemplate(title='Bootstrap Template')
app_bootstrap.main.append(
    pn.Row(
        pn.Card(fig1, title='Hyperbolic Tangent'),
        pn.Card(fig2, title='Sine')
    )
)
# Callback for fig1 button
def fig1_callback(event):
    app_bootstrap.modal.clear()
    app_bootstrap.modal.append(fig1)
    app_bootstrap.open_modal()

# Callback for fig2 button
def fig2_callback(event):
    app_bootstrap.modal.clear()
    app_bootstrap.modal.append(fig2)
    app_bootstrap.open_modal()

# Create, link and add the button to the sidebar
btn1= pn.widgets.Button(name="Figure 1")
btn1.on_click(fig1_callback)
app_bootstrap.sidebar.append(btn1)

# Create, link and add the button to the sidebar
btn2 = pn.widgets.Button(name="Figure 2")
btn2.on_click(fig2_callback)
app_bootstrap.sidebar.append(btn2)

app_bootstrap.servable()

Thanks in advance.

Sam

Hi!

I believe the solution to your problems is in the Template docs:

These four areas behave very similarly to other Panel layout components and have list-like semantics. This means we can easily append new components into these areas. Unlike other layout components however, the contents of the areas is fixed once rendered. If you need a dynamic layout you should therefore insert a regular Panel layout component (e.g. a Column or Row) and modify it in place once added to one of the content areas.

So the trick would be here to set the modal first to contain an empty Column, and then to clear it and populate it as you want.

import panel as pn
import numpy as np
import holoviews as hv

pn.extension(sizing_mode='stretch_width')

freq = 2
phase=2*np.pi

xs = np.linspace(0, np.pi)
ys = np.tanh(xs*freq+phase)
zs = np.sin(xs*freq + phase)


fig1 = hv.Curve((xs, ys)).opts(title='fig1')
fig2 = hv.Curve((xs, zs)).opts(title='fig2')

app_bootstrap = pn.template.BootstrapTemplate(title='Bootstrap Template')
app_bootstrap.main.append(
    pn.Row(
        pn.Card(fig1, title='Hyperbolic Tangent'),
        pn.Card(fig2, title='Sine')
    )
)

# Add an empty Column
app_bootstrap.modal.append(pn.Column())

# Callback for fig1 button
def fig1_callback(event):
    # Clear and append to the Column instead to modal,
    # since one rendered it can no longer be updated.
    app_bootstrap.modal[0].clear()
    app_bootstrap.modal[0].append(fig1)
    app_bootstrap.open_modal()

# Callback for fig2 button
def fig2_callback(event):
    app_bootstrap.modal[0].clear()
    app_bootstrap.modal[0].append(fig2)
    app_bootstrap.open_modal()

# Create, link and add the button to the sidebar
btn1= pn.widgets.Button(name="Figure 1")
btn1.on_click(fig1_callback)
app_bootstrap.sidebar.append(btn1)

# Create, link and add the button to the sidebar
btn2 = pn.widgets.Button(name="Figure 2")
btn2.on_click(fig2_callback)
app_bootstrap.sidebar.append(btn2)

app_bootstrap.servable()

Let me know whether this helps you! :slight_smile:

1 Like

Thanks @maximlt , this solved my problem. Sorry for responding late.
Just a small modification to your solution, that I created a modal_content variable, initialized with pn.Column(), and then appended and cleared its content. This way everything worked well.

Thanks once again.

Sam

2 Likes