Wrong Technology

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:

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.

  1. Place each view() in a separate function.
  2. To show the next view, call view.jump(do_something) instead of do_something(view). view.jump() will be released in v0.13.
  3. Pass these functions as routes when creating the root View().

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),
],
)

Complete source

Summary

Compared to vanilla Nitro apps, the solution above adds about 3 extra lines of code per page:

  1. The function def.
  2. Reading the existing state.
  3. 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!

« A Revertible Python dictionary