Via The Weekly Squeak, I found a link to a new Seaside tutorial. The Software Architecture Group of the Hasso-Platter-Institut implemented a todo application tutorial Seaside.

The tutorial is very complete:

Extern resources like images, css or javascript do belong to a proper website as well. Chapter seven contains the possibilities you have with Seaside and their pros and cons. Up to now, the whole application is only practicable for one time, afterwards all values of the user are forgotten. This problem of the persistence is treated by the eight chapter which, next to it, presents three different possibilities in detail. … In the nineth part the focus is put on an additional library which makes it possible to implement Ajax in Seaside Websites. Script.aculo.us with the integration by Lukas Renggli offers an easy and simple way to create your own Website in the style of Web 2.0.

Go and read it. Very good !

The application’s models

We’ll need a Todo, TodoUser and TodoUserDatabase. Each user will keep a copy of it’s todos, and the database will give us methods to find and register new users. Let’s start with the user’s database:


1 Object subclass: #TodoUserDatabase
2 instanceVariableNames: ‘database’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-Model’

In case you never noticed, this is a message named #subclass:instanceVariableNames:classVariableNames:poolDictionaries:category, and the receiver is Object. Through the magic of code formatting, this looks like a class declaration, but it’s just another message.

The database object needs a way to initialize itself. We have two ways to do it: either at instance initialization time, or on first access time. Let’s go the latter way, which is what seems most prevalent in Smalltalk / Seaside code I’ve read:


1 database
2 ^ database ifNil: [database := OrderedCollection new]

Put this method in protocol private. Then, we need a way to add and remove users:


1 addUser: aUser
2 (self findWithLogin: aUser login)
3 ifNil: [self database add: aUser. ^ aUser]
4 ifNotNil: [self raiseDuplicateLoginName]
5
6 removeUser: aUser
7 database remove: aUser

These go in protocol accessing. Notice #addUser: calls #raiseDuplicateLoginName. Let’s define that immediately:


1 rraiseDuplicateLoginName
2 Error raiseSignal: ’Can’’t have two users with the same login’

Put this method in protocol error handling. #addUser: calls another helper method: #findWithLogin:. The implementation looks like this:


1 findWithLogin: aLogin
2 ^ self database detect: [:each | each login = aLogin] ifNone: [nil]

Again, put this method in protocol accessing. We’re done with the database side of things. We can now switch to the user itself.

TodoUser model


1 Object subclass: #TodoUser
2 instanceVariableNames: ‘login password todos’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-Model’

As the class comment, enter this text:

Instances of myself represent a user with a login and password, as well as a collection of Todo instances.

In protocol accessing, we define basic accessor methods:


1 login
2 ^ login
3
4 login: anObject
5 login := anObject
6
7 isSamePassword: aPassword
8 ^ password = aPassword
9
10 password: anObject
11 password := anObject
12
13 todos
14 ^ todos ifNil: [todos := OrderedCollection new]

Again, note how the todos instance variable is initialized if it wasn’t previously initialized. Equivalent Ruby code uses the ||= operator.

Then, we need to add and remove todos from the user:


1 addTodo: aTodo
2 self todos add: aTodo
3
4 removeTodo: aTodo
5 self todos remove: aTodo

Pretty simple, as things go. Put these in the accessing protocol. The final model is the Todo itself.

Todo model

Let’s declare the class:


1 Object subclass: #Todo
2 instanceVariableNames: ‘createdAt completed description completedAt’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-Model’

And the class’ comment:

Instances of myself represent a task that should be done, a todo. Todos are pretty simple: they have a description and a flag that identifies the completion status. Todos also keep track of when they were instantiated and completed.

This time around, we need an #initialize method:


1 initialize
2 createdAt := DateAndTime now

Put this in the initialization protocol. Next, in protocol accessing, we add a couple of basic accessors:


1 createdAt
2 ^ createdAt
3
4 description
5 ^ description
6
7 description: aString
8 description := aString
9
10 isCompleted
11 ^ completedAt notNil
12
13 completedAt
14 ^ completedAt
15
16 completedAt: aDateTime
17 completedAt := aDateTime
18
19 markCompleted
20 completedAt := DateAndTime now.

The Seaside UI: controllers and views

To kick things off, I define a new Seaside WAComponent subclass which will be our root component:


