Persist state of form when navigating away from it

What I’m trying to do:
Create a way for my custom Calendar component to persist the last Year Month it was on when returning to the Calendar page. More generally, trying to find a strategy of persisting state when navigating away from a form.

morefun

What I’m thinking:
The obvious thing to do is when clicking on the Calendar button load in the last saved state from DataTables, and then pass it into the constructor for the Calendar object, and somehow inject a method capable of saving the state of the calendar, into the constructor as well. Something that looks like:

  def button_calendar_click(self, **event_args):
    self.content_panel.clear()
    self.content_panel.add_component(CalendarContainer(persistMethodHere))

This seems kinda hacky, and not very generalizable…

Clone link for app using custom calendar component:
https://anvil.works/build#clone:C2M52L5A7Y574TKK=B47UG4KVIRGBIXY7C57ZW2RB

Clone link where custom calendar component is defined
https://anvil.works/build#clone:2YTMQUCIDGNA2IRB=MDMPBUZGUV37OUW2I35QNITS

Any suggestions are appreciated.

1 Like

I might be misunderstanding (not having looked at your clones), but could you use a “global” variable using the method suggested in the docs? https://anvil.works/docs/client/python/modules#global-variables

Calling the CalendarContainer each time is the problem here.
Really you want to cache that Form so that you always load the same form… this way it persists across navigation changes.

A simple fix is to define attributes.

class Startup(StartupTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    self.calendar_form = None
    self.main_form = None

  def button_calendar_click(self, **event_args):
    self.calendar_form = self.calendar_form or CalendarContainer()
    self.content_panel.clear()
    self.content_panel.add_component(self.calendar_form)

This way you only create a single instance of the CalendarContainer and next time the button is clicked it loads the cached form.

You can adjust the code be more DRY, and even take advantage of functools.cache

e.g.

import functools
from ..MainForm import MainForm
from ..CalendarContainer import CalendarContainer

class Startup(StartupTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    self.tag_to_form = {"main": MainForm, "calendar": CalendarContainer}
    self.button_cal.tag = "calendar"
    self.button_main.tag = "main"
  
  @functools.cache
  def get_form(self, tag):
    """return the cached form or creates a new one if not in the cache"""
    return self.tag_to_form[tag]()

  def nav_button_click(self, sender, **event_args):
    form = self.get_form(sender.tag)
    self.content_panel.clear()
    self.content_panel.add_component(form)

Another option is to use the hash routing module.
This caches the forms so you don’t have to.

from anvil_extras import routing
from ..MainForm import MainForm
from ..CalendarContainer import CalendarContainer

@routing.main_router
class Startup(StartupTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    self.button_cal.tag = "calendar"
    self.button_main.tag = ""

  def nav_button_click(self, sender, **event_args):
    # loads a cached form or creates a new one based on the url_hash
    routing.set_url_hash(sender.tag)

---

# In the calendar form
@routing.route("calendar")
class CalendarContainer():
    ...

---

# In the MainForm
@routing.route("")
class MainForm():
    ...

And then there’s the anvil_extras navigation module which is worth exploring to help with navigation techniques.

5 Likes

Thanks for your detailed response. I’ll be researching the hash routing for now.

I didn’t think of that, but in general, I try and stay away from global vars.

When I want to use the values used the last time the form was used as default, whether in this session on in the previous session, I do:

from anvil_extras.storage import local_storage

    def form_show(self, **event_args):
        year = local_storage.get('date picker year', 1999)

    def form_hide(self, **event_args):
        local_storage['date picker year'] = year

In my apps I rarely use the form_hide, I usually use the event that happens immediately after the value changes, this is just a quick example.


All my apps use the hash routing module. Often, when the value of a text box changes, I put all the values of the text boxes and other input components in url_dict, then I call:

    routing.set_url_hash(url_pattern='samepage', url_dict=url_dict, load_from_cache=False)

so not only is the current page updated, it is also possible to go back and forth in the browser history and it is possible to create a shortcut that includes the input as is at any point in time.

2 Likes

I didn’t know @functools.cache was available.

I checked the documentation and it says “New in version 3.9”.

So… is functools really available?
Or is this your muscle memory talking?

as the person who implemented functools in skulpt I can confirm it’s available :wink:
But it’s only available client side.
client side remains 3.7(ish) and the ish means you’ll occasionally get a 3.9 feature springled in.
(type hinting generics anyone?)

2 Likes

I have used type hinting on modules that are imported both on client and server side, and I was pleasantly surprised that it worked!

Thank you!

yes but on the client you can also do…
dict[str, list[int, ...]]

PEP 585 – Type Hinting Generics In Standard Collections | peps.python.org


(although you can also do that on the server with from __future__ import annotations)
and if you wanted it in a module that was used on both client and server, you could wrap the import in a try except.

we digress.

2 Likes

I ended up doing the first method using attributes. Simple to understand, works.

Thank you!

1 Like