Dialogs in Seaside
 
A common task in web applications is asking users for information. At some point one needs to obtain data like a users name, address, phone number, credit card number, password, etc. Typically one does the following:
  1. Display some overall explanatory text
  2. Display a text field or other appropriate input widget for each datum
  3. Display a label for each datum
  4. Display some explanatory text, perhaps on request, for each datum
  5. Validate each datum
  6. Save the data in a database
The request for the data may be spread across multiple pages for a number of reasons. At this is a common task a web framework should make this easy. We will look at several ways this can be done in Seaside. The database access is not discussed here. That is a subject for a later post.
The examples are done using VisualWorks Smalltalk 7.5, Seaside 2.61.140 and Magritte lr.193. This version of Seaside still used both original renderer, WAHtmlRenderer and the newer WARenderCanvas. As the later will be the only render in Seaside 2.8, it is used here.
For our examples we will restrict the requested information to:
  1. Name
  2. Phone number
We will start with the following "model" class for this information.
Smalltalk.Seaside defineClass: #Contact
    superclass: #{Core.Object}
    instanceVariableNames: 'name phoneNumber '
 
name
    ^name
 
name: anObject
    name := anObject
 
phoneNumber
    ^phoneNumber
 
phoneNumber: anObject
    phoneNumber := anObject
 
hasValidName
    ^name notNil
 
hasValidPhoneNumber
    phoneNumber ifNil:[^false].
    ^phoneNumber size > 6
 
By Hand
We will start by creating a single component to perform the task. As you can see below the code is not too bad. The good news is that we have complete control over all aspects of the application - how to display the request, validation, how to display the errors, etc. The bad news is that there is a lot of logic that is repeated each time we do this. Note in particular the logic needed to detect and handle errors.
The class methods handle different aspects of registering the component as a web application, hence are not directly related to the issues at hand.
 
Smalltalk.Seaside defineClass: #FormExample
    superclass: #{Seaside.WAComponent}
    instanceVariableNames: 'contact errors '
    
 
Class Methods
canBeRoot
    "Allow the component to be standalone application"
    ^ true
 
initialize
    "Register application url"
    (self registerAsApplication: 'whitneyExamples/form')
 
description
    "Application description"
    ^'Doing it the hard way'
 
Instance Methods
renderContentOn: html
    html heading: 'Enter your information'.
    self hasErrors
        ifTrue:
            [html text: ['Please correct the following errors'].
            html unorderedList addAll: errors].
    html form:
            [html text: 'Name'.
            (html textInput)
                on: #name of: contact;
                exampleText: 'John Doe'.
            html break.
            html text: 'Phone'.
            html textInput on: #phoneNumber of: contact.
            html break.
            html submitButton callback: [self ok]]
 
rendererClass
    "Use new rendering engine"
    ^WARenderCanvas
 
hasErrors
    ^errors size > 0
 
ok
    "Action on submitting data"
    self validate.
    self hasErrors ifTrue:[^self].
    self answer: contact
 
updateRoot: anHtmlRoot
    super updateRoot: anHtmlRoot.
    anHtmlRoot title: 'Form Decorator Example'.
 
validate
    errors := OrderedCollection new.
    contact hasValidName ifFalse:
        [errors add: 'You must enter a valid name'].
    contact hasValidPhoneNumber ifFalse:
        [errors add:  'Enter a valid phone number'].
 
initialize
    contact := Contact new.
 
Using Decorators
Seaside supports the decorator pattern on components. We will use the following standard decorators:  WAMessageDecoration, WAValidationDecoration and WAFormDecoration. The complete code for the example is below.
First we will look at setting up the decorator chain. The order of the decorators in given in the following diagram.
In the FormDecoratorExample>>initialize method the decorator chain is created with the code:
form := WAFormDecoration new buttons: self buttons.
self addDecoration: form.
self validateWith: [:aContactOrSelf | aContactOrSelf validate].
self addMessage: 'Please enter the following information'.
 
