Login

Terse Guide to Seaside

Learning Seaside

Seaside isn't as hard as it might initially seem, it's just different. There are only a few essential classes you need to learn to use most of it so here's a quick lesson.

To get going, you'll need to understand WAComponent, WARenderCanvas, WATask, and WASession. You'll also need to understand that Seaside is a framework, not an API, so you'll work with it by subclassing and extending these core classes.

WAComponent

WAComponent is the main class you'll be working with in Seaside. A component represents both the concept of "page" and "user control". If you're coming from another framework, consider the word "page" and "component" to be indistinguishable.

To allow a component to be configured as the root of an application in the configuration UI (/seaside/config), you'll need to override #canBeRoot on the components class side.

FooComponent class>>canBeRoot
    ^true

This will make the component show up as an option in the root component drop down in the configuration editor.

A more direct, and I'd say preferable route, would be to create your site programmatically by creating an #initialize method on your root class like so...

FooComponent class>>initialize
    "self initialize"
    | app |
    app := self registerAsApplication: #foo.
    app libraries add: SULibrary.
    app preferenceAt: #sessionClass put: FooSession

This sets up a dispatcher at /seaside/foo with the Scriptaculous library (SULibrary) and a custom session class that might contain things like the current user or current database session. You can then highlight the comment "self initialize" and run it to create your site. This has the additional advantage of automatically setting up your site in any new image when you load your package into it and also allowing you to programmatically recreate your site on demand. This comes in very handy when upgrading to newer versions of Seaside which sometimes requires recreating your sites.

Rendering

Seaside can render two kinds of things, views, WAComponent subclasses via the overridden #renderContentOn:, or any other non UI object via the overridden #renderOn: method.

Both #renderContentOn: and #renderOn: are framework methods, you override them so the framework can call them. Never call these methods yourself in an attempt to render an object and never just add a #renderContentOn: to any random object thinking it'll just work, it won't. #renderContentOn: only works in WAComponent subclasses.

Views

For those who like the model view controller style, you'll want to keep all of your rendering code in WAComponent subclasses representing your views. To create a view in Seaside, you subclass WAComponent, override the #renderContentOn: method, and start writing HTML using the render canvas, which is passed in as an argument to #renderContentOn by the framework when the component is rendered.

renderContentOn: html
    html div: 'Hello World'

WAComponents have a collection, called #children, consisting of other WAComponents. This serves as the user interface's control tree. A component is built from itself and optionally, nested subcomponents or models, allowing component composition. This is where people start running into trouble and meet the dreaded "Components not found while processing callbacks" error. You must return all visible subcomponents in a collection from the #children accessor that you have to override...

children
    ^ { header. currentBody. footer }

renderContentOn: html
    html div id: #header; with: [html render: header].
    html div id: #body; with: [html render: currentBody].
    html div id: #footer; with: [html render: footer]
Models

For those who just want their models to render themselves and don't need multiple visual representations of the same model, you can skip the WAComponent and just override #renderOn: in your model.

Rendering Mistakes

People often overlook this and do it wrong leading to all sorts of weird errors. You must know how to properly render subcomponents, don't do this...

renderContentOn: html
    foo renderOn: html

And don't do this...

renderContentOn: html
    foo renderContentOn: html

There is only one correct way to render another object, regardless of what kind of object it is...

renderContentOn: html
    html render: foo

It's a good idea to create a subclass of WAComponent once at the project level, and write the rest of the components in that project, using your custom component as the superclass. This gives you a place to push up component-type things you want to apply project-wide, and override defaults project-wide, like a different render canvas.

Using #call: and #answer:

In traditional web frameworks, you move between pages either by building forms the user posts, or anchor tags and server-side redirects containing parameters that can be parsed from the request by the other page. This means any page is a potential entry point and that no parameters passed to it can be trusted and must be validated and parsed sanely because the user can modify them by editing the URL in their browser. It also means you can only pass simple parameters like strings and numbers that can fit in a URL.

Seaside works differently. In Seaside components are real objects and they can #call: and #answer: to each other allowing you to pass any object as a parameter or result between them. Both methods are meant to be used during the callback phase, i.e. your controller methods. For example, you may click and edit the link in a row of results that allows you to edit some object...

editFoo: aFoo
    self call: (FooEditor on: aFoo)

renderContentOn: html
    foos do: [:each | 
        html div class: #fooRow; with:
            [html anchor 
                callback:[ self editFoo: each ]; 
                text: 'Edit'.
             html text: foo description ]]

One component should never call or answer another component during the rendering phase. Take note above that the #call: in the method #editFoo: is passed in a callback block to the anchor in #renderContentOn:, it will not be invoked during render time, it will only be invoked when the user clicks the anchor.

This is a simple case, a more complex case might include some sort of workflow where components return results...

orderFoo: aFoo
    | customer address order |
    customer := (self call: CustomerForm new) ifNil: [ ^self ].
    address := (self call: AddressForm new) ifNil: [ ^self ].
    order := (self call: (FooOrder foo: aFoo customer: customer shipTo: address)) ifNil: [ ^self ].
    self sendEmailConfirmationFor: order

