A Gentle Introduction to Amber

Engaging in Smalltalk with Her

Richard Kenneth Eng
Smalltalk Talk

--

Sorry, not that Amber. I’m talking about Amber, the dialect of Smalltalk designed for writing browser-based applications. I’m talking about the client-side language that I’ve chosen for myself over other candidates such as Dart and CoffeeScript.

In an attempt to shed some light on Smalltalk’s presence in (client-side) web development, I shall offer here a handy tutorial on Amber.

Why Smalltalk?

Well, for one thing, I’m already familiar with Smalltalk, having worked in the Seaside web framework a number of years ago. (Seaside is a most remarkable server-side framework. GemStone has helped to make it especially suitable for enterprise development.) It quickly became my favourite programming language because of its incredible simplicity and elegance (the entire language spec can fit on a postcard!). Smalltalk’s “live” programming environment, where you’re always working with “live” objects that you can examine and change at will, confers enormous power to the developer [1]; there’s nothing else like it in the world. Consequently, you can be extremely and rapidly productive with Smalltalk.

Many people regard Smalltalk as an “obscure” language, but that isn’t true at all. Released in 1972, Smalltalk was the first major OOP language and, in my opinion, still the best. Over the decades, it has strongly influenced the design of numerous other languages, including Objective-C, Ruby, Groovy, Scala, and Dart. Today, Smalltalk is used in the industrial and financial sectors (as a secret weapon offering a competitive advantage). To be sure, it’s nowhere near as popular as, say, Java, but Smalltalk is definitely alive and well, especially in the Pharo incarnation. The point is, if you find languages like Java, C++, PHP, and JavaScript painful to use, there is a clear alternative staring you in the face that is fun, easy, and highly productive.

Reads Like English, and It’s Still Succinct!

It’s more like pidgin English, but Smalltalk is eminently readable. This is important because you typically write code once, but read the same code again and again, perhaps months or years later. Here’s an example:

"a class definition"
Widget subclass: #TCounter
instanceVariableNames: 'count header'
package: 'Tutorials'
"a statement you might find in a rendering method:"
html button
with: '++';
onClick: [self increase].
"a method definition"
increase
count := count + 1.
header contents: [:html | html with: count asString]
"display an instance of TCounter using jQuery"
TCounter new appendToJQuery: 'body' asJQuery

“It’s alive! It’s alive!”

The Smalltalk environment is much like a living, breathing organism. Every object is “live” and you can examine it or change it at will during development and testing. This results in a short test cycle that makes for quick and easy coding of your application. A great deal of Smalltalk’s much-vaunted high productivity is thanks to this environment.

In Amber, the programming environment is called Helios, which has been rewritten (in Amber) from the ground up to be improved over the older legacy IDE.

“Source code in files. How quaint.” — Kent Beck

In software development, the traditional way to physically organize source code has always been to use files (and folders). Smalltalkers believe there is a better way: your application is a system of live objects that can be stored, system state and all, in one location. The Smalltalk environment, then, is a higher level of abstraction and a more sophisticated approach for writing software. Welcome to the future!

Language Wars: A New Hope

Amber compiles to JavaScript. Pure JavaScript. The resultant application is simply a set of files containing HTML, CSS, and JavaScript that can be statically served by any web server. So deploying Amber applications is much different from the traditional way of deploying Smalltalk applications, thus removing a long-standing criticism of Smalltalk that it’s awkward to package and distribute its applications.

All the beauty and power of Smalltalk without the drawbacks. Is this the renaissance of a pioneering hero?

Since Amber maps nicely to JavaScript, the compiled code is lean and fast, removing yet another criticism that Smalltalk runs too slowly in a byte-code virtual machine.

Learning Smalltalk is easy, not only because the language itself is simple, but because there are ample introductory books available (most of them free!). I especially like Smalltalk by Example. Since Pharo is considered the reference implementation for Amber, Pharo by Example (or a later edition, which is in beta) would be a good start. An online, interactive tutorial is fabulous for newcomers. For C# and Java programmers, this brief guide comparing Java syntax with Smalltalk syntax is very helpful. You can also reach out to the friendly and helpful Amber and Pharo communities in their respective forums (here and here).

Learn Amber and grow the community!

Installing Amber

