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:
ContactBook. Let us start with the
A contact has two instance variables
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
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
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
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
Save your image and save the code using the version control browser (Monticello Browser).
A contact book contains a collection of
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
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
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
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
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):
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
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
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
In the 'iterating' protocol add the method
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
renderContactsOn: defines a table with a header and delegates the rest of the rendering to 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
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
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
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 email@example.com, 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
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
We define a new method
renderContentOn: for the class
renderEmailFieldOn: both render a label and an input field.
Here is 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:
tbsFormControladds 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.
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
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
This message is part of the
answer: protocol of Seaside.
call: schedules a component
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>>renderCancelButtonOn:), the control flow comes back to this button's callback.
The add new contact button is as simple: We add a message
renderGlobalButtonsOn: defines a simple button.
call: returns the same object that was returned by the corresponding message
WAContact>>renderCancelButtonOn:). Because pressing the cancel button in the contact editor passes
answer:, the message
call: may return
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.