Aida/Web Tutorial

1. Introduction

To understand how to create web applications with Aida and to start programming on your own, here is a short tutorial which guides you through the main steps in creating an on-line address book.

This tutorial is prepared for Squeak and Pharo but it can be used on other Smalltalks as well (only class declarations are different). It requires a basic knowledge of a chosen Smalltalk and how to use its tools.

First, be sure that after installation of Aida you start it. Open the Workspace tool and evaluate this code:

SwazooServer stop; start.

Then open a page http://localhost:8888 and login with username admin and password password.

Now we are ready to start preparing a domain model.

2. Preparing a domain model

Aida uses a strict separation of domain objects from their visual representation in a MVC like way. Therefore we need to create some domain model objects first. In our example we'll make a domain model for our example address book, instantiate it and fill it with a few example addresses:

  • add new package Aidaweb-Tutorial and there create a new class ADemoAddressBook with instance variable addresses:
Object subclass: #'ADemoAddressBook'
  instanceVariableNames: 'addresses '
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Aidaweb-Tutorial'
  • create an initialize method, accessors for the ADemoAddressBook instvar addresses and addition method, which will also set a back reference from address to its parent, the address book. This back reference will come handy later for navigation from address page back to address book. Here are those three methods (write them without a class name and >> in first line):
 ADemoAddressBook>>initAddresses 
  addresses := OrderedCollection new
ADemoAddressBook>>addresses addresses isNil ifTrue: [self initAddresses]. ^addresses
ADemoAddressBook>>addAddress: aDemoAddress self addresses add: aDemoAddress. aDemoAddress parent: self
  • create new class ADemoAddress with instance variables: name surname phone email
Object subclass: #'ADemoAddress'
  instanceVariableNames: 'parent firstName lastName phone email '
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Aidaweb-Tutorial'
  • Create accessing and changing methods (mutators) for all instance variables in class ADemoAddress. You can do this automatically in the browser or type the following methods manually:
 ADemoAddress>>firstName 
  ^firstName 

ADemoAddress>>firstName: aString 
  firstName := aString 

ADemoAddress>>lastName 
  ^lastName 

ADemoAddress>>lastName: aString 
  lastName := aString 

ADemoAddress>>phone 
  ^phone 

ADemoAddress>>phone: aString 
  phone := aString 

ADemoAddress>>email 
  ^email 

ADemoAddress>>email: aString 
  email := aString 

ADemoAddress>>parent 
  ^parent

ADemoAddress>>parent: aDemoAddressBook 
  parent := aDemoAddressBook
  • init instvars to empty string with a method initialize:
ADemoAddress>>initialize 
  self firstName: ''; lastName: ''; phone: ''; email: ''.
  • implement a class method new with initialization:
ADemoAddress Class>>new 
  ^super new initialize
  • add a class method for fast ADemoAddress creation:
 ADemoAddress Class>>newName: aName surname: aSurname phone: aPhone email: anEmail
  ^self new 
    firstName: aName; lastName: aSurname; phone: aPhone; email: anEmail
  • Now let we make a live address book and prefill it with some addresses. In a Workspace we make an instance of ADemoAddressBook and fill it with a few example ADemoAddresses by evaluating this code:
book := ADemoAddressBook new.
book
  addAddress: (ADemoAddress newName: 'Sebastjan' surname: 'Dormir' 
    phone: '01/514 33 66' email: 'sebastjan@something.si');
  addAddress: (ADemoAddress newName: 'John' surname: 'Newton' 
    phone: '05/555 77 66' email: 'john@something.si');
  addAddress: (ADemoAddress newName: 'Elizabeth' surname: 'Schneider' 
    phone: '03/561 23 12' email: 'elizabeth@something.at').

3. Registering a root domain object

After our ADemoAddressBook is prepared, we register its URL in Aida, so that it is accessible from web. In our Workspace we evaluate:

AIDASite default register: book onUrl: '/addressbook'

Now if we try to load http://localhost:8888/addressbook we will get a message like this:

Cannot find aWebApplication for object aADemoAddressBook

which is fine for now. It tells us that the URL was registered properly, but still we need to make some work before making the objects visible from the web, which is what we will see on the next section.

You need to register only a root object of your domain model. URL links to all objects reachable from the root will be automatic.