In each case here, each called component calls #answer: with either a result or nil if the user presses cancel. The entire workflow for a multi-page order process is represented here, with each component result being used later in the other steps or bailing if the user canceled.

By attaching callbacks directly to user actions, you never have to worry about how to represent your state in the URL or parsing and validating the input and re-fetching your models from the database on subsequent steps of the workflow. Every page in a Seaside application is not a valid entry point so users can't arbitrarily hack the URL to navigate to a page you didn't intend for them to see.

WARenderCanvas

WARenderCanvas contains the API for creating HTML. It's currently the default canvas, but you can override it to return your own customized subclass if you like, the purpose of which would be to extend the default canvas with new methods to abstract common things you might want to do often in your application. For example, you may not like the default implementation of a Date input, maybe you require a timezone-aware Date input for accurate scheduling. You can write your own date tag and override the #dateInput method in your custom canvas to return your application-specific version of a #dateInput. You decide; you can customize to suit any need, extending the framework's canvas to speak in the language of your application.

rendererClass
    ^ MyCoolRenderCanvas

The WARenderCanvas is a starting point for understanding how to write code in #renderContentOn:, when you get confused, just look at the canvas and find the method you want, see which tag object it creates, then look on the tag class to see what attributes are valid for it.

I've learned just about everything I know about Seaside using exactly this method. Documentation is nice, but it isn't always available and you really can easily find what you need just by looking at the tag classes directly through the entry point of the canvas, a simple builder pattern.

WATask

WATask is a special subclass of WAComponent used to do workflow. WATask is used just like a component, except you override #go instead of #renderConentOn: and you must #call: another component because a task has no UI and thus is not a renderable object.

Tasks essentially coordinate the display of other components, allowing you to write very simple and elegant code by calling, displaying, and getting answers from components, which you can use to determine what step comes next.

When one component calls another component, the callee replaces the caller in the UI. If the caller was a subcomponent of another component, then the callee appears to takes its place in the composite component. All components continue to exist, the caller is simply not displayed until the callee answers. This allows you to easily set up a parent component to mediate the display of its children, this comes in very handy when a complex workflow is involved.

WASession

WASession is optional, you don't strictly need to subclass it and use it, but if you have data you want available globally within the scope of the current session, it is often convenient to create a custom session class with accessors for that data. The session serves as a context object available to all of your components. This is where you'd put a database connection, some configuration data, or a current user. A Seaside session is inherently single-threaded, so you don't have to worry about concurrency or locking unless you explicitly start forking stuff.

Whatever session object you use, it's available on every component via the #session accessor. From within any component, you can say "self session" and have access to your session.

WASession is also where you can find other things you'll eventually want access to, like the current request. You can read in query string values like so...

self fieldsAt: #someKey

WASession is also where redirectTo: is located, something you'll likely need.

self session redirectTo: 'http://www.google.com'



Danger Will Robinson!

  • Seaside uses thread local variables to store the current session, so if during processing you #fork a block to do some work on another thread, you'd better pass that block the data it needs when called because once launched on the other thread, it will no longer have access to the current session.
  • Unless you're a masochist, don't try creating UI components on a background thread because Seaside expects to have access to the current session when WAComponent subclasses are instantiated.
  • Don't put workflow logic in render methods or change the state of your component in them, a render method should be able to be called many times without affecting the component or changing its state. If the user refreshes their page, your component will be rendered again in its current state, so don't go mucking around with state during render. Simple logic like deciding if something should be rendered or not, or rendering something in a loop is fine, but don't be changing inst var values or calling other components at render time.
  • Don't call components directly from the render methods of other components, always make sure any #call:'s are inside #callback: blocks, this bites every newbie for some reason.
  • When a page posts back, any form data will be used to update the components state, prior to the processing of any callbacks. You don't need to access the request directly, you just use your instance variables which Seaside has kindly updated for you.
  • Bind form controls directly to accessors either on the component, or on the components model, using the #on:of: shortcut, this saves much typing and works with most controls, and helps keep command code factored into separate methods rather than inline in some render method where it can't be easily found or reused. This is merely a shortcut for attaching a callback: block to the control, something to ease rapid prototyping when you're still figuring things out.
  • If a component has child controls, you must override #children, and return a collection containing all current child controls or you will have issues. You must create those child controls either in the parent components #initialize method, or during a callback when it's safe to alter a component's state. Do not create children lazily during the render phase.
  • If the list of child controls is dynamic or you want the back button to actually revert your server-side state to match what's in the user's browser cache, be sure to override #states and return a collection of objects that need backtracking, possibly including self.

Is that all?

No, there's much more to learn about Seaside, I left out plenty, but these are the basics and should get you going and productive fairly quickly and within a short time you should understand why Seaside is game-changing; this isn't your last web framework, but it just might be your last web framework. 15 years later and we have frameworks like React, little more than a poor imitation of Seaside's component-based model.

Simple Example of a Login Process and Component

I cheat a little by calling #inform:, which is a built-in generic dialog for displaying a message to the user and getting an OK.