Bootstrapping the Amber development environment requires Node.js; they make it relatively easy to install Node.js. (For Node in Linux, look at these instructions: https://nodejs.org/en/download/package-manager/.) After you’ve done this, the following commands will complete the Amber installation:

# for OS X and Linux, you need the following two commands
npm config set prefix=~/npm
export PATH="$PATH:$HOME/npm/bin" # add to .bash_profile or .bashrc
npm install -g amber-cli grunt-cli grunt-init

You will also need to install git, if it’s not already installed.

To create a new Amber project, do the following:

# Create the project structure
mkdir example-project
cd example-project

# Create and initialize a new Amber project
amber init

The ‘amber init’ step will ask you several questions about your new project, including a namespace. You can accept most of the defaults. After you’re done, start the server:

amber serve

And visit http://localhost:4000 in your browser to see the Amber application running. Select the Helios IDE. We will adapt this welcome app for our tutorial. Our goal is to demonstrate the following:

  1. How to create and process a HTML form.
  2. How to communicate with a REST API server.
  3. How to import and use an external JavaScript library.

Note that this application makes heavy use of jQuery. It is a very important foundation for a web app because it makes dealing with JavaScript so much easier.

The All-seeing Helios

Here’s a very basic introduction to Helios, the Amber IDE [2]:

The Single Page Application

As a Single Page Application (or SPA), our software consists of one webpage only. Within this webpage is a client area where all the action takes place. The client area is defined by a HTML ‘div’. Outside the ‘div’, the webpage can be designed and styled (with CSS) any way you like. Our ‘div’ is defined thus (just place this near the top of the webpage in index.html for now):

<div id="client-main">
</div>

The Web Package

This is an important Amber package for building HTML. Its classes define key methods such as #asJQuery, #renderOn:, and #appendToJQuery:. When presented with an instance of HTMLCanvas, you can send messages to it to define various HTML components, such as forms, tables, inputs, etc. For example, where ‘html’ is a HTMLCanvas instance:

html p: 'The rain in Spain'. "this is a paragraph"
html a href: 'http://smalltalkrenaissance.ca';
with: 'Smalltalk Renaissance'. "this is an anchor or link"

You can nest HTML tags inside #with: blocks, for example:

html form with: [
html table with: [
html tr with: [
html td with: 'Username'.
html td with: [html input]].
html tr with: [
html td with: 'Password'.
html td with: [html input type: 'password']]]]

will product this HTML:

<form>
<table>
<tr><td>Username</td><td><input></td></tr>
<tr><td>Password</td><td><input type="password"></td></tr>
</table>
</form>

The Form

Our form is in tabular format. The layout and specification for the form elements are represented by an Array and a Dictionary. The Array (‘formInputs’) determines the order of input elements. The Dictionary (‘inputFactories’) contains rendering information.

We start by creating a class called ‘FormExample’:

Widget subclass: #FormExample
instanceVariableNames: 'formValues formInputs inputFactories extractionRecipes'
package: 'KingTut'

It has four instance variables:

  • ‘formValues’ contains all the input values after form submission
  • ‘formInputs’ contains the input elements in the specified order
  • ‘inputFactories’ contains the input elements’ rendering specifications
  • ‘extractionRecipes’ contains field value extraction code

An object of this class is initialized with:

initialize
formValues := Dictionary new.
formInputs := Array new.
inputFactories := #{
'Username' -> [ :html :name :type |
html input name: name;
type: type;
at: 'required' put: 'required';
yourself].
'Password' -> [ :html :name :type |
html input name: name;
type: type;
at: 'required' put: 'required';
yourself].
'SexLabel' -> []. "just show the label"
"the following three radio buttons belong in the group
named 'Sex'..."
'Male' -> [ :html :name :type |
html input name: 'Sex';
value: name;
type: type;
at: 'checked' put: 'checked';
yourself].
'Female' -> [ :html :name :type |
html input name: 'Sex';
value: name;
type: type;
yourself].
'Yes' -> [ :html :name :type |
html input name: 'Sex';
value: name;
type: type;
yourself].
'Comment' -> [ :html :name :type |
html textarea name: name;
yourself].
'Country' -> [ :html :name :type |
html select name: name;
with: [self countryOptions: html];
yourself].
'_default' -> [ :html :name :type |
html input name: name;
type: type;
yourself]
}.
extractionRecipes := #{
'RememberMe' -> [ :input |
input asJQuery prop: 'checked'].
'Sex' -> [ :input |
(input asJQuery prop: 'checked')
ifTrue: [ input asJQuery val ]].
'_default' -> [ :input |
input asJQuery val]
}

