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:

How to create and process a HTML form. How to communicate with a REST API server. 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:

Install the library using bower. If a ‘local.amd.json’ files does not exist for the bower package, create a ‘libname.amd.json’ file in the project root. Run ‘grunt devel’ (or ‘grunt deploy’ if you’re ready to deploy your application). 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!)