SeasideLoginTask class>>initialize
    "sets up app at http://localhost/seaside/loginSample"
    self registerAsApplication: #loginSample

SeasideLoginTask>>go
    | user |
    user := self call: SeasideLogin new.
    user ifNil: [self inform: 'Unknown user or password, please try again!']
        ifNotNil: 
            [self inform: 'Congratulations, you are in!'.
            self session redirectTo: 'http://onsmalltalk.com']

and...

SeasideLogin>>userName
    ^ userName

SeasideLogin>>userName: aName
    userName := aName

SeasideLogin>>password
    ^ password

SeasideLogin>>password: aPassword
    password := aPassword

SeasideLogin>>login
    (userName = 'seaside' and: [password = 'rocks']) 
        ifTrue: [ self answer ]
        ifFalse: [ self answer: nil ]

SeasideLogin>>renderContentOn: html 
    html form: 
        [html heading level3; with: 'User Name:'.
        html textInput on: #userName of: self.
        html heading level3; with: 'Password:'.
        html passwordInput on: #password of: self.
        html break; submitButton on: #login of: self]

Comments (automatically disabled after 1 year)

Lidell 6363 days ago

Great explanations Ramon! This helps clear up a few murky notions I've had about Seaside. It's really great when somebody can explain things so clearly and succinctly. At the risk of imposing, can you illustrate these concepts with a few working examples? There's nothing like working code to nail a point home.

Here's a simple Seaside application: present a login page that asks the user to enter a name and a password to authenticate. If the password is correct, the application presents a page that reads: 'Congratulations, you are in'. If the password is incorrect, the application displays a page that reads: 'Unknown user or password. Please try again.'

A typical web app solution would have the programmer design several web pages each with wired-in logic to conform to the control flow of logging in correctly or trying again. Would the Seaside solution look like a subroutine call? I think this example can really illustrate Seaside's strong suit.

Ramon Leon 6363 days ago

There you go...

Lidell 6363 days ago

Much obliged, Ramon! That's a great demo of how Seaside truly rocks!

Carl Gundel 6362 days ago

This is great, thanks! Do you have any experience with styles? Everything I do with CSS and Seaside ends up being a hack. I can't seem to properly set styles for individual subcomponents of my Seaside app. Everything ends up in the root class.

Ramon Leon 6362 days ago

Coming soon...

[...] Terse Guide to Seaside | OnSmalltalk: A Squeak, Smalltalk, Seaside, Web Development Blog Ramon serves up the core of Seaside with a few notes. (tags: howto seaside smalltalk tutorial) [...]

Alan Meier 6340 days ago

Hi there,

I would be interested in knowing whether there are any commercial websites using seaside as a framework. I searched teh net but could not find a single one.

I am interested in doing something with seaside but am reticent that it may not be scalable enough.

regards Alan

Ramon Leon 6339 days ago

I'm not familiar with many, I know Loop Aero uses it, DabbleDb uses it (for their application, I think the dabble site itself is done with Wordpress), and I'll be using it at my company shortly for a site that does about 30,000 uniques a day scaled across a 3 server farm. I think it's as scalable as any other session based web server, it's a matter of how much hardware you have for it, how much memory the sessions use, and how long the session timeout is.

Seaside is great for the complex application parts of a site, but you can blend it into an existing site by mapping only those areas to Seaside while keeping the static content served up by Apache or some other framework you're running.

Marc 6270 days ago

"Seaside is great for the complex application parts of a site, but you can blend it into an existing site by mapping only those areas to Seaside while keeping the static content served up by Apache or some other framework you're running."

Hi Ramon, I would like to know how you can blend classic pages and seaside code. I didn't find much information about it. I read Seaside Parasol can host this type of solution, but I don't see how it works. For the moment I just upload my squeak image to seasidehosting.st. That works but I would prefer to use Seaside for only a part of the website, like in dabbledb. If you can help me, thanks a lot in advance! Marc

Ramon Leon 6270 days ago

Marc, see my post Running Seaside Apache and IIS on WindowsXP where I explain how to configure Apache to do this.

Eric Hochmeister 6102 days ago

Why do you need the repeat block in the #go method? I tried out of curiousity to see what would happen if I removed it and it behaved exactly the same. Or am I missing something? I don't exactly understand what is happening, other than it seems to be re-running the WATask from the start. I put an #inform at the end, and it seems to progress on to that #inform, but once it hits the end of the #go method it just starts from the beginning again.

go | user | user := self call: LoginTestLogin new. user ifNil: [self inform: 'Unknown user or password, please try again!'] ifNotNil: [self inform: 'Congratulations, you are in!'. self session redirectTo: 'http://onsmalltalk.com'] self inform:'Booya'.

Ramon Leon 6102 days ago

Ha, good catch, it's totally unnecessary. I actually know that now, but at the time I wrote this I didn't. I think I was thinking that a task "must" render a UI component not realizing that if it didn't it'd just re-run itself.

[...] http://onsmalltalk.com/programming/smalltalk/terse-guide-to-seaside/ Muy claro en la presentaciĆ³n general de la arquitectura y uso de las clases principales de Seaside [...] ]

about me|good books|popular posts|atom|rss