The addDecoration: method in WAComponent add decorators to the front of the decorator chain of a component. The methods validateWith: and addMessage: are helper methods that create the decorators for us.
A WAMessageDecoration object just displays the message (string actually) that it is given. The WAMessageDecoration is place first on the chain so that any error messages occur after the message. If you want the error messages to appear above the message then place the WAMessageDecoration object after the WAValidationDecoration object.
When creating a WAValidationDecoration object you give it a block, which it runs to validate your component. This validation is done when your component "exits" using answer or answer:. When the exit is done via answer: the argument of answer: is passed as the argument to the validation block. When the exit is done via answer the sender of the method answer is the argument of the validation block. If the validation code detects a problem it raises the WAValidationNotification error to indicate a problem. Typically this is done by calling the Object>>validationError: method. The argument of the method is the error message that is to be displayed to the user. Below is the validation code in the Contact class.
Contact>>validate
    self hasValidName
        ifFalse:
            [self validationError: 'You must provide a valid name'].
    self hasValidPhoneNumber
        ifFalse:
            [self validationError: 'You must provide a valid phone
                number'].
 
Since one can only pass a string as an argument of Object>>validationError: it is hard to provide multiple error messages that can be displayed reasonable. The validation code above only indicated one error at a time, which is a poor way to treat a user. Below is a screen shot after the user click on the Ok button without entering either a name or a phone number. The layout is the default produced by the decorators. Use CSS to provide a better view. The message is marked with <h3> tag. The error message is in a <div class="validation-error"> tag. Seaside makes it easy to view the source of a page, so you can determine what tags you need CSS for to improve the layout & look of the page.
The WAFormDecoration object requires a list of buttons (as a collection of strings or symbols) for the form. The text displayed in each button is generated from the text in the list of the buttons. The first letter of the string is capitalized. A space is added before any capital letters inside the string. So "cancel" is displayed as "Cancel" and "goHome" is displayed as "Go Home". The buttons above were generated from #(ok cancel). The unmodified text for the button also is the callback message sent to the component. So in our example the FormDecoratorExample class needs to implement the methods ok and cancel. FormDecoratorExample also needs to implement the method defaultButton which is to return the string for the button the form is to treat as default button. The form decorator sends this message to the next item in the decorator chain. This message is not part of the standard decorrator protocol and is not passes on the chain in the version of Seaside used for these examples. As a result no decorators can be between the form decorator and your component.
Smalltalk.Seaside defineClass: #FormDecoratorExample
    superclass: #{Seaside.WAComponent}
    instanceVariableNames: 'contact '
 
Class Methods
initialize
    (self registerAsApplication: 'whitneyExamples/formDecorator')
 
description
    ^'Using a form decorator'
 
canBeRoot
    ^ true
 
Instance Methods
initialize
    | form |
    super initialize.
    contact := Contact new.
    "Add decorators"
    form := WAFormDecoration new buttons: self buttons.
    self addDecoration: form.
    self validateWith:
        [:aContactOrSelf | aContactOrSelf validate].
    self addMessage: 'Please enter the following information'.
 
renderContentOn: html
    "Display labels and input fields"
    html text: 'Name'.
    (html textInput)
        on: #name of: contact;
        exampleText: 'John Doe'.
    html break.
    html text: 'Phone'.
    html textInput on: #phoneNumber of: contact
 
rendererClass
    ^WARenderCanvas
 
buttons
    ^#(ok cancel)
 
cancel
    "User clicked on the Cancel button"
    self answer
 
defaultButton
    ^self buttons first
 
ok
    "user clicked on the Ok button"
    self answer: contact
 
validate
    "Only called when cancel (self answer) is call, so nothing to
        validate"
    ^true
 
updateRoot: anHtmlRoot
    super updateRoot: anHtmlRoot.
    anHtmlRoot title: 'Form Decorator Example'.  
 
Labeled Dialog
The decorator solution is better than the first solution in that some of the logic common to dialogs can be reused and some of the low level details are done for us. The decorator solution still requires one to layout the input widgets and their labels, which can be automated. In Seaside the WALabelledFormDialog component does just that.
WALabelledFormDialog is an abstract class. To use it one must create a subclass. Your subclass needs to implement the methods labelForSelector:, model and rows. If you want to change the default set of buttons used on the form you need to override the buttons method in your class. For each submit button on the form you need to implement a callback method.
WALabelledFormDialog uses the same decorator structure as we used in the decorator example FormDecoratorExample.
 
Since WALabelledFormDialog already adds a WAFormDecoration we do not need to add it to the decorator chain. Here is the initialize method for the LabelledFormDialogExample (full source code for the example is below).
LabelledFormDialogExample>>initialize
    super initialize.
    contact := Contact new.
    self validateWith: [:aContact | aContact validate].
    self addMessage: 'Please enter the following information'.
 
As mentioned above we need to implement the methods labelForSelector:, model and rows in our subclass of WALabelledFormDialog. The model method just returns the object whose fields we wish to populate with date. The rows method returns a collections of symbols. One symbol for each row of data in the dialog. The symbol is used generate the accessor methods for the data in the model. The method labelForSelector: returns the labels for each row and each submit button in the form. See the source code for the example below.
The contact example generates the following page:
The default input widget is the html input tag. To override the default implement the method renderXOn:, where x is the symbol for the row, in your subclass of WALabelledFormDialog. To use a different input widget for "name" in our example we would use:
LabelledFormDialogExample>>renderNameOn: html
    html
        selectFromList: #( 'Roger' 'Pete')
        selected: 'Roger'
        callback: [:v | contact name: v]
 
The resulting page is below. Since I am using Seaside 2.6 the renderer passed to this method is WAHtmlRenderer. In Seaside 2.8 that renderer is completely replaced by WARenderCanvas. Since in Seaside 2.6 WALabelledFormDialog uses WAHtmlRenderer you cannot use WARenderCanvas in your subclass.
 
Here is the complete Contact example using WALabelledFormDialog.
Smalltalk.Seaside defineClass: #LabelledFormDialogExample
    superclass: #{Seaside.WALabelledFormDialog}
    instanceVariableNames: 'contact '
    Class Methods
 
canBeRoot
    ^ true
 
initialize
    (self registerAsApplication: 'whitneyExamples/formDialog')
 
description
    ^'Using a form Dialog'
 
Instance Methods
initialize
    super initialize.
    contact := Contact new.
    self validateWith: [:aContact | aContact validate].
    self addMessage: 'Please enter the following information'.
 
model
    ^ contact
 
rows
    ^ #(name phoneNumber)
 
ok
    self answer: contact
 
labelForSelector: aSymbol
    aSymbol == #name ifTrue: [^'Your Name'].
    aSymbol == #phoneNumber ifTrue: [^'Phone Number'].
    aSymbol == #ok ifTrue: [^'Ok'].
    ^ super labelForSelector: aSymbol
 
updateRoot: anHtmlRoot
    super updateRoot: anHtmlRoot.
    anHtmlRoot title: 'Form Dialog Example'.
 
Edit Dialog
WAEditDialog is useful in editing new or existing model objects. The main thing it adds to WALabelledFormDialog is the automatic rollback of the model if the user cancels the operation. WAEditDialog is an abstract subclass of WALabelledFormDialog. Your subclass needs to implement the methods labelForSelector: and rows. The method labelForSelector: does not have to handle the form buttons. Your model class has to implement the method copyFrom: as it is used to rollback changes when the user cancels the edit. Here is the method for our Contact class.
Contact>>copyFrom: aContact
    name := aContact name.
    phoneNumber := aContact phoneNumber
 
Since it is used to edit existing model objects WAEditDialog operates slightly differently than WALabelledFormDialog. When a WAEditDialog object returns from a call it returns a boolean not the model. Here is sample code using a WAEditDialog subclass.
contact := Contact new.
editor := EditFormExample model: contact.
wasChanged := self call: editor.
 
Note in the source for EditFormExample how the validate block has changed from previous examples. Don't get too fond of WAEditDialog as it was dropped in Seaside 2.8.
Smalltalk.Seaside defineClass: #EditFormExample
    superclass: #{Seaside.WAEditDialog}
    instanceVariableNames: ''
    
Instance Methods
initialize
    super initialize.
    self validateWith:
        [:saved | saved  ifTrue:[self model validate]].
    self addMessage: 'Please enter the following information'.
 
labelForSelector: aSymbol
    aSymbol == #name ifTrue: [^'Your Name'].
    aSymbol == #phoneNumber ifTrue: [^'Phone Number'].
    ^ super labelForSelector: aSymbol
 
rows
    ^ #(name phoneNumber)
 
updateRoot: anHtmlRoot
    super updateRoot: anHtmlRoot.
    anHtmlRoot title: 'Edit Dialog Example'.
 
Magritte
Magritte, developed by Lukas Renggli, provides a different approach to these types of dialogs. In Magritte one provides meta-data about the model. This meta-data is used to generate the dialogs. For each instance variable of interest you create a class method called descriptionX, where X is some meaningful text. The description contains information about the type and restrictions of the described instance variable and its display location.
Given the Contact class below we generate a dialog page using:
    contact := Contact new asComponent.
    contact addValidatedForm.
    self call: contact.
 
which generates the page:
 
If one clicks on the Save button without entering any text one gets the following page.
Magritte is an interesting system. There are a number of things that need to be covered about Magritte, but that will have to wait for another posting.
Smalltalk.Seaside defineClass: #Contact
    superclass: #{Core.Object}
    instanceVariableNames: 'name phoneNumber '
    classInstanceVariableNames: ''
    imports: '
            Magritte.*
            '
    category: 'SeasideExamples'
 
Class Methods
 
descriptionName
    ^(MAStringDescription auto: 'name' label: 'Name' priority: 30)
        beRequired;
        yourself.
 
descriptionNumber
    | description |
    description := MAStringDescription
                auto: 'phoneNumber'
                label: 'Phone number'
                priority: 40.
    ^description
        beRequired;
        addCondition:
            [:value | value size > 6]
          labelled: 'invalid phone number';
        yourself
 
Instance Methods
name
    ^name
 
name: anObject
    name := anObject
 
phoneNumber
    ^ phoneNumber
 
phoneNumber: anObject
    phoneNumber := anObject
 
Special Purpose Dialogs
Seaside comes with some special purpose dialogs. Below I give a code snippet showing the use and the small screenshot of the result. Two of the dialogs (WAInputDialog and WAYesOrNoDialog) can be used via connivence methods, which I use.
WAChoiceDialog
selection := WAChoiceDialog options:
    #('Smalltalk' 'Perl' 'Python' 'Ruby').
result := self call: selection.
WASelection
selection := WASelection new.
selection items: #(1 'cat' 'rat').
selectedItem := self call: selection.
WAYesOrNoDialog
aBoolean := self confirm: 'Time to sleep?'.
WAInputDialog
aString := self
                request: 'Do you like Seaside?'
                label: 'Ok'
                default: 'Suggested answer'.
 
If you are using Seaside 2.6 you may notice that some dialogs are missing. For Seaside 2.8 dialog classes that were not being used were removed to clean up the Seaside core. The dialogs that were removed are:
  1. WAChangePassword
  2. WAEditDialog
  3. WAEmailConfirmation
  4. WAGridDialog
  5. WALoginDialog
  6. WANoteDialog
 
Monday, July 9, 2007