For convenience 'AIDASite default' will return a default website in case there are more than one site in this image. After installation a site named 'aidademo' is prepared in advance and set as default

4. Creating presentation classes (Apps)

To represent our domain objects as web pages we need to create a web application (presentation) class for every domain object we would like to show. In our case the domain classes are ADemoAddressBook and ADemoAddress. This is simple; just subclass WebApplication with a new class named according to the formula: 'domain class name'+App. In our case we need two Apps: ADemoAddressBookApp and ADemoAddressApp:

WebApplication subclass: #'ADemoAddressBookApp'
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Aidaweb-Tutorial'

WebApplication subclass: #'ADemoAddressApp'
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''
  category: 'Aidaweb-Tutorial'

4.1. View methods

We start by making our first 'view' method, that is, a method which builds a web page to represent one of the views of our objects. View methods are named according to the formula: view+'View name'. For instance: for view 'summary' we make a method viewSummary. However every App should also have a default view 'main' and therefore a view method viewMain. Let's write one for our ADemoAddressBookApp using the protocol 'printing':

ADemoAddressBookApp>>viewMain
  | e |
  e := WebElement new.
  e addTextH1: 'Address book'.
  self style pageFrameWith: e title: 'Address book'

In this case we created a local instance of WebElement, which is both a subclass of all other elements of a web page (such as WebText, WebLink etc.) and a composite, so that we can add another WebElements to it. We can also do this in a longer way by instantiating a new element and then adding to it, for example:

