Moving between Views¶
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
Menu that was added to the application.
Menu, a user can choose to navigate to the
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
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:
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
Views . In this example, there is only one
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
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.
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
Menus (or even the bookmarks of the browser
itself). Hence usually we would draw the schematic without its
and only include arrows for
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 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))
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 that
returns True or False. Usually, the framework just picks the
Transition it can find when needed and follows it. Augmenting
Transitions with guards changes this scheme.
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