Working with browser history to support back and forward buttons

I’m trying to set up a solution to deal with users clicking the back button in my app. It’s pretty frequent because many people use it on an iPad and just swipe backward, so having a back button inside the app itself isn’t doing the trick.

Right now, I’m attempting to interact with window.history via javascript using a native library, and that’s going well. I’m able to successfully call from Python to JS to pushState via call_js(), and then I’m able to successfully retrieve the history and state too. Unfortunately, though, I can’t seem to figure out how to listen for window.onpopstate so I can take action in Python to render the right form. If I could get javascript to raise_event or call a method on my main form (and pass state), I’ll be able to render the right form and data in Python.

Here are the onpopstate docs I’m using: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate

I’m admittedly not too familiar with javascript, so if I’m going about this all wrong I’m open to other ideas. Thanks for taking a look!

I think I’ve come up with a decent solution to this problem. To get it done, I load this as a native library and then use the javascript-python interop features to pass data back and forth between window.history and python:

<script>

function pushState(state) {
  window.history.pushState(state, null, "");
};

window.onpopstate = function(e) {
  // calls a python method that uses the current window.history.state to build the right form and data.
  anvil.call($('.content'), "js_onpopstate", window.history.state)
};

</script>

On the Python side, I store everything I need to know to render a content form in a dictionary called self.parent.app_state, where parent is a content_panel as used in this multi-page app recipe. Any user interaction makes updates as needed to self.parent.app_state and then raises an event called x-js-pushState which uses self.call_js() to execute the pushState() function in javascript. If I’m understanding it correctly, this is effectively adding a new entry to window.history.

Then, I assign a function to window.onpopstate that passes window.history.state to a python method called js_onpopstate(). This method then clears the content_panel and uses the information in window.history.state to build the appropriate form and data.

Feedback, ideas, and improvements are more than welcome.

2 Likes

That technique will work! One thing I’d suggest is to use a global variable in a module, rather than using self.parent to navigate the heirarchy of containers (that’s a bit brittle if you change how your form is arranged). Instead, you can have a module with a global variable and then reference that global variable explicitly from everywhere.

3 Likes

Great suggestion, thanks! I was so focused on getting the history to work that using a global variable for state didn’t even occur to me. Is it safe to store that variable in a client module?

Knowing what I know now, I completely understand why back & forward button functionality is not built in to Anvil - it’d be ridiculously complex to support it out of the box for all apps. Storing session state in window.history.state is a great workaround though - with that you can detect back/forward events and respond accordingly. As an added bonus, I was also able to link in-app nav buttons to the browser history by calling a js method for window.history.back() or window.history.forward(). Pretty cool.

I’m going to edit the thread name to make it easier for others to find. “Accessing javascript listeners from app” isn’t a good description - I was really asking about browser history support. (@meredydd - Feel free to change it back or edit it to something else.)

4 Likes

@kevin Are you able to share a demo app with this navigation functionality?

Thank you,

Jeff

Hi @jeff - sorry for the slow reply. Unfortunately the functionality is baked pretty deeply into my core product, and I can’t easily share that as a demo for security reasons. Here are the key points though:

First, my Native Libraries code contains this <script> tag:

<script>
function pushFormState(state) {
  window.history.pushState(state, null, "")
}
function getFormState() {
  return window.history.state
}
window.onpopstate = function(e) {
  anvil.call($('.content'), "js_onpopstate", window.history.state)
}
</script>

From there, I took @meredydd’s advice and have a Client Module called _BrowserHistory_client that stores a dictionary called app_history_state.

When a form loads, it executes a method that sets the _BrowserHistory_client.app_history_state dictionary and then passes it to the browser history via the pushFormState js function. The app_history_state dictionary includes everything I need to know to rebuild the current state - typically that means the name of the form and any session-specific variables. Anvil has a dedicated method that makes all this possible by allowing Python to interact with javascript functions (and the docs are great): .call_js().

Then when the onpopstate event occurs the process runs in reverse - it passes the browser history state to a Python function called js_onpopstate in the Main Form which interprets the app_history_state dictionary, clears the content panel, and loads the right form.

I won’t lie to you - it’s pretty tricky to implement the first time and will need to be customized to your specific app. But the good news is once you get it working, it’ll sort of “click” and feel much easier going forward. I’m sorry it’s not possible for me to build a working demo outside of my own application - I hope this info helps. I’m also happy to help you troubleshoot your app if you get stuck. Best of luck!

3 Likes

Turns out I was wrong - this was a lot easier to demo than I’d thought. Here’s an example app that supports browser nav from form to form. It has some minor differences from what I described above, but the basic principles are exactly the same.

https://anvil.works/build#clone:OX5QE6DBUGRZR67R=N5JDPZT6K2UIIMDK6HFYIXCE

5 Likes

This was such a great demo - Thanks so much for sharing!

It took me a while to get my head around as javascript is a little foreign to me.
I had a play with the demo and tried adding some URL hashing which seemed very minimal additional code. Basically just an extra parameter in the ‘push_form_state’ function call and opening a ContentForm by the hash value.

I think it plays quite nicely with the demo you shared.

https://anvil.works/build#clone:EGXZCOBTHISCZGMR=WQDHI346FVBPTYGM6GZLLHNG

edit:
as @kevin mentions, you’ll need to use the share button to see the url hash working - it doesn’t work in the IDE. Here’s a link to a published version to see it working in practice https://witty-flawless-talk.anvil.app

4 Likes

This is genius - thanks a lot for building on the demo! I didn’t even realize that was possible - javascript is completely foreign to me too. I’d been waiting for a set url hash function in Anvil, and this takes care of that missing feature for me.

Now I’ll be able to support users who want to copy/paste urls in addition to the back/forward nav. It’s basically real routing at that point. Nice work!

Edit: One note for other users… you may need to use the Share App link in a new tab to see the URL hash feature that @stucork added. It doesn’t work for me inside the IDE in Firefox.

1 Like

Thanks again, @stucork - I was already able to add this feature to my app. It’s an elegant solution, and we now support deep linking. :slight_smile:

1 Like