User interaction

Forms and Inputs

Forms get input from users. Build up the Form by adding Input Widgets to it (or one of its children), via an appropriate FormLayout.

To group some TextInputs visually, add them to FieldSet via a FormLayout. The FormLayout arranges the TextInputs with their associated labels and validation error messages. Call add_child() to add the FieldSet to the Form.

class AddressForm(Form):
    def __init__(self, view):
        super().__init__(view, 'address_form')

        inputs = self.add_child(FieldSet(view, legend_text='Add an address'))
        inputs.use_layout(FormLayout())
        
        new_address = Address()
        inputs.layout.add_input(TextInput(self, new_address.fields.name))
        inputs.layout.add_input(TextInput(self, new_address.fields.email_address))

Fields provide metadata

Each Input is wired to an associated Field. A Field holds more information about the similarly named attribute of the Address. EmailField constrains the input to be a valid email address. Invalid input is blocked by the EmailField and the FormLayout displays an error message underneath its TextInput.

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

A rough design sketch

Fields are defined on Address using a method decorated with exposed:

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)

Note

Don’t confuse Fields with SQLAlchemy’s Columns.

The email_address Field sets the email_address attribute of an Address when a Form is submitted (and after validating the input). A Column on that same email_address takes another step—to persist the attribute to the database. They are independent and do not have to be used together.

Buttons and Events

To save an Address to the database, create a save() method on it. Expose an Event for save() so that it can be tied to a Button.

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)

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

    @exposed('save')
    def events(self, events):
        events.save = Event(label='Save', action=Action(self.save))

Add a Button, linked to the save Event. When the Button is clicked, its Event occurs, and the Action is executed.

Note

You don’t have to process input data

Before the Action is executed, each Field processes its input data and sets it on the object the Field is bound to.

class AddressForm(Form):
    def __init__(self, view):
        super().__init__(view, 'address_form')

        inputs = self.add_child(FieldSet(view, legend_text='Add an address'))
        inputs.use_layout(FormLayout())
        
        new_address = Address()
        inputs.layout.add_input(TextInput(self, new_address.fields.name))
        inputs.layout.add_input(TextInput(self, new_address.fields.email_address))

        inputs.add_child(Button(self, new_address.events.save, style='primary'))

A Transition is defined in AddressBookUI.assemble to say that, if the save Event occurs on the home UrlBoundView, the user should just stay on the home UrlBoundView.

class AddressBookUI(UserInterface):
    def assemble(self):
        home = self.define_view('/', title='Address book', page=AddressBookPage.factory())
        self.define_transition(Address.events.save, home, home)

No instance of Address is available at the time Transitions are defined. ‘save’ is added to the @exposed decorator of Address.events to be able to reference Address.events.save instead.