JavaScript

OverviewJs FacetDeploymentBrowser RuntimeAlternative RuntimesNode.jsInvoking Fantom from JavaScriptCode ConventionsNativesTesting

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.