Wanted -- A Seaside Tutorial: Part 5

In the last post we created a constructor for the WantedItem class that made it easier to add WantedItems to the WantedDatabase in a workspace. In this entry we'll create a WantedEditor component that will allow us to create and edit WantedItems using the web app. Grab your beverage of choice and fire up Squeak with the want-tutorial.image.

Step 1: Refactor the WantedItem Class
One of the things that the WantedItem title:notes: constructor does is initialize the enteredDate instance variable. If we create a WantedItem using the new message, the enteredDate will not get set. Let's change WantedItem to guarantee that any new WantedItem instance automatically gets an enteredDate. We'll do that by adding an initialize method to WantedItem:

initialize
super initialize.
enteredDate := DateAndTime now

Now that the initialize method handles initializing the enteredDate, we can take it out of the title:notes: constructor on the class side:

title: aTitle notes: someNotes
^ self new title: aTitle;
notes: someNotes;
yourself


Step 2: Create the WantedEditor Class

Create a new class called WantedEditor that subclasses WTComponent and add a wantedItem instance variable:


Highlight the wantedItem instance variable, then right-click or option-click it and choose "selection...", then "create accessors". Change the wantedItem getter to the following:

wantedItem
^ wantedItem
ifNil: [wantedItem := WantedItem new]

The purpose of WantedEditor is to either create a new WantedItem instance, or edit an exising one. Having a getter that lazy initializes wantedItem to a new WantedItem instance if it doesn't already exist allows for creation. Having a setter that allows us to set wantedItem to an existing instance allows for editing. We'll get to this in a bit.

Call And Answer
Before we continue finishing the WantedEditor component, we need to learn how WantedList will be able to transfer control from itself to an instance of WantedEditor. Seaside provides the call and answer messages for this. We'll have WantedList send itself the call: message with an instance of a WantedEditor as the argument. Control of the application will then switch to the WantedEditor instance which will display its interface and wait for user action. When WantedEditor is done with its work, it can revert control back to WantedList by sending itself the answer: message. The answer: message accepts an argument that is returned back to the site of the call:. This way a WantedList can ask a WantedEditor to create a WantedItem and return it back to the WantedList with the answer: message:


We're going to work a little backwards by implementing answer: before call: since we're already in WantedEditor. Let's start with creating WantedEditor's renderContentOn: method.

