panel icon indicating copy to clipboard operation
panel copied to clipboard

Sync widgets with URL does not respect browser url go back/forward

Open singharpit94 opened this issue 1 year ago • 1 comments

Hi,

I am trying to update my widgets with the url search query params but seems like using the pn.state.location.sync does not work when I press the go back or forward arrows in the browser url tab.

Minimal reproducer:

widget = pn.widgets.FloatSlider(name='Slider', start=0, end=10)
widget2 = pn.widgets.TextInput(name='Text')
widget3 = pn.widgets.RangeSlider(name='RangeSlider', start=0, end=10)

if pn.state.location:
    pn.state.location.sync(widget, {'value': 'slider_value'})
    pn.state.location.sync(widget2, {'value': 'text_value'})
    pn.state.location.sync(widget3, {'value': 'range_value'})

pn.Column(widget, widget2, widget3).servable()

Can you suggest some solution here or add this support?

I am using the latest panel 1.5.x version

singharpit94 avatar Oct 04 '24 06:10 singharpit94

Hi, any updates here?

singharpit94 avatar Oct 05 '24 13:10 singharpit94

I am able to reproduce this as well.

andrewfulton9 avatar Nov 05 '24 00:11 andrewfulton9

Reposting the solution here - thanks to @pierrotsmnrd for coming up with it originally:

The difficulty of this issue is that when the value of a synced widget changes, the GET parameter in the URL for the widget is changed, which triggers inevitably a new entry in the browser's navigation history. The only solution is to interact with the browser's navigation history to result in a more user-friendly solution.

Here is a solution that works, but unfortunately only available for Chrome and Edge :

import panel as pn

widget = pn.widgets.FloatSlider(name='Slider', start=0, end=10)
widget2 = pn.widgets.TextInput(name='Text')
widget3 = pn.widgets.RangeSlider(name='RangeSlider', start=0, end=10)

if pn.state.location:
    pn.state.location.sync(widget, {'value': 'slider_value'})
    pn.state.location.sync(widget2, {'value': 'text_value'})
    pn.state.location.sync(widget3, {'value': 'range_value'})

fix = pn.pane.HTML("""
<script>

const navigationEntryOnLoad = window.navigation.currentEntry;

window.navigation.addEventListener("navigate", e => {

    const isBackward = event.navigationType === 'traverse' && event.destination.index < window.navigation.currentEntry.index;

    if (isBackward) {

        // Find the index of navigationEntryOnLoad
        const indexOnLoad = window.navigation.entries().findIndex(entry => entry.id === navigationEntryOnLoad.id);

        // Check if there is a previous entry before the current one
        if (indexOnLoad > 0) {
            const firstPreviousEntry = window.navigation.entries()[indexOnLoad -1];
            window.navigation.traverseTo(firstPreviousEntry.key);
            return;
        }

    }

});

</script>
""")

pn.Column(widget, widget2, widget3, fix).servable()
# panel serve --autoreload script.py

This is not an ideal solution, and only works to go back in the navigation history - going forward results in iterating through each change in the GET parameters.

@philippjfr Not sure if there's something else we can do here, but I'd be grateful for your opinion about this if you have a minute; if this is the best solution for now, maybe we can close this out?

peytondmurray avatar Apr 04 '25 17:04 peytondmurray

Reposting a different solution from @dalthviz which should work cross-platform:

fix = pn.pane.HTML("""
<script>
window.addEventListener('popstate', function(event) {
  window.location.replace(window.location.href);
});
</script>
""")

So updating the example above you would have:

import panel as pn

widget = pn.widgets.FloatSlider(name='Slider', start=0, end=10)
widget2 = pn.widgets.TextInput(name='Text')
widget3 = pn.widgets.RangeSlider(name='RangeSlider', start=0, end=10)

if pn.state.location:
    pn.state.location.sync(widget, {'value': 'slider_value'})
    pn.state.location.sync(widget2, {'value': 'text_value'})
    pn.state.location.sync(widget3, {'value': 'range_value'})

fix = pn.pane.HTML("""
<script>
window.addEventListener('popstate', function(event) {
  window.location.replace(window.location.href);
});
</script>
""")

pn.Column(widget, widget2, widget3, fix).servable()

Checking that on Firefox I can get the following behavior:

Image

From the docs, it seems like most of the browser should support the popstate usage but probably testing the specific browser used by users that still are reporting the issue could be useful 🤔

peytondmurray avatar Jul 01 '25 16:07 peytondmurray