In this tutorial, you will develop a simple contact book application with a web interface. It consists of a couple of classes for the model and the UI of the application. Figure 44.1 shows the resulting application. As you see it is simple but covers many aspects of defining and deploying a web applications.
You will develop the web interface using Seaside (http://ww.seaside.st). Seaside is a powerful web framework for developing highly dynamic and complex web application. For more information you can read the book: 'Dynamic Web Development with Seaside' which is freely available at http://book.seaside.st. In this little tutorial we will only use some simple server side Seaside behavior. We will use the Twitter Bootstrap library that is fully integrated to Seaside.
Note that the presented solution is often simple and we will list ideas of further improvements that you could add. In addition, it is worth to know that Pharo developers often use object descriptions (as done via the Magritte framework) to generate new web components instead of manually developing them.
While we love coding test first and coding in the debugger (since it let us go much faster), in this tutorial we will not follow such style because it requires a lot of text to mention what to do with the user interface of the Pharo tools. We decided to take a neutral stand point and to let you decide if you code first the tests, if you prefer to go slowly or not. This tutorial presents the mandatory information so that you can get it done. Program it the way you want.
In this tutorial, we use the Seaside web framework to define the views of our application. Seaside must be loaded in the image to start using it. You have several ways to obtain Seaside
The contact book is composed of two model classes: Contact
and ContactBook
. Let us start with the Contact
class.
A contact has two instance variables fullname
and email
, both strings. Create the Contact
class with its instance variables:
It should be possible to create a new contact by executing the following code:
It means that we send a message (newNamed:email:
) to the class Contact
itself and that it should return a Contact
instance.
Add the newNamed:email:
class method in the instance creation
protocol of the Contact
class. To define a class method, pay attention that you should browse the class side of the Contact
class.
You can now define a test (in the tests
protocol) to make sure that you can create an instance and that it contains the right information.
For this test to work you need to define the following methods (in the accessing
protocol).
Run the test to make sure it passes. It is a good idea to version your code and save the image.
You can add some extra logic to make sure that the name is not surrounded by space. In addition handling whether you take care of note about lowercase and uppercase should be addressed. You can use messages such as asLowercase
to, for example, store only lowercased strings.
If you inspect or print the contact
variable created above, you will see that the string representation of this Contact
instance is "a Contact"
which says nothing about the contact itself. This is problematic when debugging and developers typically appreciate nicer string representations. You can change that by overriding the method Object>>printOn:
(in the printing
protocol) in the Contact
class and sending several nextPutAll:
messages to the stream argument as follows:
The string representation of the above contact
variable will then be:
We hope you didn't forget to add a comment to the Contact
class. If you did, here is a possible one:
Note that in a real application, it might be better to use an Email
class to represent a contact's email address.
Save your image and save the code using the version control browser (Monticello Browser).
A contact book contains a collection of Contact
instances:
To initialize the contacts
variable to an empty collection, you can either define an initialize
method (that is automatically invoked at instance-creation time) or use lazy initialization. The following code uses lazy initialization. Add this method in the accessing
protocol.
Pay attention that with lazy initialization, you should systematically use accessors else you could access to a variable not well-initialized.
It should be possible to add and remove contacts from the collection. Add the necessary methods in the action
protocol:
Define some tests to cover the addition and removal of contacts.
You will see in particular that the definition for removeContact:
is not that robust when we want to remove a contact that is not in the collection: Removing an unexisting contact raises an error. Change the definition of the removeContact:
method (check other remove: methods in collection) and define a test that covers this particular aspect.
To simplify further development, we define a default contact book with predefined contacts inside. We do this by adding the method createDefault
as a class method to the 'default instance' protocol of ContactBook class
:
Don't forget to comment the class and to save your image and version your code.
Now that we have the model, we need a web view. We will use Seaside for that. Seaside is a component framework: Seaside web applications are built by aggregating components. Typically, an application consists of a top-level component delegating parts of its rendering to sub-components. Let us define a first simple component.
Our simple application consists of a top-level component represented by the WAContactBook
class, subclass of WAComponent
. Create the WAContactBook
class with a contactBook
instance variable:
Every Seaside component class must override the renderContentOn:
method to specify how a component is rendered. Define this method in the rendering
protocol:
The method argument html
acts as a canvas that can emit adequate HTML code. Above code asks the heading brush to the canvas and uses it to emit this HTML code: <h1>My Contact Book</h1>
. Seaside abstracts you from the details of the syntax of the HTML expressions.
It is not a good style to generate component UI using directly HTML commands representing styles (table, section...). Usually developers emit tags that map with css class tags provided by an application designer. We will show you that later.
The next step is to register the WAContactBook
class to the /contacts
URL path. This way you will be able to reach the application at http://localhost:8080/contacts. Do this by implementing an initialize
class method in WAContactBook class
(class initialization
protocol):
An initialize
class method is executed when the class is loaded in memory. Because our class is already loaded, we should execute it once manually. Write and execute this code, as method comment or in the Playground for example:
The last step before getting something on the web browser is to start the web server. Open the Seaside control panel
tool (open the Pharo menu and go to the Tools sub-menu). Now, add a ZnZincServerAdaptor
by right-clicking on the top pane and choosing Add adaptor...
. When prompted, choose a port, for example 8080 and press Start
.
Open your favorite web browser on http://localhost:8080/contacts and you should see something similar to Figure 44.2. Currently, the title of the web page is "Seaside", as we can see it in the web browser window title and the tab title. This can be changed by overriding the updateRoot:
method (in the updating
protocol):
You can refresh the page in the web browser to see the result.
Now we define some methods to access to the model from the view. We define them in the accessing
protocol:
In the 'iterating' protocol add the method contactsDo:
This contactsDo:
method is not as useless as it might seem. This method hides the existence of a contactBook
collection and could be useful later to replace the collection by a database.
Below the title, we want a table containing the contacts of the contact book. For this, we need to change the renderContentOn:
method and add a few new messages. We will decompose the behavior in several methods to facilitate understanding.
We just used a new method named renderContactsOn:
(all rendering methods should be put in the rendering
protocol).
The method renderContactsOn:
defines a table with a header and delegates the rest of the rendering to the method renderContact:on:
.
The method renderContact:on:
defines the rendering of a single contact in a table row.
As we saw, the renderContentOn:
method delegates the table rendering to the renderContactsOn:
method. The latter creates a table with a heading row and delegates the contact rendering to the renderContact:on:
method. This method renders a table row with the contact's details.
Refreshing the web browser should now show a list of contacts as can be seen in Figure 44.3.
The rendering can be visually improved by adding some Cascading Style Sheets (CSS). In the following, we use the Twitter Bootstrap framework that must be loaded in the image. It is loaded by default in the Seaside distribution. If not, open the Catalog browser tool and search for Bootstrap. Install the stable version.
The contact book application must declare its dependency on Bootstrap. This is done by modifying the initialize
class method:
Execute this method manually again:
The Seaside version of Bootstrap defines some special brushes as messages (such as tbsContainer:
and tbsTable
) to improve the application rendering. We now adapt our existing code to use these methods:
As you can see, the adaptation consisted in adding a container with tbsContainer:
and replacing a table:
by a tbsTable:
message.
The result in Figure 44.4 already looks much nicer. However, in a real application, it is recommended to avoid Bootstrap specific methods such as tbsContainer:
and tbsTable:
but to use Bootstrap mixins instead. We explained the idea in Section 44.6.
We now improve the contact book application by displaying photos next to each contact. We fetch these photos automatically from the web using Gravatar. Gravatar provides a web API to retrieve a photo from an email address:
For example, for marcus.denker@inria.fr, the Gravatar URL is:
The web application must be adapted with a new column for the photos:
The result in Figure 44.5 contains a new column for the contact photos, automatically fetched from a web service.
We now add buttons to add a new contact and to remove and edit an existing contact.
We first add a remove button on each contact line in the table.
You can refresh the page in the web browser and you will see the remove buttons. However, none of them will work because an HTML form must wrap the buttons. This can be done by modifying the renderContentOn:
method again and add tbsForm:
:
The remove buttons should now work fine.
Implementing buttons to add a new contact or edit an existing one is a bit more involving because it requires creating a new component to edit the contact fields as in Figure 44.6.
We know create a component to be able to edit a contact. This is typically such task that the use of a system like Magritte can avoid.
Create a new subclass of WAComponent
which contains a contact
instance variable.
We define a class method editContact:
to set the corresponding contact.
This time we do not use lazy initialization but we initialize the object by specialising the method initialize
.
We define a new method renderContentOn:
for the class WAContact
The method renderFullnameFieldOn:
and renderEmailFieldOn:
both render a label and an input field.
Here is the renderFullnameFieldOn:
method:
The tbsFormGroup:
method is a Bootstrap method to visually group a label and an input field together. Sending the textInput
message to html
creates a new text input. The other messages configure it:
tbsFormControl
adds Bootstrap-specific HTML markup;placeholder:
adds a ghost text indicating the purpose of the field and expected value that the user is expected to enter;callback:
attaches some code to be executed when the form is validated: the first parameter of the block is the value typed in the input field;value:
writes a default value in the field.
The renderEmailFieldOn:
is very similar:
The only difference with renderFullnameFieldOn:
lies in the fact that the input field is dedicated to entering email addresses (the textInput
message has been replaced by the emailInput
message). In the callback:
block, the parameter email
is an instance of WAEmailAddress
: it is necessary to send the message address
to this object to get a string.
We now need to add 2 buttons: one to save the changes and one to cancel the changes. We introduce the message renderButtonsOn:
in the WAContact
as follows:
Then we define the corresponding methods as follows:
It is important to see that the callback of the submit button is using the message answer:
.
This message is part of the call:
and answer:
protocol of Seaside. call:
schedules a component
and answer:
unschedule it and return a value to the caller.
In the cancel button, we use answer: nil
, since there is no value returned. Still the component (here the editor) should be closed.
The last piece of the puzzle is the addition of the add and edit buttons on the contact book component.
We start with the edit button, at the end of each contact row in the table:
In the edit button callback, the message call:
is sent to self to temporarily replace the contact book component by the contact editor. When the contact editor sends answer:
to itself (in WAContact>>renderSubmitButtonOn:
and WAContact>>renderCancelButtonOn:
), the control flow comes back to this button's callback.
The add new contact button is as simple: We add a message renderGlobalButtonsOn:
.
The method renderGlobalButtonsOn:
defines a simple button.
The message call:
returns the same object that was returned by the corresponding message answer:
(in WAContact>>renderSubmitButtonOn:
and WAContact>>renderCancelButtonOn:
). Because pressing the cancel button in the contact editor passes nil
to answer:
, the message call:
may return nil
. If call:
returns something different, it will be a new contact which should be added to the contact book.
You should now get the same result as in Figure 44.1.
Now as we mentioned before when using the Seaside tags tbs
we are promoting a bad practice. We did so because we did not want to spend time explain CSS and HTML. But you should really change your code.
We are adding styling to the places where it should not be and we are breaking the idea that the code and its display should be separated. You can read the following blog to get a deeper view on it: http://ruby.bvision.com/blog/please-stop-embedding-bootstrap-classes-in-your-html
Here is a summary
The argument is that having html code like this:
goes against the separation of rendering and content that CSS is
supposed to bring. Adding all those bootstrap classes everywhere in the
HTML is not better than adding <table>
tags to layout web pages.
The solution is to use SCSS/SASS/Less mixins to add sanity to your html. Something like:
and the SCSS/SASS/Less stylesheet:
During this tutorial we defined a simple model and two simple web views. We could have written tests and implement methods in the debugger while the application is running. Pharo developers often prefer to write code in the debugger because they go faster and the execution provides objects to play with.
As you see the current functionality is rather limited, here is a list of possible extensions.