renderContentOn: html
html
form: [html text: 'Title: '.
html textInput on: #title of: self wantedItem.
html break.
html text: 'Notes: '.
html textArea on: #notes of: self wantedItem.
html break.
html submitButton on: #save of: self.
html cancelButton on: #cancel of: self]

This will produce the following form:

The WantedEditor renders an html form that contains text form elements for title and notes and two submit buttons for saving and cancelling. Each html element is created by asking the html canvas for the control it wants, then sending the control the on:of: message.

The on:of: messages wire each form element to a selector (the on argument) of an object (the of argument) when the form is submitted. For the buttons, this means sending the WantedEditor instance the save or cancel message. For the text elements, this means sending the value in the form element to the selector (title and notes) of "self wantedItem". Remember, if a wantedItem hasn't been set, a new instance of WantedItem will be created in the first call to "self wantedItem".

We need to create the two methods that correspond to the selectors used for the buttons:

cancel
self answer: nil

and:

save
self answer: wantedItem

Notice that both methods end by sending the WantedEditor instance (self) the answer: message. Both return an object back to the calling site: cancel returns a nil object since nothing has changed or been created, and save returns the wantedItem we were editing.

Now we need to wire up WantedList to WantedEditor.

Step 3: Call WantedEditor to Add WantedItems
Back in WantedList, add a link to add new wanted items in renderContentOn:

renderContentOn: html
html render: wantedReport.
html anchor on: #add of: self

When a user clicks the link, the add message will be sent to self (the WantedList instance). Let's go ahead and create add:

add
| wantedItem |
wantedItem := self call: WantedEditor new.
wantedItem
ifNotNil: [WantedDatabase wantedItems add: wantedItem]

We start by creating a wantedItem temporary variable. We then send the WantedList instance the call: message with a new instance of WantedEditor. The WantedEditor will assume control of the web application and display the user the form that allows them to set the wantedItem instance attributes. The first call to "self wantedItem" will lazy initialize a new wantedItem instance (which will now have the enteredDate set). If the user clicks the Cancel button a nil object will be returned (answered). If the user clicks the Save button, the wantedItem instance they were editing will be returned (answered). The result of either cancel or save is assigned to our wantedItem temporary variable. If it is not nil (they pressed the Save button), we add it to the WantedDatabase wantedItems collection.

Let's add a new WantedItem to make sure it works. Open a web browser to http://localhost:8080/seaside/wanted. Click the "Add" link, fill in the form, then click the "Save" button. You should be returned to the wanted list which now contains the new wanted item at the bottom. Woohoo!

Step 4: Call WantedEditor to Edit WantedItems
To let the user select a wanted item to edit, we'll turn the Titles of the wanted items into links that when clicked call a WantedEditor instance:


To turn the Title text into links, we'll need to revisit the initialize method of WantedList where we created the WATableReport. Change the line where the title WAReportColumn is being created by adding a clickBlock and returning yourself:

initialize
| rows columns |
super initialize.
columns := OrderedCollection new
add: ((WAReportColumn selector: #title title: 'Title')
clickBlock: [:item | self edit: item];
yourself);

add: (WAReportColumn selector: #enteredDate
title: 'Entered Date');

add: (WAReportColumn
renderBlock: [:item | item isPurchasable asString]
title: 'Can Buy');
yourself.
rows := WantedDatabase wantedItems.
wantedReport := WATableReport new rows: rows;
columns: columns;
yourself

The clickBlock will receive the WantedItem that was clicked on (we name the block argument "item"). The block sends the edit message with the item to self (the WantedList instance). We need to create the edit method:

edit: aWantedItem
self call: (WantedEditor new wantedItem: aWantedItem;
yourself)

Here we are calling a new WantedEditor instance again, but this time we are setting the wantedItem instance instead of having a new one lazy initialized. When WantedEditor's renderContentOn: method is called, it will use the values from the wantedItem object we passed to it to fill in the form values.

Go back to the browser and click on the "New Session" link at the bottom of the page. We have to do this since our WantedList instance has already been initialized--clicking the "New Session" link will force a new WantedList instance to intialize, creating a new wantedReport that has the clickBlock changes we made. Click on one of the title links and you should get the wanted editor page. If you edit the title and click Save you will be taken back to the wanted list page and the title for that wanted item should have changed in the wanted list.

We're almost there: the last piece of functionality is to remove WantedItems.

Step 5: Add Remove Functionality
To add remove functionality, we're going to add a "Remove" link for each wanted item row. To do this, we'll have to add another column to our wantedReport in the WantedList>>initialize method:

initialize
| rows columns |
super initialize.
columns := OrderedCollection new
add: ((WAReportColumn selector: #title title: 'Title')
clickBlock: [:item | self edit: item];
yourself);

add: (WAReportColumn selector: #enteredDate
title: 'Entered Date');

add: (WAReportColumn
renderBlock: [:item | item isPurchasable asString]
title: 'Can Buy');

add: (WAReportColumn new title: 'Remove';
valueBlock: [:item | 'Remove'];
clickBlock: [:item | self remove: item];
yourself);
yourself.

rows := WantedDatabase wantedItems.
wantedReport := WATableReport new rows: rows;
columns: columns;
yourself

We set the title to "Remove", set the value to render (the link text) to "Remove", and the action to take when clicked to sending self the remove message.

Let's add the remove: method:

remove: aWantedItem
WantedDatabase wantedItems remove: aWantedItem

This simply removes the WantedItem instance from the WantedDatabase wantedItems collection.

Flip back to the browser and click the "New Session" link. You should see a "Remove" link next to each wanted item row. Go ahead and click on of them. The item should be removed from the WantedDatabase, then the page should re-render with the item removed from the table.

And that's it! Go forth and buy less!

Conclusion
At a basic level, this tutorial is complete. We have a first cut at a working application: we can Create, Report, Update and Delete WantedItems. The posts so far have shown how to build a simple application using the basic building blocks of Seaside. The app is by no means finished: it's ugly, it's not using the latest and greatest ideas and tools available for seaside (things like Magritte and Scriptaculous), there aren't any unit tests, we're using the image to store our items , we haven't provided a way to import or export the data, etc. Future posts will focus on some of these concerns, but may not use the Wanted material. Stay tuned!

Update
There was a bug in WantedEditor where pressing the Cancel button would persist any changes made to the title or notes fields. To change this, the Cancel "submitButton" has been changed to a "cancelButton". If you are using the same squeak-web image version downloaded in the first part of this tutorial or you do not have WARenderCanvas>>cancelButton, you will need to upgrade the Seaside version. Here's how to do it step by step:

  1. Left-click on the world and choose "open..." then "Monticello Browser".

  2. Click on the "Seaside2" entry in the left list pane.

  3. Click on the "http://www.squeaksource.com/Seaside" url entry on the right list pane so that it is highlighted.

  4. Click the Open button above the right list pane.

  5. Find and click on the entry marked "Seaside2.7a1-mb.142.mcz" in the right list pane so that it is highlighted. This was the commit that added the cancelButton functionality.

  6. Click the Load button above the right list pane.

  7. Close the Monticello windows and change the Cancel submitButton to a cancelButton in WantedEditor>>renderContentOn:.


I apologize for not finding this earlier.

3 comments:

Anonymous said...

A tutorial on using CSS with Wanted would be nice.

mj said...

You got it. I'll start working on it tonight.

Anonymous said...

Excellent step-by-step tutorial into Seaside as well as Smalltalk. Thanks a million for writing it!
I was getting so frustrated by a lot of other tutorials that didn't even explain how to subclass objects or write new methods. I am totally new to Smalltalk and seaside and find this by far the most helpful resource!