See the PharoJS GitHub repository for installation instructions in a Pharo image
pharo-local/iceberg/PharoJS/PharoJS/HTML
It contains subfolders for each example and test.
You can also find it in the PharoJS Git repository, along with Pharo code: https://github.com/PharoJS/PharoJS.
We have an experimental feature. It shows the feasibility of the idea, but the solution is not clean. That is, if you have multiple Seaside components with PharoJS code, you'll end up with redundant JS code. The component will work, but it will waste resources (network bandwidth). This at least 65KB per component.
PharoJS integration with Seaside is defined in a dedicated package PharoJsSeaside
. You'll find below instructions on how to install it.
Metacello new baseline: 'PharoJsSeaside'; repository: 'github://PharoJS/PharoJS'; load
Gofer it smalltalkhubUser: 'noury' project: 'PharoJS'; configurationOf: #PharoJsSeaside; loadBleedingEdge.
The code is pretty straight forward. To test it, start Seaside and open
http://localhost:8080/PharoJS/
.
Since PharoJS is on GitHub, essentially what you do is make a fork of PharoJS, load your fork with Iceberg, make and commit your changes, and then create a pull request. Then Noury or Dave will load your pull request, test it, and put it into the PharoJS repository.
YourName
Metacello new baseline: 'PharoJS'; repository: 'github://YourName/PharoJS'; load
issue
nn with the issue number from aboveKeyboardEvent
or MouseEvent
?
window KeyboardEvent new: #keypress with: { #key -> #Enter. #keyCode -> 13. #which -> 13 } asDictionary window MouseEvent new: #click
rectangle := document createElement: 'div'. rectangle id: 'grn'. document body appendChild: rectangle.
In most cases, simply treating it as a message send works:
jsobj fieldName. " equivalent to jsobj.fieldName " jsobj fieldName: 0. " equivalent to jsobj.fieldName=0 "You do exactly the same to call a method:
jsobj methodName. " equivalent to jsobj.methodName() " jsobj methodName: 0. " equivalent to jsobj.methodName(0) " jsobj methodName: 0 foo: 42. " equivalent to jsobj.methodName(0,42) " jsobj methodName: 0 bar: 42. " equivalent to jsobj.methodName(0,42) "Notice that the message name for multiple parameters is only matched to the first parameter, so the last 2 examples are functionally identical.
If the field is a method (function) then simply referencing it will call it, so to get or change the value you must use:
jsobj instVarNamed:#count. jsobj instVarNamed:#count put: 0.You can also use this to force creation of a slot.
There is a minor detail about Javascript "classes" such as MouseEvent
because they are implemented as special Javascript functions, but usually you don't want to call them, you want to send them a new
message.
So if the message name starts with an uppercase letter, it is treated as a value reference, rather than as a function.
This allows the following to work:
window MouseEvent new: #click
The premise of PharoJS is that you can develop code in the Pharo IDE with the GUI in the browser, including running tests. Then when you're finished, you can export the whole thing to Javascript and have it execute the same way (or similar, see the question about differences).
To achieve this, we communicate across a bridge between the running Pharo image and the running browser (see the question about the bridge). This means that there are limitations in how processing on the browser side can proceed. Most paticularly, you can't call-back a block (and wait for the return value) from within code running on the browser. The closest you can do is call a block, and pass it a continuation block as a parameter. It could then run on the Pharo side, and call the browser block passing the result.
We control the transpilation to Javascript, but we have no control over the Pharo Smalltalk compilation, so we can't add any fundamentally new concept.
The bridge is used during development to connect the running Pharo image and the running PharoJS code in the browser - but does not exist in production code. The bridge is implemented over a WebSocket which allows bi-directional asynchronous communication.
There are proxy objects on the Pharo side that represent objects on the browser.
Some of there are global proxies, such as document
, window
, etc. which are implemented as Javascript globals (see the question on globals).
When you send a message to a proxy, a DNU intervenes and sends the parameters across the bridge to the browser, which calls the method on the object referred to by the proxy, and returns the result.
The other main thing that comes across the bridge are callbacks.
A callback is a reflection of an event occuring on the broswer.
The structure for callbacks is created by sending an addEventListener:block:
message to a proxy (see the question on this).
In order to faithfully emulate the Javascript execution model, no callback can happen while an existing callback is executing, or while any initial interactionwith the browser is taking place.
addEventListener:block:
to set event handlers?
addEventListener:block:
message to a proxy (typically of a DOM object).
The referenced Smalltalk block must be held as long as there are references to it on the browser.
Unfortunately, there isn't currently a weak data-structure available in browsers that would allow us reliably to clean these up.
So we resort to using reference counting, so you also need to use addEventListener:block:
with a nil
block to remove the callback.
Not explicitly removing a callback can lead to memory being tied up on the Pharo side until the bridge is closed, but seems like a reasonable trade-off as it only applies during development.
double
s, so there are no big-ints, or fractions.thisContext
does not exist, so any method that references it will be uncompilable.become:
is unsupported.SequenceableCollection>>reverse
is defined by ANSI Smalltalk to make a copy, but ECMAScript (aka Javascript) defines it to reverse in place. Instead you should use reversed
which is defined in Pharo and PharoJS to return a copy.asArray
always returns a copy of the receiver, whereas in Pharo Array>>asArray
returns self
. Array
s and OrderedCollection
s are the same thing in PharoJS, so outside of tests, it is probably better to use asOrderedCollection
, but this also always returns a copy.Number class>>#primesUpTo:
?
Number
) or other classes without importing the whole class” is on our todo list.
In the meantime, add the following to PjNumber
class-side:
jsTranspilationImportMethodsNonstandard <pharoJsSkip> ^ { Number class -> #(primesUpTo: primesUpTo:do:). }This says to import those 2 methods from the
Number
class.
This is the user definable version of jsTranspilationImportMethods
(for which PjNumber
has quite a large implementation) which allows you to augment the list of methods that it imports.
You should put this method in PjNumber class
with a category of *YourPackage
so that it will be associated with your package by Monticello or Iceberg.
We can't import all of e.g. Number
or Object
, because it would cause considerable code bloat in your application, and because there are methods in some of those classes that cannot be compiled (see the question on differences).
Yes, you certainly can use Node. Many people would question the decision as they would prefer to provide the application in Pharo directly, with all the advantages that entails. However, there are situations where deploying as Node is preferred (such as creating an Electron application - before you ask: we don't have that quite figured out yet, but we'd help someone who wants to).
Subclass PjNodeApplication
.
To access modules, define a polyfill similar to PjWebSocketPolyfill
(see the question on polyfills).
A polyfill is a code shim that implements a feature on Javascript engines that do not otherwise support the feature. In PharoJS, polyfills add code at the beginning of the generated Javascript that does whatever is necessary to make a global value/class available in the Javascript engine. As such, a polyfill is a Javascript global (see the question on globals). If the values aren't predefined and some initialization is required in order for them to be available (see the question on the bridge), then Javascript code needs to be generated to do that initialization.
The code for doing this initialization is placed in the methods browserPolyfill:
, domPolyfill:
, and nodePolyfill:
.
A given polyfill may have all three methods, but often they are actually defining globals that are available on other Javascript engines.
For example, PjProcessPolyfill
creates a value process
for browsers that is comparable to the process
value that exists in NodeJS.
Similarly, PjWebSocketPolyfill
makes available a websocket in NodeJS that is comparable to the WebSocket
code that exists in web servers.
Copying one of these is usually a good starting point for making your own polyfills.
PharoJS uses the Pharo Smalltalk pool dictionary facility to make Javascript globals available to code running in the Pharo image.
In particular groups of such symbols are defined in subclasses of PjJavascriptGlobals
.
The appropriate globals are automatically available to subclasses of PjApplication
.
If you need them in a class that doesn't subclass that, add a poolDictionaries:
parameter to your subclass:instanceVariableNames:classVariableNames:package:
(see PjBrowserApplication
for an example).
If you create your own globals, you should use the trait PjTJavascriptGlobalsInitializer
, which defines a class initialize
method so that a proxy is available.
The simplest thing to do is to copy the PjUniversalGlobals
class which make some common values available as classVariableNames:
.
If the global you want to use is from some Javascript library or otherwise needs some initialization, you should look at the question on polyfills.
SecurityError (DOM Exception 18): The operation is insecure
This error prevents running PharoJS tests with Safari as a default browser. This problem also arises when opening a PharoJS playground on a app (e.g. `PjCounterBrowserApp`).
The cause of the bug is that currently, we open HTML files from the disk. This works fine with Firefox and Chrome. But, Safari's new security system forbids accessing JS global `localStorage` when the HTML or the JS code is loaded from file://
Luckily, there is a workaround. First, ensure you have the Safari developer tools installed. Then, in the `Develop` menu check the "Disable local file restrictions" item.
This is needed only during development. Once your app is ready for production, you export the code and make it available on a server. You should not experience the issue.
Don't forget that PharoJS allows building HTML apps. Thus, if you require accessing sensitive resources, you are likely to have to define a content security policy in your HTML file using the meta
tag. You can find some examples in the Stack overflow response addressing this question
CORS security error
On Firefox, open about:config
and then enter the setting privacy.file_unique_origin
, as explained here.
Similar problem on Chrome URL scheme must be "http" or "https" for CORS request.
- See this stackoverflow answer
about:config
and set allow_scripts_to_close_windows
to true.
If you have other questions, the best place to ask is the Slack workspace (see the question on getting help). As questions are asked there, they get added to this FAQ.