Back button support
We're trying to see if it's possible to provide some kind of automatic back-button support for Nitro apps without losing state.
Goals #
The primary requirements are:
- Preserve Nitro's programming model.
- Support history / back-button traversal.
- Don't lose state when traversing back and forth.
For reference, the sixth rule in Ben Shneiderman's The Eight Golden Rules of Interface Design says:
Permit easy reversal of actions.
As much as possible, actions should be reversible. This feature relieves anxiety, since users know that errors can be undone, and encourages exploration of unfamiliar options. The units of reversibility may be a single action, a data-entry task, or a complete group of actions, such as entry of a name-address block.
This is complicated, to say the least, but necessary. Naturally, everyone is asking for it.
Solution #
Here's one attempt to address it. The solution ends up being a tad more verbose compared to vanilla Nitro apps.
There are 3 refactorings you'll need to do to get this working.
- Place each
view()
in a separate function. - To show the next view, call
view.jump(do_something)
instead ofdo_something(view)
.view.jump()
will be released in v0.13. - Pass these functions as
routes
when creating the rootView()
.
Demo #
Here's a basic 5-page UI that demonstrates this:
from h2o_nitro import View, box, option
from h2o_nitro_web import web_directory
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
class State:
def __init__(self):
self.person = Person('Boaty', 'McBoatface')
self.father = Person('Papa', 'McBoatface')
self.mother = Person('Mama', 'McBoatface')
def ask_name(view: View):
person: Person = view.context['state'].person
person.first_name, person.last_name = view(
f"# Step 1",
box('Your first name?', value=person.first_name),
box('Your last name?', value=person.last_name)
)
view.jump(ask_father_name)
def ask_father_name(view: View):
father: Person = view.context['state'].father
father.first_name, father.last_name = view(
f"# Step 2",
box("Father's first name?", value=father.first_name),
box("Father's last name?", value=father.last_name)
)
view.jump(ask_mother_name)
def ask_mother_name(view: View):
mother: Person = view.context['state'].mother
mother.first_name, mother.last_name = view(
f"# Step 3",
box("Mother's first name?", value=mother.first_name),
box("Mother's last name?", value=mother.last_name)
)
view.jump(show_results)
def show_results(view: View):
state: State = view.context['state']
view(
f"# Results",
f"Your name: {state.person.first_name} {state.person.last_name}.",
f"Your father's name: {state.father.first_name} {state.father.last_name}.",
f"Your mother's name: {state.mother.last_name} {state.mother.last_name}.",
halt=True,
)
def main(view: View):
view.context['state'] = State()
view(
'# Welcome to the wizard!',
'Use the back button, Luke!'
)
view.jump(ask_name)
nitro = View(
main,
title='Hello Nitro!',
caption='v1.0',
routes=[
option(ask_name),
option(ask_father_name),
option(ask_mother_name),
option(show_results),
],
)
Summary #
Compared to vanilla Nitro apps, the solution above adds about 3 extra lines of code per page:
- The function
def
. - Reading the existing state.
- Jumping to the next view.
Overall, I'm happy with the solution so far. There's a possibility that the you might forget to add one or more functions to the View's routes
list, but this can be mitigated easily by having a custom @route
annotation do this automatically for you.
Happy hacking!