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

Widgets are the basic building blocks of a user interface. A Widget displays something to a user, and can be manipulated by the user. Widgets 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 Widgets 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 Widgets 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 main column of the page used in our “hello” example like this:

class HelloPage(TwoColumnPage):
    def __init__(self, view):
        super(HelloPage, self).__init__(view)
        self.main.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>

Widgets with complicated behaviour have a complicated implementation. This implementation is, of course, hidden from the programmer using such Widgets. This makes using a complicated Widget just as simple as using the simple Widgets 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 Widgets.

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 TwoColumnPage. This is pure lazyness: TwoColumnPage is a page with two columns, a header and footer – all nicely laid out already. We just add our content to its .main column.

class AddressBookPage(TwoColumnPage):
    def __init__(self, view):
        super(AddressBookPage, self).__init__(view, style='basic')
        self.main.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(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))

Note how AddressBookPanel is composed by adding children Widgets 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)))


# The model from before:

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 – using the arguments passed when the Factory was created.

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 which View (of the many defined on the UserInterface) should be shown for the particular URL of the current request.
  • Next, the relevant ViewFactory is used to create the necessary View, its page and all related Widgets 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 file
  • Use 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!

Here is the complete source so far:

from __future__ import unicode_literals
from __future__ import print_function
from reahl.web.fw import UserInterface, Widget
from reahl.web.ui import TwoColumnPage, Panel, P, H


class AddressBookUI(UserInterface):
    def assemble(self):
        self.define_view('/', title='Addresses', page=AddressBookPage.factory())


class AddressBookPage(TwoColumnPage):
    def __init__(self, view):
        super(AddressBookPage, self).__init__(view, style='basic')
        self.main.add_child(AddressBookPanel(view))


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 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:
import elixir
from reahl.sqlalchemysupport import Session, metadata

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)

    def save(self):
        Session.add(self)

Table Of Contents

Previous topic

Models live in components

Next topic

Getting input from a user