e add: (WebText text: 'Address book' attributes: #bold)

The shorter, more direct convenience methods can all be found in the WebElement adding protocols (adding text, images, links, form elements, ajax components, other).

The last line of our example adds our element into a page and frames it with a standard header and navigation bar on the left. This layout is defined is a so called style class, in our case DefaultWebStyle. That why you call as self 'style...'. Try to change that to add directly to self (self add: e)!

The last line of our example adds our element to a page which already contains a standard header on the top and a navigation bar on the left. A so called style class defines the layout of this page (the default class is DefaultWebStyle). In order to apply the used page style to our new element we have to call self style pageFrameWith:title:.

Now we can open our first page in browser: http://localhost:8888/addressbook .

If not yet, you need to login as admin with password: 'password', otherwise you wont be able to see your page. As admin you have the right to see all pages by default, while as a guest you cannot (although there are security settings which allow this).

4.2. Text

Now lets write out all the addresses in our address book main view in a class ADemoAddressBookApp:

ADemoAddressBookApp>>viewMain
  | e |
  e := WebElement new.
  e addTextH1: 'Address book'.
  self observee addresses do: [:each |
    e addTextBold: each firstName, ' ', each lastName.
    e addText: ', ', each phone, ', ', each email.
    e addBreak].
  self style pageFrameWith: e title: 'Address book'

Notice the message self observee . This way we ask our App to return a reference to the domain object it represents (do you remember the observer pattern and MVC?). The method addText: is used to add text to our page, which can include HTML tags too. Finally, the method addBreak creates a new line.

But this page is ugly! Let's put data into a nice table:

4.3. Tables

Tables in Aida are built in a convenient way, which shortens the work otherwise needed for web tables:

ADemoAddressBookApp>>viewMain
  | e |
  e := WebElement new.
  e addTextH1: 'Address book'.
  e table border: 1.
  self observee addresses do: [:each |
    e cell addText: each firstName. e newCell addText: each lastName.
    e newCell addText: each phone. 
    e newCell align: #right; addText: each email.
    e newRow].
  self style pageFrameWith: e title: 'Address book'

Every element has a dormant table which is activated when we first start to use table accessing/manipulation methods (table, newTable, cell, newCell, row, newRow). By simply sequencing those methods we can do everything with tables. We can even nest tables that way (remember, a table is also a web element and therefore can have a table inside!). Let's put name and surname in a nested table:

e cell cell addText: each firstName. 
e cell newCell addText: each lastName.

4.4. CSS styles

Let's make this table even nicer. A better way is to use CSS (Cascaded Style Sheet) to separate design from content. There are already a lot of styling methods included in the class WebStyle (see methods css in the protocol styles-screen). We will reuse already defined styles in a method css24WebGrid for tables. Let's change our table line (e table border: 1) to:

e table class: #webGrid.

Now our table has a colored background and the font is smaller. Style methods in WebStyle are composed together by alphabetic order into one CSS page, which can be seen at http://localhost:8888/screen.css. Alphabetic order is important for CSS and that's the reason that css methods have numbers in their names to maintain order. Every method which starts with 'css' will be included in our CSS declaration. We can make a separate CSS for printing too, by naming methods to start as cssPrint .

To make your own style (skin), just subclass WebStyle and add or override css methods. Then change the style in AIDASite:

(AIDASite named: 'test') styleClass: 'MyWebStyle'.

Note: changing the AIDASites instvar style to nil will lazily instantiate it to a new style class at the first use.

5. Linking objects together

Because our domain objects are linked together with object references, our web pages should be too. For example, our ADemoAddressBook holds references to all ADemoAddresses, and because we will represent each address with a separated web page, we need to make an url link from the ADemoAddressBook page to it.

First let's start by preparing a main view for addresses in ADemoAddressApp:

ADemoAddressApp>>viewMain
  | e |
  e := WebElement new.
  e addTextH1: 'Address'.
  e table class: #webGrid.
  e cell addText: 'first name: '. e newCell addTextBold: self observee firstName. e newRow.
  e cell addText: 'last name: '. e newCell addTextBold: self observee lastName. e newRow.
  e cell addText: 'phone: '. e newCell addTextBold: self observee phone. e newRow.
  e cell addText: 'email: '. e newCell addTextBold: self observee email.
  self style pageFrameWith: e title: 'Address'

5.1. Preferred URLs

We would like to have nice looking URLs for our addresses, something like /address/newton and not automatically generated ones, like /object/o4899057, which is what is created for you as a default by Aida. To suggest to Aida what is a better URL, implement in your domain class the method preferredUrl. Let's do that for ADemoAddress :

ADemoAddress>>preferredUrl
^'/address/', self lastName

URL suggesting will be accepted if the URL is unique, otherwise it will degrade to an automatically generated one. Note also, that you need to implement preferredUrl method before you first link to that object, otherwise you'll already have an automatically generated URL. To change that later, use URLResolver>>changeToPreferredUrl: method.

Now we are ready to make links to our addresses:

5.2. URL links

URL links are made automatically by simply pointing to a domain object we'd like to show. In our case we will point to each of the addresses in address book. We will use the #addLinkTo:text: method from WebElement to change surname into a link, for example:

 e addLinkTo: address text: address lastName.

Let's update our viewMain in the ADemoAddressBookApp:

ADemoAddressBookApp>>viewMain
  | e |
  e := WebElement new.
  e addTextH1: 'Address book'.
  e table class: #webGrid.
  self observee addresses do: [:each |
    e cell addText: each firstName.
    e newCell addLinkTo: each text: each lastName.
    e newCell addText: each phone.
    e newCell align: #right; addText: each email.
    e newRow].
  self style pageFrameWith: e title: 'Address book'

Now all the last names will be changed to URL links. Check out how they look in your browser. Our URL suggestions obviously work. If we click on one of them, we can check if each address is properly shown as a web page as we wrote in the AddressApp viewMain.

6. Forms

It's time to add new entries in our AddressBook. For that, we have to create a form, process the inputs and add the new entry to our address book.
Let we add a button on the main view of the address book:

ADemoAddressBookApp>>viewMain
  | e |
  e := WebElement new.
  e addTextH1: 'Address book'.
  e table class: #webGrid.
  self observee addresses do: [:each |
    e cell addText: each firstName.
    e newCell addLinkTo: each text: each lastName.
    e newCell addText: each phone.
    e newCell align: #right; addText: each email.
    e newRow].
  e addBreak.
  e addButtonText: 'Add a new entry'.
  self style pageFrameWith: e title: 'Address book'

And create an action method which will be called when above button will be pressed. Action buttons are named similarly to view ones, so for a default action on main view we create a method named actionMain.

ADemoAddressBookApp>>actionMain
  | new |
  new := ADemoAddress new.
  new parent: self observee.
  self redirectTo: new view: #add

This action will therefore create a new address and redirect us to its view add. We set parent back reference in advance for later to know to whom add this new address. More a bit later.

In ADemoAddressApp the viewAdd method will be called. Let we implement this method:

ADemoAddressApp>>viewAdd
  ^self viewEdit

Why that? Because we need a form for fill the data of a new address but we need actually the same form later to edit too. We can therefore reuse an edit view for adding as well.

Let we now create this viewEdit method in the ADemoAddressApp class:

ADemoAddressApp>>viewEdit
  | e |
  e := WebElement new.
  e addTextH1: 'Add/edit the address'.
  e cell addText: 'First name:' ; addBreak.
  e cell addInputFieldAspect: #firstName for: self observee.
  e newRow.
  e cell addText: 'Last name:' ; addBreak.
  e cell addInputFieldAspect: #lastName for: self observee.
  e newRow. e cell addText: 'Phone:' ; addBreak.
  e cell addInputFieldAspect: #phone for: self observee.
  e newRow.
  e cell addText: 'Email:' ; addBreak.
  e cell addInputFieldAspect: #email for: self observee.
  e newRow.
  e cell addButtonText: (self view = #add ifTrue: ['Add'] ifFalse: ['Save']).
  self style pageFrameWith: e title: 'Add/edit the address'.

See how the button text is set according the current view, which enable reusing of this method for both adding and editing the address.

When the button is pressed, Aida will call the method actionAdd (by default, the submit button of a form call the method actionNameOfTheView). Let's have a look:

actionAdd
  self observee parent addAddress: self observee.
  self site urlResolver changeToPreferredUrl: self observee.
  self redirectToView: #main.

First line seems a bit complicated, so let we look at it. Remember that we are on the address App right now and here the Add button was pressed. But we need to add this new address to address book, which is a parent of address. That's why we have self observee parent addAddress: self observee.

Second line change the current URL of our address to preferred one, because we just now added enough information for URL to be a correct one.

Let we now finish with enabling editing our address simply by adding and Edit button to main view of ADemoAddressApp:

viewMain
  ....
  e addBreak.
  e addButtonText: 'Edit'.
  self style pageFrameWith: e title: 'Address'

And action method for main view:

actionMain
  self redirectToView: #edit

And finally an action method for edit view, to jump back to main view after posting edited form values:

actionEdit
  self redirectToView: #main

For more informations about the Forms in Aida, have a look at the doc: http://www.aidaweb.si/forms.html or browse the source code.

7. Enhance the table

Aida makes JavaScript/Ajax scripting really easy. No need to know JavaScript, Aida does everything for you.
We'll improve our AddressBook table by adding two features: sorting and filtering
Here's the complete #viewMain method in ADemoAddressBookApp:

ADemoAddressBookApp>>viewMain
  | e entryList |
  e := WebElement new.
  e addTextH1: 'Address Book'.
  entryList := WebGrid new.
  entryList 
    columnNames: #('' 'Name' 'Last name' 'Phone' 'Email');
    columnFilters: #(nil true true nil nil);
    columnAspects: #(nil #firstName #lastName #phone #email);
    setNumbering;
    sortOn: 2;
    collection: self observee addresses;
    column: 3 addBlock: [ :each |
      WebElement new addLinkTo: each text: each lastName].
  e add: entryList.
  e addBreak.
  e addButtonText: 'Add an entry'.
  self style pageFrameWith: e title: 'Address book'

Do you see the difference?
We've created a WebGrid object and gave it all the informations it needs to work properly .

  • columnNames: is used to give a name to each column.
  • setNumbering simply tells the webGrid to give each line a number (which will be in the 1st column)
  • columnFilters: tells the WebGrid on which column we want to use a filter. In this case, only the 'Surname' and 'Name' column will have a filter box.
  • sortOn: sets on which column the initial sorting has to be done
  • collection: sets the collection that will be displayed by the webGrid. We use the #observee to retrieve our addressBook.
  • columnAspects: tells which method has to be called for each column on very object of the collection.
  • column: addBlock: The specified column will be rendered with the given block (which must return a WebElement).

That's it! Refresh your browser and enjoy your brand new table.

8. Conclusion

This concludes a tutorial for fundamental Aida features and for "traditional" Aida web apps. To continue with nowadays highly ajaxified HTML5 apps, install and study an Aida Todo Example.