Summary
Question number 1 : Can you please elaborate on how to define self.url_patterns
?
Some context:
The first time main router loads it will do the following.
- run
__init__
- check the url hash (convert this to a url_pattern + url_dict e.g. #articles?id=3 → ‘article’, {‘id’,‘3’)
- of course, the first url hash from the user will probably just be an empty string but maybe not if they always end up at #dashbard or whatever.
- The main_router then loops through the list
self.url_patterns
- It checks each pattern to see whether the current url_hash has the correct string pattern and all the correct keys from the url_dict.
- if there is no match then it will load your error form (if you have declared one in the
main_router
’s init
method. Else it will just raise an exception.
- if there is a match then it will load the form you told it to load having declared this in
self.url_patterns
.
To declare self.url_patterns
use the router.Path
class which you get when you do from Hash_Routing import routing
self.url_patterns = [routing.Path(Home, pattern=''),
routing.Path(ListBlogPostsForm, pattern='blogposts'),
routing.Path(BlogPostForm, pattern='blog_post', keys=['id']),
routing.Path(ListArticlesForm, pattern='articles'),
routing.Path(ArticleForm, pattern='article', keys=['id']),
]
NB: You will need to have imported all these forms earlier e.g.
from .BlogPostForm import BlogPostForm
Path takes the arguments
"""
form: Anvil Form to load,
pattern: string pattern to match from url hash
keys: list of keys (strings) to match from the url dict
properties: dict - extra stuff that you normally pass to that particular form. Example the user e.g. properties = {'user': user}
"""
To declare an error form do this in the main_router
init
function like:
self.error_form = Error_Form
I don’t see any on_history_back
/ on_history_forward
event.
Question number 2 : How does it work / what happens when the user clicks on the back / forward button?
This is in the JS Native libraries
.
There are three JS functions that do the work.
pushFormState - //appends the state to the window history
replaceState - //replaces the last window history state with the current state
onpopstate - //when there is a navigation change or a url_hash change then this js function
//is called automatically - it then sends the call to the main_router method js_onpopstate
so on history forward and back or on a url change the js_onpopstate function is called…
There are two options -
either there is some state information that we stored in the window’s history when we originally loaded the form - e.g. the item property.
NB: If there is history state information then js_onpopstate was because of back/forward navigation
or
There is no history state information when js_onpopstate
is called because this was a url_hash change (the result of set_url_hash being called - or manually typing a url in the addressbar) i.e. it’s new to the window history… and so no state information.
Both situations result in something similar:
- main_router loops through self.url_patterns to match the url_pattern again
- The correct form will be loaded
- if it was from back_forward navigation additional properties will be passed to the form that were stored in the window history state information. (like the item)
@routing.route('blog_post', keys=['id'], on_update_form=BlogPostForm.get_blog_post)
Question number 3: Can you add an on_update_form parameter that expects a caller?
I think it’s possible for sure.
some more context:
if self.item
is a dictionary (and can be converted to json) then it should just work. This dictionary will be stored in the history state so on back/forward navigation it should load with the form no issues.
The problem with a live table row is that you can’t store that in the window history because the history doesn’t know how to store this type of object. (That’s why Hash_Routing converts a live_table_row to its row_id when it’s stored to the history state)
Note that you should always aim to be able to get the properties you need for that form only by the url_hash/url_dict. This is because your user might navigate to that page as a new url by manually changing address bar.
Example:
You can see this in the gif above when I manually change the id.
My form loads with no item property just the url_dict and so I need to retrieve this article by id using a server call.
The code was:
if not self.item:
try:
self.item = anvil.server.call('get_blog_post_by_id',self.url_dict['id'])
except:
raise Exception(f'no blogpost with id {self.url_dict["id"]}')
If you have an example where this breaks let me know and I’ll try and help/add the functionality.
Since I was thinking about forms as loading with a self.item
property I didn’t see a reason to add an extra function call to distinguish between different types of navigation… Either my forms has a self.item property or it doesn’t so retrieve it… and all this could happen in the init
function.
Question number 4: What is the difference between push and replace? And do I care?
Not really. Hopefully this is taken care of for you but there are some instances where it’s useful to know the distinction.
push appends to window history
replace replaces in window history
push is the default for link clicks or whenever you just load the form by doing
content_panel.clear()
content_panel.add_component(MyForm()) # the form will load and be appended to history
I have found it useful is when, say, you are on a page that the user can delete then you would want to load a different form and set replace_state=True
to improve the navigation experience. Otherwise, they’d probably get an error from navigating back to the deleted page
def trash_button_clicked(self, **event_args):
from ..DashboardForm import DashboardForm
self.item.delete()
get_open_form().content_panel.clear()
get_open_form().content_panel.add_component(DashboardForm(replace_state=True))
I also did this above with the login form because I didn’t want the login form to be part of the history
so I loaded the dashboard with replace_state=True
I also found it useful to load a form with set_history=False
if I was loading a form that was a route form but in this situation I didn’t want it to be part of the history…
More aggressive would be to use route=False
which will just ignore all the behaviour that would usually come with a route form
. (useful if say you want a form to behave like a component sometimes and a route form other times - say you load the form in an alert)
def button_click(self, **event_args):
from ..ExampleForm import ExampleForm
alert(ExampleForm(route=False)) # ignores the usual route behaviour
Keep me posted - curious to hear how it goes and what works and doesn’t…