Forms

David Shaffer
Shaffer Consulting

Basics: Text fields

Let's create a form for a user to enter their "personal information":
WAComponent subclass: #PersonalInformationView
  instanceVariableNames: 'name address gender sendEmailUpdates birthdate'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'SCSeasideTutorial'

name
  ^name

name: aString
  name := aString

address
  ^address

address: aString
  address := aString

gender
  ^gender

gender: aString
  gender := aString

sendEmailUpdates
  ^sendEmailUpdates

sendEmailUpdates: aValue
  sendEmailUpdates := aValue

birthdate
  ^birthdate

birthdate: aDate
  birthdate := aDate
We will begin by prompting for the name (a text field) and address (a text area):
renderContentOn: html
	html form: 
		[html spanClass: 'label' with: 'Name:'.
		html spanClass: 'field'
		     with: [html textInputWithValue: self name
				 callback: [:v | self name: v]].
		html spanClass: 'label' with: 'Address:'.
		html spanClass: 'field'
		     with: [html textAreaWithValue: self address
				 callback: [:v | self address: v]].
		html spanClass: 'button'
		     with: [html submitButtonWithAction: [self save] text: 'Save']]

save
  self inform: self name , '--' , self address

style
	^'
*.label {
	clear: both;
	float: left;
	width: 100px;
}

*.field {
	float: left;
	width: 200px;
}

*.button {
	clear: both;
	width: 200px;
	margin-left: 100px;
	margin-top: 10px;
	float: left;
}'
Notice that form submission buttons use the same callback mechanism as anchors. Text inputs and areas also use callbacks. For each form component you can specify a one argument callback block which will be sent the contents of the input field. When a form is submittted Seaside processes all input field callbacks before the submit button callback. This is very important since it ensures that the action in your submit button can count on your input field callbacks to have done their job (quite often just validating and storing the text that was supplied). So, the save method can count on the input field callbacks having been called if the user entered any data in those fields.

Since this is a new class we need to tell Seaside to use it as the root of an application (just as we did with HelloWorldComponent). Evaluate:

PersonalInformationView registerAsApplication: 'personal'
Now point your web browser at http://localhost:9090/seaside/personal and you should see the form:

Try entering values and submitting the form. Start a new session with the component and submit the form (with empty fields). Notice that you are given a call stack trace for an error that occurred and the option (link) to debug it. Following the "Debug" link will raise the debugger dialog inside Squeak. Try it and find out why this error occurred. If you like you can correct it now or wait until later in this tutorial when we deal with validation.

Text input convenience methods

Quite often your input field callbacks look like those above...simply take the text that the user entered and store it (for example self name: value in the code above).. also, the component's initial value often comes from an accessor method (for example self name in the code above). The renderer provides some methods which do this automatically for you so the following code operates exactly as the code above:
renderContentOn: html
	html form: 
		[html spanClass: 'label' with: 'Name:'.
		html spanClass: 'field' with: [html textInputOn: #name of: self].
		html spanClass: 'label' with: 'Address:'.
		html spanClass: 'field' with: [html textAreaOn: #address of: self].
		html spanClass: 'button'
		     with: [html submitButtonWithAction: [self save] text: 'Save']]
The textInputOn:of: and textAreaOn:of: methods take the (symbol) name of the property to be displayed and the object which holds the property (responds to the accessor and mutator). Seaside generates the method names from the property names using the usual Smalltalk accessor/mutator naming conventions. For example a property called #name would use a method called name as an accessor and a method called name: as a mutator.

Lists

The renderer provides several methods for constructing lists. The following line could be added inside the form: block above to render a list for gender selection:
	html spanClass: 'label' with: 'Gender:'.
	html spanClass: 'field'
	     with: [html
                    selectFromList: #(Male Female)
                    selected: self gender
                    callback: [:v | self gender: v]].
Notice that the selected: argument allows us to specify which item is selected by default (when the list is first displayed). Seaside provides the usual shorthand for our simple case where the callback is a mutator and the starting value comes from an accessor:
 	html spanClass: 'label' with: 'Gender:'.
	html spanClass: 'field'
	     with: [html selectInputOn: #gender of: self list: #(Male Female)]
Let's update save to display the gender:
save
  self inform: name , '--' , address , '--' , gender
Try the application now...you should see either a list box or a combo box (depending on your browser) to select the gender. There are several forms of the list generation methods which I encourage you to explore. Look in WAHtmlRenderer for method names beginning with "select" or, alternatively, the optionWithLabel: methods.

Radio buttons

In our gender example above the list seems like a bit of overkill. Let's present the user with radio buttons instead. Replace the list code with the following:
html spanClass: 'field'
     with:  [|group|
	group := html radioGroup.
	html radioButtonInGroup: group
	     selected: self gender = #Male
	     callback: [self gender: #Male].
	html text: 'Male'; break.
	html radioButtonInGroup: group
	     selected: self gender = #Female
	     callback: [self gender: #Female].
	html text: 'Female'].
Radio buttons in a group are mutually exclusive (only one can be selected at a time). You first ask the renderer to create a new group using radioGroup. This group is then supplied as the first argument to the radio button creation methods. The selected: argument determines if the browser will render the page with that button selected. Notice in our example that we select the button if it corresponds to the current value of the gender variable. That way the form reflects the state of our component. The callback: argument should be a zero argument callback block which is executed when the page is submitted with that radio button selected. Notice the callback block is not called for options that were not selected.

Check boxes

Checkboxes are useful for boolean inputs. Consider the sendEmailUpdates instance variable in our component. We want to render a check box editing that field. Add the following code inside the form: block:
	html spanClass: 'label' with: 'Updates:'.
	html spanClass: 'field' with: [html checkboxOn: #sendEmailUpdates of: self].
I went right to the shorthand version of this method. There is, of course, a method checkboxWithValue:callback:. Update the display method to show the value of this flag:
save
	self inform: name , '--' , address , '--' , gender , '--' , sendEmailUpdates printString
Try the application now. (Notice that the checkbox is selected even though the initial value of the instance variable is nil.) Fill out and submit the form to see that the checkbox is working.

Date inputs

The simplest way to provide a date editor is the renderer's dateInputWithValue:callback: method. Add the following code to your form: block:
	html spanClass: 'label' with: 'Birthdate:'.
	html spanClass: 'field'
	     with: [html dateInputWithValue: self birthdate callback: [:v | self birthdate: v]].
and update our display method:
save
	self inform: name , '--' , address , '--' , gender , '--' , sendEmailUpdates printString ,
                     '--' , self birthdate printString
If you view this component in your browser you'll get an error because, unlike the other components, the date input does not like being given nil for its current value. We need to give a reasonable initial value to the date. Modify the birthdate accessor to lazy initialize the date:
birthdate
	^birthdate ifNil: [birthdate := Date today]
Now load the application in your web browser and everything should be fine. Your final version should look something like this:
C. David Shaffer
Last modified: Wed Jul 20 23:26:21 EDT 2005