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.
- Mess with
pn.pane.HTML
using how to css modals or similar. - Create a custom panel template using how to css modals or similar.
- Implement a custom Bokeh Extension.
- Wait for the new templates in Panel 0.10. I believe they support modals. But I am not sure.
- Wait for a modal to be added to the awesome-panel-extensions package. I have it on the radar but has not done it yet.
- For example I hope to add the MWC dialog one day.
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
Very nice example! Iāve been wondering how to do that.
We should learn more about custom templates @Jhsmit
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?
Yes.
You can place any panel components inside the pn.Column of the dialog_panel. So replace the plot with markdown, checkboxes, radioboxes etc.
[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.
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()
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
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
orRow
) 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!
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
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
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.