Moving between Views

Most UserInterfaces consist of several Views that are designed to work together. It is useful to have a clear picture of which Views exist in a user interface, and how they relate to one another.

For example, the user interface of the address book application can be split up into two different Views : one that lists all the addresses, and another on which you can add a new address.

../_images/addressbooksplit.png

The address book application with two Views .

Notice the Menu that was added to the application. From the Menu, a user can choose to navigate to the “Addresses” View, or the “Add Address” View. This is an example of navigation provided which is under the control of the user.

Also shown in the schematic is that when a user is on the “Add Address” View, and clicks on the Add button, the user is automatically transitioned back to the “Addresses” View. In this case, the web application controls the navigation.

Each of these modes of navigation are dealt with differently:

Transitions

Bookmarks form the cornerstone of any navigation which is under the control of the user. Navigation that happens under the control of the application has to be programmed explicitly.

The schematic diagram at the beginning of this example is a good way to visualise the different Views in the application and how the user will be transitioned between the different Views in response to user actions. One of the aims with Reahl is to have code that clearly maps to such a schematic representation of the application. This is the purpose of the .assemble() method of a UserInterface. In this method, the programmer first defines each View of the UserInterface, and then defines each possible Transition between Views . In this example, there is only one Transition:

class AddressBookUI(UserInterface):
    def assemble(self):
        addresses = self.define_view('/', title='Addresses')
        add = self.define_view('/add', title='Add an address')
        
        bookmarks = [v.as_bookmark(self) for v in [addresses, add]]

        addresses.set_page(HomePage.factory(bookmarks))
        add.set_page(AddPage.factory(bookmarks))

        self.define_transition(Address.events.save, add, addresses)

In previous examples an EventHandler was defined before a Button is placed that needed to trigger an Event. That plan works well for simple examples – where a user stays on the same page after an Event. Where users are moved around in an application, it makes more sense to show such navigation more explicitly. Transitions are special EventHandlers used for this purpose.

Drawing a diagram of how a user can self-nagivate to pages is not very useful though, because users can jump to many different Views via Bookmarks on Menus (or even the bookmarks of the browser itself). Hence usually we would draw the schematic without its Menu, and only include arrows for Transitions between Views . This is the picture which the code of .assemble() tries to make explicit.

Normally, in order to define a transition, one needs to specify the Event which will trigger it. In our example that Event is not available, since there is no instance of Address available in the .assemble() method of the UserInterface to ask for its .events.save. For that reason, .events.save can also be called on the Address class itself. The call to .define_transition (and its ilk) does not need the actual event – it just needs to know how to identify the Event which serves as its trigger. The .events attribute, when accessed on a class yields this information, but it can only do so for events that were named explicitly in the call to the @exposed decorator. (This limitation is neccessitated by what is possible in the underlying implementation of the mechanism.)

The modified final code for our current example is shown below. Note the following changes as per the explanation above:

  • the call to .define_event_handler was removed
  • a new call to .define_transition was added (accessing Address.events.save); and
  • the @exposed decorator was changed to explicitly include the names of the Events it will define when called
from __future__ import unicode_literals
from __future__ import print_function
import elixir

from reahl.sqlalchemysupport import Session, metadata

from reahl.web.fw import UserInterface, Widget
from reahl.web.ui import TwoColumnPage, Form, TextInput, LabelledBlockInput, Button, Panel, P, H, InputGroup, HMenu
from reahl.component.modelinterface import exposed, EmailField, Field, Event, Action


class AddressBookPage(TwoColumnPage):
    def __init__(self, view, main_bookmarks):
        super(AddressBookPage, self).__init__(view, style='basic')
        self.header.add_child(HMenu.from_bookmarks(view, main_bookmarks))


class HomePage(AddressBookPage):
    def __init__(self, view, main_bookmarks):
        super(HomePage, self).__init__(view, main_bookmarks)
        self.main.add_child(AddressBookPanel(view))


class AddPage(AddressBookPage):
    def __init__(self, view, main_bookmarks):
        super(AddPage, self).__init__(view, main_bookmarks)
        self.main.add_child(AddAddressForm(view))


class AddressBookUI(UserInterface):
    def assemble(self):
        addresses = self.define_view('/', title='Addresses')
        add = self.define_view('/add', title='Add an address')
        
        bookmarks = [v.as_bookmark(self) for v in [addresses, add]]

        addresses.set_page(HomePage.factory(bookmarks))
        add.set_page(AddPage.factory(bookmarks))

        self.define_transition(Address.events.save, add, addresses)


class AddressBookPanel(Panel):
    def __init__(self, view):
        super(AddressBookPanel, self).__init__(view)

        self.add_child(H(view, 1, text='Addresses'))
        
        for address in Address.query.all():
            self.add_child(AddressBox(view, address))


class AddAddressForm(Form):
    def __init__(self, view):
        super(AddAddressForm, self).__init__(view, 'add_form')

        new_address = Address()

        grouped_inputs = self.add_child(InputGroup(view, label_text='Add an address'))
        grouped_inputs.add_child( LabelledBlockInput(TextInput(self, new_address.fields.name)) )
        grouped_inputs.add_child( LabelledBlockInput(TextInput(self, new_address.fields.email_address)) )

        grouped_inputs.add_child(Button(self, new_address.events.save))


class AddressBox(Widget):
    def __init__(self, view, address):
        super(AddressBox, self).__init__(view)
        self.add_child(P(view, text='%s: %s' % (address.name, address.email_address)))


class Address(elixir.Entity):
    elixir.using_options(session=Session, metadata=metadata)
    elixir.using_mapper_options(save_on_init=False)
    
    email_address = elixir.Field(elixir.UnicodeText)
    name          = elixir.Field(elixir.UnicodeText)

    @exposed
    def fields(self, fields):
        fields.name = Field(label='Name', required=True)
        fields.email_address = EmailField(label='Email', required=True)

    def save(self):
        Session.add(self)
        
    @exposed('save')
    def events(self, events):
        events.save = Event(label='Save', action=Action(self.save))

Guards

Transitions between Views define the “page flow” of a web application. Such “page flow” is not always simple. Sometimes it is desirable to have more than one Transition for a particular Event, but have the system choose at runtime which one should be followed.

In order to allow such an arrangement, a Transition can be accompanied by a guard. A guard is an Action which wraps a method which will always return True or False. Usually, the framework just picks the first Transition it can find when needed and follows it. Augmenting Transitions with guards changes this scheme.

When a Transition is found that can be followed, he guard of the Transition is first executed. That Transition is the only followed if its guard returns True. If a guard returns False, the search is continued non-deterministically for the next suitable Transition (with True guard).

In order to specify a guard for a Transition, pass an Action to the ‘guard’ keyword argument of .define_transition(). (See Reacting to user events for an example.)

Table Of Contents

Previous topic

Buttons allow users to act

Next topic

Make light work of similar-looking pages