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 Widgets, called Inputs. 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 Inputs 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 Inputs: they differ from other Widgets in that they have to live as children of a Form.

Here is a what we need in the address book application:

A diagram showing a TextInput linking to an EmailField, in turn linking the email_address attribute of an Address object

A TextInput linked to the email_address attribute of an Address, via an EmailField

Reahl models use makeup

The first step towards allowing a user to input anything, is to augment the model with Fields 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 Fields, 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 Inputs.

Note

Fields vs Fields

One thing that may be confusing is that Elixir Fields are added to Address, but Reahl Fields 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 Fields 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 Fields 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 TextInputs for each of the Fields 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 TextInputs 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 LabelledBlockInputs will arrange themselves neatly below each other.

What’s needed thus, is to create a Form, containing an InputGroup, to which is added two LabelledBlockInputs, 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.