Moving between Views¶
Most UserInterface
s consist of several
View
s that are designed to work together. It
is useful to have a clear picture of which
View
s exist in a user interface, and how the
user can move between them.
For example, the user interface of the address book application can be
split up into two different View
s : one that
lists all the addresses, and another on which you can add a new
address.
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 itself controls the navigation.
Each of these modes of navigation are dealt with differently:
Transitions¶
Bookmark
s 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 View
s in the application and how the user
will be transitioned between the different View
s 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 View
s . 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 by the application, it makes more
sense to show such navigation more explicitly. Transition
s are special
EventHandler
s 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 View
s via
Bookmark
s on Menu
s (or even the bookmarks of the browser
itself). Hence usually we would draw the schematic without its Menu
,
and only include arrows for Transition
s between View
s . 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
Event
s it will define when called
from __future__ import print_function, unicode_literals, absolute_import, division
from sqlalchemy import Column, Integer, UnicodeText
from reahl.sqlalchemysupport import Session, Base
from reahl.web.fw import UserInterface, Widget
from reahl.web.ui import HTML5Page, Form, TextInput, LabelledBlockInput, Button, Div, P, H, FieldSet, Menu, HorizontalLayout
from reahl.component.modelinterface import exposed, EmailField, Field, Event, Action
class AddressBookPage(HTML5Page):
def __init__(self, view, main_bookmarks):
super(AddressBookPage, self).__init__(view, style='basic')
self.body.add_child(Menu(view).use_layout(HorizontalLayout()).with_bookmarks(main_bookmarks))
class HomePage(AddressBookPage):
def __init__(self, view, main_bookmarks):
super(HomePage, self).__init__(view, main_bookmarks)
self.body.add_child(AddressBookPanel(view))
class AddPage(AddressBookPage):
def __init__(self, view, main_bookmarks):
super(AddPage, self).__init__(view, main_bookmarks)
self.body.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(Div):
def __init__(self, view):
super(AddressBookPanel, self).__init__(view)
self.add_child(H(view, 1, text='Addresses'))
for address in Session.query(Address).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(FieldSet(view, legend_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(Base):
__tablename__ = 'pageflow2_address'
id = Column(Integer, primary_key=True)
email_address = Column(UnicodeText)
name = Column(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¶
Transition
s between View
s 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 that
returns True or False. Usually, the framework just picks the
first Transition
it can find when needed and follows it. Augmenting
Transition
s with guards changes this scheme.
When a Transition
is found that can be followed, the 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.)