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?

1 Like

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
6 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:

2 Likes

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

Is there a way to make the modal window blocking?
My usecase is that I want the user to make some choices in the modal before they can proceed, so I would like to keep it open until they click a “Load and close” button.

Right now the modal closes by any click outside and even if the user selects text in a FloatInput and click-drags out of the modal.

I was thinking that one way could be to intercept the closing event and then reopen the modal if it wasnt closed via the designated “Load and close” button

this idea is implemented with a html pane, but it would be better done with the reactiveHTML element, so you can validate some data in client.

the css is extracted from

close

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


css = """
.modal-body{z-index :1010;}

"""
html = pn.pane.HTML('')
btn_close = pn.widgets.Button(name = 'validate')

def update():
    html.object = """<script> 
                        let div = document.createElement("div"); 
                        console.log("as")
                    </script>"""
    html.object =  """<script>
                        close_btn =  mod = document.getElementsByClassName("pn-modal-close")[0]
                        close_btn.style = 'display:none;' 
                        mod = document.getElementsByClassName("pn-modal")[0]
                        mod.insertBefore(div, mod.nextElementSibling);
                        console.log("as1") 
                    </script>"""
    html.object =  """<script> 
                            div.style = 'z-index: 1000; position: fixed; top: 0; left: 0; width: 100%;height: 100%;'
                    </script>""" 

    html.object =  """<script> 
                        mod.style = 'z-index: 1200;' ;
                    </script>"""


pn.state.onload(update)
pn.config.raw_css.append(css)

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')
    )
)

text = pn.widgets.TextInput()

def close_modal(e):
    print (text.value)
    if text.value == 'close':
        html.object = """<script> 
                        modal.style.display = "none";
                    </script>"""
        time.sleep(0.1)
        text.value == " "
        html.object = " " 

btn_close.on_click(close_modal)

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(pn.Column(text,btn_close))
    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)

app_bootstrap.sidebar.append(html)

app_bootstrap.servable()

Thank you for your example @nghenzi.
I like your code and it almost fits the bill.
However, the modality breaks when I select the text by click-drag across the text and (accidently) out to the main area. I know, this is just clumsy control of the mouse but anyway something I could envision people doing from time to time.

Meanwhile I have a hackish enforcement of the modal by:
Start of modal:
self._modal_enforcer = pn.state.add_periodic_callback(self.main.open_modal, 300)

Called from the modal’s close button:

def _close_callback(self):
  self._modal_enforcer.stop()
  del self._modal_maintainer
  self.main.close_modal()

I think this is borderlining abuse of the periodic_callback, but I am not fluent enough in js to come up with something better…

yes, you are right. The window.onclick function defined in the template continues working If one overrides that function I think it works. only one more line in the update function is needed ( window.onclick = function(){console.log(’’)};).

def update():
    html.object = """<script> 
                        window.onclick = function(){console.log('')}; // this is the new line
                        let div = document.createElement("div"); 
                        console.log("as")
                    </script>"""
    html.object =  """<script>
                        close_btn =  mod = document.getElementsByClassName("pn-modal-close")[0]
                        close_btn.style = 'display:none;' 
                        mod = document.getElementsByClassName("pn-modal")[0]
                        mod.insertBefore(div, mod.nextElementSibling);
                        console.log("as1") 
                    </script>"""
    html.object =  """<script> 
                            div.style = 'z-index: 1000; position: fixed; top: 0; left: 0; width: 100%;height: 100%;'
                    </script>""" 

    html.object =  """<script> 
                        mod.style = 'z-index: 1200;' ;
                    </script>"""

i think the periodic function is not a good solution if you have several users. This solution can be improved with the reactiveHTML component and moving all this code inside that component.