1 WAComponent subclass: #TodoComponent
2 instanceVariableNames: ‘’
3 classVariableNames: ’UserDatabase’
4 poolDictionaries: ‘’
5 category: ’Todo-UI-Seaside’

We define ourselves a class variable named UserDatabase which will hold an instance of TodoUserDatabase. Let’s give ourselves two accessor methods to the database: one class side and the other instance side. Put this one in accessing, class side:


1 userDatabase
2 ^ UserDatabase ifNil: [UserDatabase := TodoUserDatabase new]

Again, we see the same pattern: initialize unless already initialized. Back on the instance side, add this method in the accessing protocol:


1 userDatabase
2 ^ self class userDatabase

This is a simple call to the class side version of the method with the same name.

TodoAuthDecorator

To implement authentication, we must wrap the application within a decorator that will take care of these details for it. The decorator’s job is simple: authenticate or register a new user, and when authenticated, show the application instead of the authentication / registration form. I based this implementation on Seaside’s WABasicAuthentication, but mine uses a form instead of the HTTP Basic Access Authentication method. I could have subclassed WABasicAuthentication, but I wanted to learn how to do it manually before I reused code.

Seaside’s decorators have an owner, which is the decorated component. The decorator has a chance to let the component render itself or not, which is what happens later in #renderContentOn:.

Let’s start by declaring the class:


1 WADecoration subclass: #TodoAuthDecorator
2 instanceVariableNames: ‘login password passwordConfirmation authenticated authenticationMessage registrationMessage’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-UI-Seaside’

login, password and passwordConfirmation are used to hold the login and password during registration and authentication. I use authenticated as a simple boolean to determine if authentication was successful or not. authenticationMessage and registrationMessage are messages that will be shown to the user (think of Rail’s flash). I begin in the initialization protocol with:


1 initialize
2 authenticated := false

All Seaside decorators that want a chance to render around their owner must provide a #renderContentOn: method. This goes in the rendering protocol:


1 renderContentOn: html
2 authenticated
3 ifTrue: [self renderOwnerOn: html]
4 ifFalse: [self renderAuthenticationFormOn: html]

When authenticated, we render our owner (the decorated component), else we call #renderAuthenticationFormOn:, which looks like this:


1 renderAuthenticationFormOn: html
2 html heading: ‘Todo List’.
3 (html div) id: ‘auth’; with: [
4 (html div) class: ‘column returning’; with: [
5 self renderReturningUserFormOn: html].
6 (html div) class: ‘column new’; with: [
7 self renderNewUserFormOn: html]].
8 html div class: ‘clear’; with: [html space]

Most of this is setting up nested DIVs to create a two-column layout. Styling is handled by the #style method:


1 style
2 ^ ’
3 #auth-area a#logout { float: right}
4 #auth .column { width: 49%; float: left; }
5 #auth form label, #auth form input { display: block; }
6 .clear { clear: both; }
7

#renderAuthenticationFormOn: uses a couple of helper methods:


