- Index
- »
- docLang
- »
- JavaScript
JavaScript
Overview
Fantom provides support for compiling to JavaScript and running in JavaScript VMs such as web browsers and Node.js. Most of the sys API is available, however not all pods and APIs are accessible due to limitations of the JavaScript VM environment.
Js Facet
You must explicitly mark types you intend to compile to JavaScript using the Js
facet:
@Js class GonnaBeJs { Void sayHi() { Win.cur.alert("Hello!") } }
Deployment
The JavaScript compiler works by translating Fantom source code directly to JavaScript source code at compile time. This differs from the JVM/CLR, which work by using an intermediate format and translating to the target platform at runtime.
All Fantom source code in a pod marked as @Js
will be compiled to a single JavaScript source file. This file will also include reflection information and other meta-data needed at runtime. The compiler generates modern ECMAScript-compliant JavaScript in both the ES6 and CommonJS module formats. The files are packaged in the js/
directory of the pod as js/<podname>.mjs
and js/<podname.js
respectively. A sourcemap is also included in that directory that is compatible with both module formats.
As JavaScript files are interpreted in the order they are parsed, each pod JS file must be written in the correct order. To simplify this process, the FilePack
API provides conveniences to generate a file list that guarantee the correct dependency order:
// expands to: sys.js, concurrent.js, graphics.js, web.js, dom.js files := FilePack.toAppJsFiles([Pod.find("dom")])
Browser Runtime
Web browsers are the primary target for Fantom JS, so most of the APIs are focused on simplifying how to wire up Fantom-based web apps.
Using FilePack
is the easiest way to bundle up your dependencies and serve up to the browser:
files := FilePack.toAppJsFiles(pods) pack := FilePack(files) ... override Void onGet() { switch (req.modRel.path.first) { case "myApp.js": pack.onGet ... } } ... out.head .initJs(["main":"myApp::Main"]) .includeJs(`/myApp.js`) .headEnd
A complete example can be found in js-hello.
Environment Initialization
The Fantom JS runtime can be initialized with custom configuration using the Env.vars
API. No explicit initialization is required by default. Simply parsing the source code will produce a valid runtime available at Win.onLoad
:
out.head.includeJs(`/myApp.js`).headEnd
To customize the default behavior, use WebOutStream.initJs
to initialize the desired Env.vars
. This must occur before any pod JS is parsed:
out.head .initJs(["main":"myApp::Main", "timezone":"Denver"]) .includeJs(`/myApp.js`) .headEnd
Principally this method is used to specify the "main" method to bootstrap your application at load time. See WebOutStream.initJs
for full list supported Env.vars
.
See js-env for example code for setting up timezones and locales.
Alternative Runtimes
You can run JavaScript compiled from Fantom by loading the pod's JavaScript source file into any JavaScript VM. There are no special requirements. Most of the information from the above sections should apply to other JsVMs.
Node.js
Fantom includes special support for running code in Node.js. Several tools are packaged together in the nodeJs
pod and can be accessed by running
$ fan nodeJs -help
You can run Fan scripts in Node.js using fan nodeJs run
command. Any class you want available in Node.js must have the @Js
facet and the main entry point must be in a class called Main
.
You can also use fanc to stub out an NPM module for your Fantom code. This tool is unique in that it will generate JavaScript for all types; not just the ones having the @Js
facet. This gives you access to much more of the Fantom API in Node.js. See the fanc for more details on that tool
Invoking Fantom from JavaScript
Fantom types are normal JavaScript objects, so they can be invoked natively from JavaScript code. In the browser, types are are formatted as below (note - this is the CommonJs pattern used for scoping types into the global namespace):
fan.<myPod>.<myType>.<method> fan.myPod.MyType.staticMain(); // invoke a static method fan.myPod.MyType.make().instanceMain(); // invoke method from instance
An example using an HTML event handler:
<button onclick="fan.myPod.MyType.doSomething();">Click Me</button>
Code Conventions
If you are writing native code, it must adhere to the following conventions to work with the ES compiler. It is highly recommended to spend some time looking at the JavaScript source code in sys and dom to see how the various types are implemented and adhere to these conventions.
Classes
All Fantom types are implemented as ES6 classes
Fantom
class Foo { }
ES6
class Foo extends sys.Obj { constructor() { super(); } }
Note that in all pods (excluding sys), the compiler will auto-generate import
statements for all your pod dependencies using this pattern (or similiarly require
for common js)
import * as <podName> from './<podName>.js';
You can then refer to types from another pod in your code as <podName>.<Type>
. Notice in the example above that the Foo
class extends sys.Obj
.
Fields
All Fantom fields are generated as private in the JavaScript and the compiler will generate a single method for getting/setting the field based on the access flags for the getter/setter in Fantom. The generated getter/setter will conform to the Naming rules outlined later.
Note that an implication of this design is that all fields in Fantom are only accessible in JavaScript as methods. You never access a JavaScript field's storage directly.
Fantom
class Foo { Int a := 0 Int b := 1 { private set } private Int c := 2 }
JavaScript
class Foo extends sys.Obj { constructor() { super(); this.#a = 0; this.#b = 1; this.#c = 2; } #a = 0; // has public getter/setter a(it=undefined) { if (it===undefined) return this.#a; else this.#a = it; } #b = 0; // has only public getter b() { return this.#b; } #c = 0; // no method generated for #c since it has private getter/setter } let f = new Foo(); f.a(100); console.log(`The value of a is now ${f.a()});
Enum
This field design has some specific implications for Enums. All static enum fields are generated as methods also. A good reference example is in src/sys/es/Weekday.js
. In general, you don't need to be concerned with the implementation details but any native code that wants to work with Enums needs to understand these conventions.
const monday = sys.Weekday.mon();
Funcs and Closure
All Fantom code that expects a closure or Func will be generated to expect a native JavaScript closure. For example, the sys::List.each
method is defined in List.fan
as
Void each |V item, Int index| c)
and implemented in List.js
as
each(f) { for (let i=0; i<this.#size; ++i) f(this.#values[i], i) }
Notice that the f
parameter is assumed to be a native JavaScript function and is invoked directly.
Naming
All class names are preserved when going from Fantom to JavaScript.
Slot and Parameter names that conflict with built-in JS keywords are "pickled" to end with $
. The list of names that gets pickled can be found in src/compilerEs/fan/ast/JsNode.fan
.
# Fantom Void name(Str var) { ... } # JavaScript name$(var$) { ... }
As a general rule, any field or method that ends with $
should be considered "public" API when using the JS code natively in an environment like Node or the browser. There are several "internal" methods and fields that are intended for compiler support and they will be prefixed with two underbars __
. They should not be used by consumer of the generated JS code and are subject to change at any time without any notice.
# JavaScript - these should all be considered internal static __registry = {}; __at(a,b,c) { ... }
Natives
To compile JavaScript natives, add the source directories to your build script using the jsDirs
field. See Build Pod for an example.
Currently the Fantom compiler supports generating JavaScript in the legacy js format and the new ES format. The ES compiler will look for natives by swizzling the first part of the path for each jsDir
to es/
and looking for the implementation there. For example
# build.fan jsDirs = [`js/`, `js/foo/`] // legacy location # The ES compiler will look for natives in [`es/`, `es/foo/`]
The JavaScript code must follow the compiler conventions discussed above.
Testing
Fantom includes built-in support for fant
to run units test in a JavaScript VM using the -es
flag:
$ fant -es myPod
Note that this is convenience for calling fan nodeJs test myPod
. To run JS tests you need to have Node.js installed.