User interface basics¶
It is time to start work on the actual user interface of our application. For a start, we’ll only build a home page that lists all our addresses.
Widgets¶
Widget
s are the basic building blocks of a user interface. A Widget
displays something to a user, and can be manipulated by the
user. Widget
s can have powerful behaviour that execute in a client
browser and/or on the server itself. A Widget
need not have
complicated behaviour, though. Simple Widget
s exist for things like
paragraphs, for example, which merely display something on screen.
A programmer constructs the user interface by stringing together a
number of existing Widget
s as children of another Widget
. For example,
let’s string together two P
(aragraph)s onto a Panel
:
class ReahllyBadPoem(Panel):
def __init__(self, view):
super(ReahllyBadPoem, self).__init__(view)
self.add_child(P(view, text='Reahl is for real.'))
self.add_child(P(view, text='You can use it in the real world.'))
To use an instance of the ReahllyBadPoem
Widget
, you add it as a child of another
Widget
. For example you can add it to the
body
of the page used in our “hello” example like this:
class HelloPage(HTML5Page):
def __init__(self, view):
super(HelloPage, self).__init__(view)
self.body.add_child(ReahllyBadPoem(view))
When rendered to a web browser, the resulting HTML would be:
<div><p>Reahl is for real.</p><p>You can use it in the real world.</p></div>
Widget
s with complicated behaviour have a
complicated implementation. This implementation is, of course, hidden
from the programmer who is merely using such
Widget
s. Using a complicated
Widget
is just as simple as using the simple
Widget
s in this example. Have a look at the
tabbed panel example to see a more
complicated Widget
in use.
A first-cut UserInterface for the address book application¶
We need to create a UserInterface
for our
application, and add a View
to it, with a page
that contains the necessary Widget
s.
One creates a UserInterface
by inheriting from
UserInterface
, and overriding the method
assemble(). Inside this method, each View
is
defined:
class AddressBookUI(UserInterface):
def assemble(self):
self.define_view('/', title='Addresses', page=AddressBookPage.factory())
AddressBookPage, in turn, is a Widget
which inherits from
HTML5Page
– a basic page that already contains
a .body for us to add stuff to.
class AddressBookPage(HTML5Page):
def __init__(self, view):
super(AddressBookPage, self).__init__(view, style='basic')
self.body.add_child(AddressBookPanel(view))
Our content is in this example encapsulated in its own
Widget
, AddressBookPanel. This is not strictly
speaking necessary – you could have made do with
AddressBookPage. Later on in the tutorial you will see though that
pages are often re-used with different contents. So, we’re just
sticking to a habit here.
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))
Note how AddressBookPanel is composed by adding children Widget
s to it in its constructor: first the heading, and then an AddressBox for each address in our database. An AddressBox, in turn, only contains a P
with text gathered from the address it is meant to display:
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)))
Factories¶
Have you noticed how .factory()
is used in the example code so
far, instead of just constructing the widget?
A web application has to be very economical about when it creates
what. A Factory
is merely an object that can be
used at a later time (if necessary) to create something like a
Widget
or a View
. The
actual Widget
will be created (eventually)
using the arguments passed when the Factory
was
created initially.
When creating a UserInterface
, for example, it
does not make sense to create all the
View
instances it could possibly
have. We rather specify how they will be created eventually, if
needed.
Understanding the lifecycle of all these mechanics is useful:
The user interface elements necessary to fulfil a particular request from a client browser are created briefly on the server for just long enough to serve the request, and are then destroyed again.
This is what happens on the server to handle a request:
- First the
UserInterface
is created (and its.assemble()
called).- The
UserInterface
then determines whichView
(of the many defined on theUserInterface
) should be shown for the particular URL of the current request.- Next, the relevant
ViewFactory
is used to create the necessaryView
, its page and all relatedWidget
s present on that page.- The result of all this is translated to HTML (and other things, such as JavaScript) and sent back to the browser
- Lastly, all of these objects are thrown away on the web server.
Try it out¶
Finally, we have something we can run and play with in a web browser.
When you try to run this app, remember the following gotchas:
Your project needs a .reahlproject file in its root.
Run
reahl setup -- develop -N
each time you update the .reahlproject fileUse reahl-control to create the database and the database tables before you run your application.
For now, drop your old database and recreate a new one if you changed anything related to its schema.
All persisted classes need to be added to the .reahlproject file.
If you have more than one version of a project, put them in directories of which the names differ! Reahl assumes that your component is named for the directory in which it resides.
Your app will not list any addresses at first, because there are none in the database yet! To create some persisted test data, we have defined a command alias (found in the .reahlproject file for the project) that you should run:
reahl demosetup
Here is the complete source so far:
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.web.fw import UserInterface, Widget
from reahl.web.ui import HTML5Page, Div, P, H
from reahl.sqlalchemysupport import Session
class AddressBookUI(UserInterface):
def assemble(self):
self.define_view('/', title='Addresses', page=AddressBookPage.factory())
class AddressBookPage(HTML5Page):
def __init__(self, view):
super(AddressBookPage, self).__init__(view, style='basic')
self.body.add_child(AddressBookPanel(view))
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 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)))
# The model from before:
from sqlalchemy import Column, Integer, UnicodeText
from reahl.sqlalchemysupport import Session, Base
class Address(Base):
__tablename__ = 'addressbook1_address'
id = Column(Integer, primary_key=True)
email_address = Column(UnicodeText)
name = Column(UnicodeText)
def save(self):
Session.add(self)