Parameterised Views¶
Not all View
s can be statically defined in the .assemble() method of
a UserInterface
. In our AddressBook example you might want to add an
EditAddress View
for each Address in the database as shown in the
following diagram:
This may result in a very large number of View
s – an
“Edit” View
would have to be added to the UserInterface
for each Address in
the database. That is clearly not an acceptable solution.
In order to solve the problem a View
can have arguments – so that a
single “Edit” View
can be defined for an as yet unknown Address. Computing
the actual contents of the View
is delayed until the Address argument
becomes available.
How it works¶
Usually, a View
would have a simple, hardcoded URL such as
‘/add’. When a View
has arguments, its URL is expanded to contain the
values of its arguments. In this example, the View
added on ‘/edit’
results in a whole set of View
s with URLs such as ‘/edit/1’ or
‘/edit/2’. The ‘/1’ and ‘/2’ are the ids for different Addresses in
this example.
Just like UserInterface
s, a View
can also have an .assemble() method in
which the definition of its contents can be finalised, based on the
arguments of the View
. When the framework has to render a View
, it
parses the requested URL to determine which View
the URL is referring
to. The values for the arguments to the View
are then extracted from
the given URL. The View
is constructed, and the .assemble() method
of the newly constructed View
is called, passing all the arguments of
the View
as keyword arguments to it.
The definition of a View
with an .assemble() method is thus partly
deferred to the time when the View
is actually accessed, instead of up
front when the UserInterface
is assembled as was previously shown.
Parameterising a View¶
To specify that a View
has arguments, a programmer supplies a
Field
for governing each argument when defining the View
.
The programmer also needs to supply a custom View
class which
subclasses from UrlBoundView
and in which the .assemble() method is
overridden with custom logic that deals with these arguments.
In the AddressBookUI class shown below, a View
is added for editing,
parameterised by the id of an Address:
class EditView(UrlBoundView):
def assemble(self, address_id=None):
try:
address = Session.query(Address).filter_by(id=address_id).one()
except NoResultFound:
raise CannotCreate()
self.title = 'Edit %s' % address.name
self.set_slot('main', EditAddressForm.factory(address))
class AddressBookUI(UserInterface):
def assemble(self):
add = self.define_view('/add', title='Add an address')
add.set_slot('main', AddAddressForm.factory())
edit = self.define_view('/edit', view_class=EditView, address_id=IntegerField())
addresses = self.define_view('/', title='Addresses')
addresses.set_slot('main', AddressBookPanel.factory(self))
bookmarks = [f.as_bookmark(self) for f in [addresses, add]]
self.define_page(AddressBookPage, bookmarks)
self.define_transition(Address.events.save, add, addresses)
self.define_transition(Address.events.update, edit, addresses)
self.edit = edit
def get_edit_bookmark(self, address, description=None):
return self.edit.as_bookmark(self, address_id=address.id, description=description)
Notice how the arguments of the View
are specified. They are passed as
Field
s in extra keyword arguments to the .define_view() method.
Since an EditView is needed, it is also specified by the view_class
keyword argument of .define_view().
The signature of EditView.assemble() needs to include a matching keyword argument for each argument thus defined.
Inside EditView.assemble(), an Address is first obtained from the
given address_id, then that Address is used to customise the title
of the View
. This Address is also used when setting the ‘main’ slot to
contain an EditForm for the given address.
It is possible that a View
may be requested for an Address id which
does not exist in the database. If this should happen, just raise a
CannotCreate
as shown in the beginning of EditView.assemble()`.
A word about bookmarks¶
Since a UserInterface
(in this case AddressBookUI) already contains the
knowledge of which View
s it contains, it seems to be good design that
other elements of the user interface ask it for Bookmark
s to those
View
s when needed.
For this reason, the .get_edit_bookmark() method was added to
AddressBookUI. You will notice in the code below that AddressBookUI
is sent all the way to each AddressBox just so that
AddressBookUI.get_edit_bookmark() can be called. Notice also that a
Bookmark
can never be obtained for ‘the edit View
‘, a Bookmark
is for
something like ‘the edit View
for address X’: it includes the
arguments of the bookmarked View
.
Here is the complete application thus far:
from __future__ import print_function, unicode_literals, absolute_import, division
from sqlalchemy import Column, Integer, UnicodeText
from sqlalchemy.orm.exc import NoResultFound
from reahl.sqlalchemysupport import Session, Base
from reahl.web.fw import CannotCreate, UrlBoundView, UserInterface, Widget
from reahl.web.ui import HTML5Page, Form, TextInput, LabelledBlockInput, Button, Div, P, H, A, FieldSet, Menu, HorizontalLayout
from reahl.web.layout import PageLayout
from reahl.web.pure import ColumnLayout
from reahl.component.modelinterface import exposed, EmailField, Field, Event, IntegerField, Action
class AddressBookPage(HTML5Page):
def __init__(self, view, main_bookmarks):
super(AddressBookPage, self).__init__(view, style='basic')
self.use_layout(PageLayout())
contents_layout=ColumnLayout('main').with_slots()
self.layout.contents.use_layout(contents_layout)
self.layout.header.add_child(Menu(view).use_layout(HorizontalLayout()).with_bookmarks(main_bookmarks))
class EditView(UrlBoundView):
def assemble(self, address_id=None):
try:
address = Session.query(Address).filter_by(id=address_id).one()
except NoResultFound:
raise CannotCreate()
self.title = 'Edit %s' % address.name
self.set_slot('main', EditAddressForm.factory(address))
class AddressBookUI(UserInterface):
def assemble(self):
add = self.define_view('/add', title='Add an address')
add.set_slot('main', AddAddressForm.factory())
edit = self.define_view('/edit', view_class=EditView, address_id=IntegerField())
addresses = self.define_view('/', title='Addresses')
addresses.set_slot('main', AddressBookPanel.factory(self))
bookmarks = [f.as_bookmark(self) for f in [addresses, add]]
self.define_page(AddressBookPage, bookmarks)
self.define_transition(Address.events.save, add, addresses)
self.define_transition(Address.events.update, edit, addresses)
self.edit = edit
def get_edit_bookmark(self, address, description=None):
return self.edit.as_bookmark(self, address_id=address.id, description=description)
class AddressBookPanel(Div):
def __init__(self, view, address_book_ui):
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, address_book_ui))
class EditAddressForm(Form):
def __init__(self, view, address):
super(EditAddressForm, self).__init__(view, 'edit_form')
grouped_inputs = self.add_child(FieldSet(view, legend_text='Edit address'))
grouped_inputs.add_child(LabelledBlockInput(TextInput(self, address.fields.name)))
grouped_inputs.add_child(LabelledBlockInput(TextInput(self, address.fields.email_address)))
grouped_inputs.add_child(Button(self, address.events.update))
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, address_book_ui):
super(AddressBox, self).__init__(view)
bookmark = address_book_ui.get_edit_bookmark(address=address, description='edit')
par = self.add_child(P(view, text='%s: %s ' % (address.name, address.email_address)))
par.add_child(A.from_bookmark(view, bookmark))
class Address(Base):
__tablename__ = 'tutorial_parameterised1_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)
@exposed('save', 'update')
def events(self, events):
events.save = Event(label='Save', action=Action(self.save))
events.update = Event(label='Update')
def save(self):
Session.add(self)
Programmatic arguments¶
Not all the arguments passed to the .assemble() method of a View
need to be parsed from the URL of the View
. Sometimes it is useful to
pass an object that is available in the .assemble() of the
containing UserInterface
to the .assemble() of one of its View
s .
For example, the .assemble() of a particular View
may need access to a
Bookmark
which is computed inside the .assemble() of its UserInterface
.
A View
can be parameterised by such arguments as well. Just pass the
actual value as keyword argument to .define_view(). The framework
distinguishes between normal programmatic arguments and those that
have to be parsed from the URL based on the fact that Field
instances
are sent for the arguments that need to be parsed from the URL. At the
end of the day they’re all just arguments to the View
though.
Transitions to parameterised Views¶
In the example above, hypertext links were added for each Address
listed on the Addresses page. Given this example that is probably
good enough. In some cases, though, it is desirable to transition a
user to a parameterised View
in response to a Button
being clicked
(ie, in response to an Event
).
When the framework transitions a user to a parameterised View
in
response to a Button
having been clicked, the arguments passed to the
View
originate from the Button
.
Remember how this is all strung together: the Button
is linked to an
Event
, which in turn fires off a Transition
that leads to
the View
itself. The parameters to be used for the View
thus have to
be passed on along this chain: when placing the Button
the programmer
has to supply the actual values to the arguments, and the Event
must
ferry along whatever arguments are passed to it. The Transition
has
the final responsibility of supplying the arguments needed by its
target View
– by picking them off the Event
that occurred.
To show how this works, let us change the current example by adding
a Button
to each AddressBox instead of a hypertext link. To be able to
do this, each AddressBox needs to be a little Form
, since Button
s
need to be part of a Form
.
When the Button
is placed, it is linked to a version of the Event
which is bound to certain argument values. This bound Event
is
obtained by calling .with_arguments() on the original Event
, passing
it the actual values needed by the target View
.
The changed implementation of AddressBox below shows how AddressBox
has been changed to a Form
, and also how the Button
is created with an
Event
which is bound to argument values:
class AddressBox(Form):
def __init__(self, view, address):
form_name = 'address_%s' % address.id # Forms need unique names!
super(AddressBox, self).__init__(view, form_name)
par = self.add_child(P(view, text='%s: %s ' % (address.name, address.email_address)))
par.add_child(Button(self, address.events.edit.with_arguments(address_id=address.id)))
Note
Methods starting with add_ always return the added thing. This is similar to methods starting with define_ which return the Factory for the defined thing. (A handy little trick borrowed from our SmallTalk friends!)
The final change to the application is the addition of a
transition. This is again done in the .assemble() method of the
AddressBookUI. Note how the structure of our initial schematic design
is visible in this method – each View
is defined, and then all the
transitions between the View
s :
class AddressBookUI(UserInterface):
def assemble(self):
add = self.define_view('/add', title='Add an address')
add.set_slot('main', AddAddressForm.factory())
edit = self.define_view('/edit', view_class=EditView, address_id=IntegerField())
addresses = self.define_view('/', title='Addresses')
addresses.set_slot('main', AddressBookPanel.factory())
self.define_transition(Address.events.save, add, addresses)
self.define_transition(Address.events.update, edit, addresses)
self.define_transition(Address.events.edit, addresses, edit)
bookmarks = [f.as_bookmark(self) for f in [addresses, add]]
self.define_page(AddressBookPage, bookmarks)