1 renderNewUserFormOn: html
2 html form: [
3 html heading: ‘I am a new user’ level: 2.
4 registrationMessage ifNotNilDo: [:msg|
5 html heading: msg level: 3].
6 html div: [
7 (html label) for: ‘new-login’; with: ‘Login’.
8 (html textInput) id: ‘new-login’; on: #login of: self].
9 html div: [
10 (html label) for: ‘new-password’; with: ‘Password’.
11 (html passwordInput) id: ‘new-password’; on: #password of: self].
12 html div: [
13 (html label) for: ‘new-password-confirmation’; with: ‘Confirm password’.
14 (html passwordInput) id: ‘new-password-confirmation’; on: #passwordConfirmation of: self].
15 (html submitButton) on: #register of: self]
16
17 renderReturningUserFormOn: html
18 html heading: ‘I am a returning user’ level: 2.
19 authenticationMessage ifNotNilDo: [:msg|
20 html heading: msg level: 3].
21 html form: [
22 html div: [
23 (html label) for: ‘login’; with: ‘Login’.
24 (html textInput) id: ‘login’; on: #login of: self].
25 html div: [
26 (html label) for: ‘password’; with: ‘Password’.
27 (html passwordInput) id: ‘password’; on: #password of: self].
28 (html submitButton) on: #authenticate of: self]

WATag’s #on:of: message is pretty powerful: it sends the specified message to the specified object. WATextInputTag also sends a mutator message to the object when doing form submissions. The returning user case calls #authenticate on self, which is implemented as follows:


1 authenticate
2 | user |
3 user := self userDatabase findWithLogin: login.
4 user ifNil: [
5 self failAuthentication: ‘Unable to authenticate using these credentials.’]
6 ifNotNil: [
7 (user isSamePassword: password)
8 ifTrue: [
9 authenticationMessage := nil.
10 self owner user: user.
11 authenticated := true]
12 ifFalse: [
13 self failAuthentication: ‘Invalid credentials for user.’]]

This goes in the actions protocol. When registering, we instead call #register:


1 register
2 | user |
3 user := self userDatabase findWithLogin: login.
4 user
5 ifNotNil: [
6 self failRegistration: ‘Login already taken’]
7 ifNil: [
8 password = passwordConfirmation
9 ifTrue: [
10 authenticationMessage := nil.
11 user := TodoUser new.
12 user login: login; password: password.
13 self userDatabase addUser: user.
14 self authenticate]
15 ifFalse: [
16 self failRegistration: ‘Password and confirmation do not match’]]

If the user doesn’t already exist (as identified through the login), and the password and the confirmation match, we register the new user and immediately authenticate him. Once the user is authenticated, we finally let the decorated component (the decorator’s owner) render itself:


1 renderOwnerOn: html
2 (html div) id: ‘auth-area’; with: [
3 (html anchor) id: ‘logout’; on: #logout of: self.
4 html heading: login capitalized, ’’’s Todo List’].
5 super renderOwnerOn: html

Put this in the rendering protocol. Here, we provide a logout link for authenticated users, as well as show who’s list this is. Then, we call our superclass’ #renderOwnerOn: to let the decorated component render itself. The logout link callsback the #logout method (in the actions protocol):


1 logout
2 self owner user: nil.
3 self clearAuthenticationInfo

Above, we called a couple of convenience methods, which obviously go in the convenience protocol:


1 clearAuthenticationInfo
2 login := nil.
3 password := nil.
4 passwordConfirmation := nil.
5 authenticated := false
6
7 failAuthentication: aMessage
8 authenticationMessage := aMessage.
9 password := nil.
10 passwordConfirmation := nil.
11
12 failRegistration: aMessage
13 registrationMessage := aMessage.
14 password := nil.
15 passwordConfirmation := nil.

Lastly, a couple of accessor methods:


1 login
2 ^ login
3
4 login: aLogin
5 login := aLogin
6
7 password
8 ^ password
9
10 password: aPassword
11 password := aPassword
12
13 passwordConfirmation
14 ^ passwordConfirmation
15
16 passwordConfirmation: aPassword
17 passwordConfirmation := aPassword
18
19 userDatabase
20 ^ TodoComponent userDatabase

TodoApp: our first real component

TodoApp is our root application component. It should thus register itself as a Seaside root. Let’s begin by declaring the class:


1 TodoComponent subclass: #TodoApp
2 instanceVariableNames: ‘viewers user’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-UI-Seaside’

Then, class side, in protocol testing, we implement #canBeRoot:


1 canBeRoot
2 ^ true

Still class side, in protocol initialization, add:


1 initialize
2 super initialize.
3 self registerAsApplication: #todo

This registers TodoApp as an application in Seaside, under /seaside/todo. Back on the instance side, in protocol initialization, we implement:


1 initialize
2 viewers := OrderedCollection new.
3 self addDecoration: (TodoAuthDecorator new).
4 self registerForBacktracking.

The root component knows that it needs authentication, so it immediately adds a decoration to itself. Seaside requires a component to register itself for backtracking if it’s collection of children will change during it’s lifecycle. Since the user will add and remove todos, our collection of TodoViewer instances will change.

Components render themselves, so put this in protocol rendering:


1 renderContentOn: html
2 html orderedList: [
3 self children do: [:each |
4 html listItem: [html render: each]]].
5 html paragraph: [
6 (html anchor) on: #newTodo of: self]

We render our collection of children, which is the viewers instance variable. Again, we have a callback when creating a new todo. In protocol call/answer, we add:


1 newTodo
2 | editor |
3 editor := TodoEditor new
4 todo: (Todo new);
5 yourself.
6 (self call: editor) ifNotNilDo: [:todo | self addTodo: todo]

Again, we have a couple of accessors which are pretty simple:


1 addTodo: aTodo
2 user addTodo: aTodo.
3 viewers add: (TodoViewer new todo: aTodo)
4
5 children
6 ^ viewers
7
8 user
9 ^ user
10
11 user: aUser
12 user := aUser.
13 viewers := OrderedCollection new.
14 user
15 ifNotNil: [
16 user todos do: [:each |
17 viewers add: (TodoViewer new todo: each)]]

#addTodo: adds the todo to the user instance, and also registers a new TodoViewer instance. #user: takes care of cleanup in case of logout (aUser isNil), and registers new TodoViewer instances when logging in (aUser notNil).

TodoViewer: A simple model viewer

This class is pretty simple. It’s job is to display a todo and allow it to be marked completed. Let’s declare the class:


1 TodoComponent subclass: #TodoViewer
2 instanceVariableNames: ‘todo’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-UI-Seaside’

We begin by rendering the component:


1 renderContentOn: html
2 (html paragraph)
3 class: (todo isCompleted ifTrue: [‘complete’]);
4 with: [
5 self renderDescriptionOn: html.
6 self renderCompletionStatusOn: html]

The call to #class: sets up an HTML class on the paragraph element, to help styling. A couple more rendering methods:


1 renderDescriptionOn: html
2 html html: todo description.
3 html space.
4
5 renderCompletionStatusOn: html
6 todo isCompleted
7 ifFalse: [
8 (html anchor) callback: [todo markCompleted]; with: ’It’’s done!‘]
9 ifTrue: [
10 html span: todo completedAt displayString]
11
12 style
13 ^’
14 .complete { color: #999; }
15 .complete span { font-size: smaller; }
16 }’

And the usual accessors:


1 todo
2 ^ todo
3
4 todo: anObject
5 todo := anObject

TodoEditor: Create or edit a Todo instance

The current version of the todo app doesn’t use TodoEditor to edit existing todos, but it does use it for creating new instances. As usual, let’s declare the class:


1 TodoComponent subclass: #TodoEditor
2 instanceVariableNames: ‘todo’
3 classVariableNames: ‘’
4 poolDictionaries: ’’
5 category: ‘Todo-UI-Seaside’

Next, we render the component:


1 renderContentOn: html
2 html form: [
3 html div: [
4 (html label) for: ‘description’; with: ‘Description’.
5 (html textInput) id: ‘description’; on: #description of: todo].
6 (html submitButton) on: #save of: self.
7 html space.
8 (html anchor) on: #cancel of: self]

Here is where things get interesting: #on:of: is used to set a callback on the todo instance. Seaside will call #description: of the todo instance to set the value on form post. We don’t need a temporary variable to hold the description in the component: the todo instance takes care of that.

In protocol call/answer, we add the following methods, which are called from #renderContentOn:


1 save
2 self answer: todo
3
4 cancel
5 self answer: nil

And the final accessors:


1 todo
2 ^ todo
3
4 todo: aTodo
5 todo := aTodo

Total line count: 233 lines, thanks to:


1 (Smalltalk allClasses
2 select:[:each | each category beginsWith: ‘Todo-’])
3 inject: 0 into: [:sum :each | sum + each linesOfCode]

Print this to get the total.

UPDATE 2007-10-15: I counted the lines initially by doing a fileOut and using standard command line tools: cat and wc. Seems I was slightly off.

Todos (pun intended)

There are a couple of things I’d like this todo app to be able to do:

  • Use an inline editor to change the description;
  • Use Scriptaculous to add a couple of effects;
  • Use Ajax to mark completed items;
  • Format dates and times;
  • Maybe set the user’s timezone and show dates and times using the user’s timezone;
  • Date/time formats per-user;
  • Enhance security by not storing plain-text versions of passwords in user instances.

This is a toy project. I might never again touch this application.

Recap

One point I’d like to be clear on:

Don’t do that!

I store an unencrypted copy of the password in TodoUser instances. That should never be done. As many others before me, I tried to simplify the code as much as possible. There probably lurks a Password model object in there.

Also, I am no expert on Smalltalk, Squeak or Seaside. There are probably a couple of things I could have done differently, and I hope some people out there might be interested in helping me learn more about Seaside.

I hope you enjoyed this article. There might be more of these coming later. Send me E-Mail at francois@teksol.info or write a comment.

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on