Amber and JavaScript
An Amber object is a JavaScript object. A string is a JavaScript string, an array a JavaScript array, and a block a function. An instance of your class is a JavaScript object whose slots are its properties. Nothing is converted at a boundary, because there is none. What differs is how a message is answered. Every example on this page was run in the Hello Amber project from the install page.
Sending a message to a JavaScript object
A JavaScript object that Amber does not know, such as document or Math, is wrapped in a proxy. The proxy turns a message into a property access or a call:
| Message | JavaScript |
|---|---|
document title |
document.title |
document title: 'Hi' |
document.title = 'Hi' |
console log: 'hello' |
console.log('hello') |
Math max: 3 with: 7 |
Math.max(3, 7) |
A unary message reads a property. If the property is a function, the message calls it. A keyword message calls the function named by the first keyword, with all the arguments. The other keywords are yours to choose: max: 3 with: 7 and max: 3 and: 7 are the same call. A one-keyword message whose name is a property, not a function, assigns to it. If there is neither a property nor a function of that name, the message is not understood, as in Smalltalk.
The compiler reads a name that starts with a capital letter as a global. Amber looks it up among its own globals, which are its classes, and then among JavaScript’s. So Math and JSON are at hand as they are, and Date is Amber’s class, which comes first:
Math max: 3 with: 7. "7"
JSON parse: '{"x": 5}'. "a JavaScript object, wrapped"
Date today. "an Amber Date"
A capitalized name that exists nowhere answers nil. A name in lower case must be a variable, an argument or an import of the package. A global that exists when the method is compiled passes too: console and document are such, so console log: 'hello' compiles. The compiler also always accepts window, document, process and global, whichever platform has them. An unknown name is a compile error, “Unknown Variable error”.
Platform globals is the global object itself, in the browser and in Node.js alike. It is the way to a global whose name is in lower case, or known only at run time. Platform globals at: 'Math' is Math. In the browser, window is that same object, so window at: 'Math' reads the same property. These pages prefer Platform globals, which a backend under Node.js shares.
The objects that speak Smalltalk
Amber maps its classes onto JavaScript’s. String, Number, Boolean, Array, Date, Error and Promise keep their names. Function is BlockClosure, and the plain object is HashedCollection. An object of one of these classes is not wrapped. It understands Amber’s protocol, and only that:
'abc' asUppercase. "'ABC'"
'abc' toUpperCase. "does not understand #toUpperCase"
#(3 1 2) sort. "an Array (1 2 3)"
3.14159 printShowingDecimalPlaces: 2. "'3.14'"
Date new year. "2026"
So a value that comes back from JavaScript is handled by its class. A string from a JavaScript call is an Amber String. A plain object from JSON parse: is wrapped in a proxy, and answers its properties as messages: (JSON parse: '{"x": 5}') x is 5.
The literal #{ 'a' -> 1. 'b' -> 2 } is a HashedCollection, which is a plain JavaScript object with those two properties. It is the way to pass options to a JavaScript function. On the Amber side it is a dictionary: at:, at:put:, keysAndValuesDo:. It does not answer a as a message. JSON stringify: #{ 'a' -> 1. 'list' -> #(1 2) } answers '{"a":1,"list":[1,2]}'.
An Amber object of your own class goes to JavaScript as it is. Its slots are properties: a Point has x and y. asJavaScriptObject answers the object itself. For the few kernel classes that keep their state in another form, such as Point, it answers a plain object.
Blocks are functions
A block is a JavaScript function, and a JavaScript function is a block. Pass a block where JavaScript wants a callback, and call a function with value:
window setTimeout: [ console log: 'later' ] with: 1000.
[ console log: 'later' ] valueWithTimeout: 1000.
Map new. "a JavaScript constructor, called"
URL newValue: 'https://amber-lang.net/a'. "with an argument"
NativeFunction constructorNamed: 'Map'. "the same, for a name known at run time"
A function’s own properties are not reachable by a message, because a block understands only Amber’s protocol. To call Object.keys, wrap the function in a proxy: (JSObjectProxy on: (Platform globals at: 'Object')) keys: anObject. Object alone would be Amber’s class.
In the sample, doConsoleLog passes a block inside a literal object. The JavaScript console then shows a callable property:
console log: #{ #greeting -> greeting. #target -> target. #callback -> [ console log: greeting, ', ', target, '!' ] }
Promises
A JavaScript promise is Amber’s Promise. then: takes a block, or an array of blocks and selectors to run in turn. catch: takes the block for a rejection. The sample’s doInspectStatus fetches a URL and inspects the parsed answer:
doInspectStatus
(self getApi: '/status')
then: {#json. #inspect}
catch: [ :err | Terminal alert: err ]
Platform fetch: is the browser’s fetch. Promise new: [ :model | model value: 42 ] makes a promise that model value: resolves and model signal: rejects. Promise all: waits for a collection of them.
Errors
A JavaScript Error is an Amber Error, so on: Error do: catches it, and messageText is its message:
[ JSON parse: '{' ] on: Error do: [ :e | e messageText ]
"'Expected property name or ''}'' in JSON at position 1 (line 1 column 2)'"
JavaScript code may throw a value that is not an Error, such as a string. That raises a JavaScriptException, whose message text starts with “JavaScript exception:”. A message that a JavaScript object has no property or function for raises MessageNotUnderstood, like any other.
A library bound to a name
An import of the form 'dayjs' -> 'dayjs' in a package’s definition binds what the module exports to the name dayjs. The name is visible in every method of that package. The libraries page sets one up. A workspace does not see the name, and reports “dayjs is not defined”.
Inline JavaScript
A method can be written in JavaScript. Its whole body is one pragma, and self is $self:
new: anObject
<inlineJS: 'return new Date(anObject)'>
This is Date class >> new: in the kernel. Inside, a message to an Amber object is sent with the JavaScript name of its selector, $recv(anObject)._printString(). Keep the JavaScript at ES2017 syntax: the optimizer that builds the.js parses every method it bundles and reads nothing later.
Calling Smalltalk from JavaScript
index.html of the sample starts the application this way:
require(['app'], amberPromise => {
amberPromise.then(amber => {
amber.initialize({ 'transport.defaultAmdNamespace': "amber-helloamber" }).then(() => {
amber.globals.HelloAmber._start();
});
});
});
amber.globals holds every class by name, once amber.initialize() has resolved. A selector becomes a JavaScript name by a fixed rule: an underscore in front, and an underscore for each colon. A binary selector gets two underscores and a name:
| Smalltalk | JavaScript |
|---|---|
anObject yourself |
anObject._yourself() |
aDictionary at: 'k' put: 1 |
aDictionary._at_put_('k', 1) |
3 @ 4 |
(3).__at(4) |
amber.api.st2js('at:put:') answers '_at_put_', and amber.api.js2st goes back. Any module that needs Amber requires amber/helpers, in the browser and in Node.js alike. It gets the same globals, api and initialize. From Amber, require value: 'amber/helpers' is the same call.