Getting input from a user¶
User input is a tricky thing. Users always provide input as strings, but programs work in terms of objects such as integers, boolean values, dates, etc. Part of dealing with user input thus means translating (or marshalling) such a string to the actual Python object it represents. That’s not all though – user input also need to be validated, and appropriate error messages given to guide a user as to what the program considers valid input.
These are considerable tasks for a framework to deal with.
User input in Reahl¶
In order to get input from a user, Reahl uses special
Widget
s, called
Input
s. The responsibility of an
Input
is to show a user the value of some item
in your program, and allow the user to change that value. There are
different kinds of Input
s representing
different ways of dealing with the visual and behavioural aspects of
this task.
Another player, the Field
, has
the job of checking whether a string sent by a user is valid, to turn
that string into Python object, and to finally set such a Python
object as the value of an attribute of one of your model objects
One thing about Input
s: they differ from
other Widget
s in that they have to live as
children of a Form
.
Here is a what we need in the address book application:
Reahl models use makeup¶
The first step towards allowing a user to input anything, is to
augment the model with Field
s that represent each attribute accessible from the user interface of
the application. In our example, the name and email_address attributes of
an Address are exposed to the user.
To augment our Address class with Field
s, create a special method fields() on the class, and annotate it with the @exposed decorator. Such a method should take one argument: fields on which you can set each Field
needed. Take care though – each Field
you create will eventually manipulate an attribute of the same name on instances of your class:
class Address(Base):
__tablename__ = 'addressbook2_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)
The @exposed decorator turns your fields() method into a property named fields on instances of this class. The property is special only in that it is initialised by the method you provide.
The .fields attribute of an Address instance now contains a
Field
for each corresponding
attribute on that Address which can be accessed via a user
interface. We will need it in a moment when we start creating
Input
s.
Note
Fields vs Fields
One thing that may be confusing is that Elixir Fields are added to
Address, but Reahl Field
s are added as well.
You have to keep in mind that Elixir has the job of persisting data in a database. The Elixir Field for email_address specifies that the email_address attribute of an Address object will be saved to a database column that can deal with unicode text.
The Reahl Field
added for email_address is not concerned with
persistence at all. It specifies several bits of useful
metainformation about the email_address attribute of an Address
object – information that can be used to govern interaction with
users.
Reahl Field
s could happily describe an attribute which does not get
persisted at all. Similarly, an Elixir Field
could happily
describe an attribute that is not exposed to a user interface.
Fields provide information about attributes¶
The extra information provided by Field
s is useful in other
contexts. Take the label that is specified everywhere, for
example. This is what a user may call the Field
in natural
language – quite a useful bit of information when you want to create a
user interface referring to the Field
.
The required=True keyword argument can be passed to a Field
to say
that this information normally has to be provided as part of input.
(What good is an Address with only a name, or only an
email_address?)
Notice also that email_address is an EmailField
. A valid email
address has a specific form. The string ‘john’ is not a valid email
address, but 'johndoe@world.org‘ is. Because email_address was
specified as an EmailField
, user input to email_address can now be
validated automatically to make sure only valid input ever makes it into the model.
Adding the actual Widgets¶
You should have gathered by now that we are going to have to add a
Form
so that we can also add
TextInput
s for each of the
Field
s we are
exposing. That’s not the entire story though: we want it to look nice
too – with a label next to each TextInput
, and
successive labels and TextInput
s neatly
aligned underneath each other. It would also be visually pleasing if
we add a heading to this section of our page.
Two elements come to our aid in this regard: An
InputGroup
is a Widget
that groups such a section together, and give the section a heading.
A LabelledBlockInput
is a
Widget
that wraps around another
Input
(such as a
TextInput
), and provides it with a
label. Successive LabelledBlockInput
s will
arrange themselves neatly below each other.
What’s needed thus, is to create a Form
,
containing an InputGroup
, to which is added two
LabelledBlockInput
s, each wrapped around a
TextInput
that is linked to the appropriate
Field
:
class AddressBookPanel(Panel):
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))
self.add_child(AddAddressForm(view))
class AddAddressForm(Form):
def __init__(self, view):
super(AddAddressForm, self).__init__(view, 'add_form')
new_address = Address()
grouped_inputs = self.add_child(InputGroup(view, label_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)) )
Let’s finish the example in the next section.