where

countryOptions: html
html option value: 'China'; with: 'Chung Kuo'.
html option value: 'Canada'; with: 'North Land'.
html option value: 'Japan'; with: 'Samurai World'.
html option value: 'United States'; with: 'Imperialists'.
html option value: 'Iraq'; with: 'Broken State'.
html option value: 'Brazil'; with: 'Sexy Land'

To render the form, we have the following methods:

"Each table row is described by 'name -> {label. type}'."renderOn: html
formInputs removeAll.
html form id: 'myForm1'; with: [
html table with: [
#{'Username'->{'Username:'. 'email'}.
'Password'->{'Password:'. 'password'}.
'RememberMe'->{'Remember me'. 'checkbox'}.
'SexLabel'->{'Sex:'. ''}.
'Male'->{'Male'. 'radio'}.
'Female'->{'Female'. 'radio'}.
'Yes'->{'Yes'. 'radio'}.
'Comment'->{'Comment:'. 'textarea'}.
'Country'->{'Country:'. 'select'}}
keysAndValuesDo: [ :key :value |
self renderInput: key
label: value first
type: value second
on: html ]].
html p: [
html input
type: 'submit';
value: 'Collect Input Field Values';
onClick: [ self collectValues ]]]

and

renderInput: name label: inputLabel type: type on: html
html tr with: [
html td with: [html label with: inputLabel].
html td with: [ | factory input |
factory := inputFactories at: name
ifAbsent: [inputFactories at: '_default'].
input := factory value: html value: name value: type.
input ifNotNil: [formInputs add: input]]]

Upon form submission, we collect all the input values:

collectValues
(('#myForm1' asJQuery get: 0) checkValidity) ifTrue: [
formInputs do: [ :each | | recipe name |
name := each at: 'name'.
recipe := extractionRecipes at: name
ifAbsent: [extractionRecipes at: '_default'].
(recipe value: each)
ifNotNil: [ :formValue |
formValues at: name put: formValue ]].
formValues keysAndValuesDo: [ :key :value |
'#output-list' asJQuery append: '<br>',key,': ',value].
^false]

Note the bolded line. This is needed in order to allow for HTML5 validation (#collectValues is called repeatedly until the form has been validated). Normally, upon successful submission, the method proceeds with standard posting behaviour. We don’t want that; it would disrupt the flow of our SPA. We let the form do its validation, and when it’s good and ready we circumvent normal behaviour by returning false.

Once you’ve collected all the input values, you can do anything you wish — execute your business logic, perform further data validation, gather more information through another form, etc.

To make use of our FormExample in the application, we must connect it to our client ‘div’ somewhere in our application class:

'#client-main' asJQuery empty. "make sure it's empty"
FormExample new appendToJQuery: '#client-main' asJQuery.

The natural place to do this is in the augmentPage method — for convenience, place the lines at the top before the button definitions.

So try it. Run this in your browser and see what it looks like.

Talking to a REST API

Now that we have a Username and Password, let’s verify the credentials with a REST API server. I’ve set up such a server at PythonAnywhere.com; it’s called ‘tut_server’.

[Note: Our REST API server expects authorization in order to use its services. This authorization is via ‘auth_user’ and ‘auth_pwd’ which are the payload in ‘data’ [3]; anyone who knows these credentials may use the services.

The REST API server relies on SSL to encrypt the ‘url’ and ‘data’ during communication between application and server. It is not the most secure setup, but for the purposes of this tutorial, it is adequate.]

We add a method in FormExample to POST to this server:

verifyUser: user password: pwd
JQuery current ajax: #{
'type' -> 'POST'.
'url' -> ('https://richardeng.pythonanywhere.com/tut_server/default/api/verify/person/',user,'/',pwd).
'dataType' -> 'json'.
'data' -> #{'auth_user' -> 'tyrion@yahoo.ca'.
'auth_pwd' -> 'Lannister'}.
'success' -> [ :jsonData | '#output-list' asJQuery
append: '<br>',(JSON stringify: jsonData)].
'error' -> [ :xhr :status | alert value: status]
}

In the #collectValues method, we add the following line just before returning false (^false):

self verifyUser: (formValues at: 'Username')
password: (formValues at: 'Password').

There is only one verifiable set of credentials at tut_server:

  • Username: oberyn.martell@gmail.com
  • Password: “RedViper”

If you enter this in the form, you will get the following JSON response (in ‘jsonData’):

