Receive message from other tabs/windows

In JS there are several mechanisms to communicate between different browser windows such as postMessage, localstorage, broadcastchannel etc.

My question is, is there any way to communicate such info to the Python code driving a Panel app?

The scenario is that I have a web application that allows you to design a simulation model, of which the results are plotted in a Panel app. So only the data visualization is in Panel, the editor is a React thing.

Now what I want to be able to do is interactively select what to plot in the editor. So currently I just have a list of things that can be plotted in the Panel app, but this will get unwieldy in more complex designs. So I want to implement a “probe” tool in the editor that allows you to select things to plot.

One way to do it is just write the probed locations to the database along with the model itself, but that seems quite inefficient. If you could just write a few lines of custom JS to communicate between the editor and the Panel backend, that seems much nicer.

You can create a custom ReactiveHTML component for communicating between frontend and backend taking advantage of for example localstorage.

You might find some inspiration here Independent cache for each user session - #3 by Marc

It looks like this example is polling the frontend. I was hoping there would be a way to do it in an event driven way.

Maybe if the template registers an event handler that sets data.value I can link that on the Python side?

I’m just trying to do

_template = """<div>
    <script>
    var ch = new BroadcastChannel("probe")
    ch.addEventListener("message", function(msg) {
        model.value = msg.data;
    })
    </script></div>"""

But there are toooooo many levels of magic going on. Like, obviously model is not defined here so I put the code in _scripts instead but it wont just insert the script in my random JS code. Seems like they can only live in stuff like onclick parameters? Same problem with calling Python code, too much magic.

So now I’m like ok what the hell I’ll make a hidden input field and just set its value, so I give it an ID, and then PANEL CHANGES THE ID!! So much for that idea…

Okay I somehow managed:

class BroadcastChannel(pn.reactive.ReactiveHTML):
    value = param.String(default=None, allow_None=True)
    
    _template = """<div>
    <input id="hiddenmsg" type="hidden" value="${value}"></input>
    <script>
    var inp = document.currentScript.previousSibling;
    var name = "${name}".trim()
    var ch = new BroadcastChannel(name)
    ch.addEventListener("message", function(msg) {
        inp.value = msg.data;
        inp.dispatchEvent(new Event('change'));
    })
    </script>
    </div>"""

    _dom_events = {'hiddenmsg': ['change']}

Feels like half of the examples in the docs just don’t work as you’d expect but with some random trial and error this seems to do it. I have NO IDEA what kind of magic is going on here.

A less magical solution would look something like this

import panel as pn
import param

pn.extension(sizing_mode="stretch_width", template="fast")

class BroadCastChannel(pn.reactive.ReactiveHTML):
    """The Broadcast Channel API allows basic communication between browsing contexts (that is, 
    windows, tabs, frames, or iframes) and workers on the same origin.

    Web API Reference: https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
    """
    value = param.String()
    

    _template = """<div id="channel" style="display:hidden"></div>"""
    _scripts = {
        "render": """
    self.name()
""",
        "value": """
state.channel.postMessage(data.value)     
""",
        "name": """
if (state.channel){
    state.channel.close()
}
state.channel = new BroadcastChannel(data.name)
state.channel.onmessage = event => {
    data.value = event
}
    """
    }
    
channel = BroadCastChannel(name="test")
pn.Column(channel, channel.param.value).servable()

I did not manage to test and trigger onmessage. It seems it will not work when working locally ?? 1600512 - BroadcastChannel.onmessage not working (mozilla.org). How do you test?

But you don’t have to use javascript to create a broadcast channel. You might also just create a Parameterized class you store in the shared pn.state.cache.

import panel as pn
import param

pn.extension(sizing_mode="stretch_width", template="fast")

class BroadCastChannel(param.Parameterized):
    value = param.String()

    @classmethod
    def create_channel(cls, name):
        if not name in cls._channels:
            cls._channels[name]=BroadCastChannel(name=name)
        return cls._channels[name]

    @classmethod
    @property
    def _channels(cls):
        if not "channels" in pn.state.cache:
            pn.state.cache["channels"]={}
        return pn.state.cache["channels"]

channel = BroadCastChannel.create_channel("test")

pn.Param(channel).servable()
1 Like

I’ve used addEventListnener instead, which worked for me.

I’m not sure your solution is that much less magic, but that’s mostly my unfamiliarity when these scripts are executed.

Your pn.state.cache solution only works between Panel apps right? I’m trying to communicate between a Panel and pure JS app.

1 Like

Yes. The pn.state.cache only works between panel apps running on the same server.