{"verified":true,"id":28}

Switch to a new form

Let’s switch to another form after the successful verification of the credentials. First, create another form with class #FormExample2, just as we did for #FormExample; give it a form id of ‘myForm2’. We need a way to replace the existing form in ‘#client-main’:

nextForm
'#client-main' asJQuery empty. "make sure it’s empty"
FormExample2 new appendToJQuery: '#client-main' asJQuery

In #verifyUser:, we need to add some code to check the JSON value of ‘verified’:

'success' -> [:jsonData |
'#output-list' asJQuery
append: '<br>',(JSON stringify: jsonData).
jsonData verified ifTrue: [ self nextForm ]].

After successful verification, we now have a new form in the client area! Note that Amber’s mapping to JavaScript allows us to access the #verified key in the JSON object quite simply (just send the #verified message).

Integrating External JavaScript Libraries

Amber gives you access to a vast array of JavaScript libraries via the bower system. In general, it’s a four-step process to integrate a JavaScript library:

  1. Install the library using bower.
  2. If a ‘local.amd.json’ files does not exist for the bower package, create a ‘libname.amd.json’ file in the project root.
  3. Run ‘grunt devel’ (or ‘grunt deploy’ if you’re ready to deploy your application).
  4. Add ‘libname’ to your application package’s #imports:.

We would like to add the very popular graphics library D3.js to our application. First, install it from bower:

bower install d3 --save

Since it doesn’t have a ‘local.amd.json’ file, we create ‘d3.amd.json’ in the root of our application folder:

{
"paths": {
"d3": "d3"
}
}

Now, run ‘grunt devel’. Finally, add ‘d3’ to the package imports for our application:

imports: {'amber/jquery/Wrappers-JQuery'. 'amber/web/Web'. 'd3' -> 'd3'. 'silk/Silk'}

[In the d3.amd.json file, the first “d3” before the colon is the symbolic name that is mapped to the second “d3” after the colon which is the library path within its directory tree without the .js extension.]

Testing D3

To test that d3 is indeed integrated (because you won’t see this package in the Browser; it’s not a browse-able, object-oriented package), we run a little test. First, we add the following to index.html:

<svg width="720" height="120">
<circle cx="40" cy="60" r="10"></circle>
<circle cx="80" cy="60" r="10"></circle>
<circle cx="120" cy="60" r="10"></circle>
</svg>

This will draw three circles on the webpage. Next, we wire up a couple of buttons (in the same way you see in the welcome app) to run the following two methods [4]:

foo
| circle |
circle := d3 selectAll: 'circle'.
Smalltalk optOut: circle.
circle style: 'fill' set: 'steelblue'.
circle attr: 'r' set: 30.
bar
| circle |
circle := d3 selectAll: 'circle'.
Smalltalk optOut: circle.
circle style: 'fill' set: 'red'.
circle attr: 'r' set: 20.

These are the Amber translations of the inline JavaScript I used in the following method [5]:

doInline
<
var circle = d3.selectAll("circle");
circle.style("fill", "steelblue");
circle.attr("r", 30);
>

As you click on the buttons, watch the circles change in size and colour. The test is successful!

Epilog

This concludes Part 1 our Amber tutorial. Rather than provide you with the full source, I encourage you to install Amber and follow through the tutorial. It’s really quite easy!

(Here is the next installment of the Amber tutorial. Great stuff lies ahead!)

[1] Not only can you inspect the execution state, edit and continue, you can also define new classes and methods in the context of a live application you are developing. You can actually program in the debugger. Start by defining just a test and continue from there.

[2] According to Greek myth, nothing could be hidden from Helios Panoptes (“the all-seeing”). Helios can see all aspects of your application, via the Browser and the Inspector.

[3] I found this very useful: http://stackoverflow.com/questions/21850454/how-to-make-xmlhttprequest-cross-domain-withcredentials-http-authorization-cor. As the author says: “It’s easier to simply write a server that accepts the authorization as part of the body of the request.”

[4] Smalltalk optOut: circle. Apparently, D3 returns a somewhat funky JS object, so we have to strip away the wrapping, thereby exposing the actual object. It’s a funky JS object, which masquerades as an array. Amber eagerly wraps it around and presents it as good old Smalltalk ‘Array’. It’s great for true array, but not here, so we have to strip.

[5] JavaScript inlining must occupy its own method fully. You cannot mix Amber code with inlined JavaScript.

--

--