Skip to content

Javascript Interop

Javascript interaction happens through something called ports, which are boundaries that you define between your Gren program, where the compiler can guarantee safety and correctness, and Javascript, where it can’t.

Browser Ports

To start using ports in the browser, you’ll need a program defined with Browser.element, Browser.document, or Browser.application.

Change your module to a port module:

src/Main.gren
module Main exposing (main)
port module Main exposing (main)

Define your incoming and outgoing ports.

src/Main.gren
port toJs : String -> Cmd msg
port fromJs : (String -> msg) -> Sub msg

Here we’re using String as the data exchange format, but you can also choose Bytes or any value that has a direct mapping to JSON. For maximum flexibility, you can use Json.Value and define your own JSON encoders and decoder. See Json.Decode and Json.Encode.

Use commands to send outgoing messages:

src/Main.gren
init : {} -> { model : Model, command : Cmd Msg }
init _ =
{ model = { message = "" }
, command = toJs "Hello from Gren!"
}

Use subscriptions to receive incoming messages:

src/Main.gren
type Msg
= MessageReceived String
update : Msg -> Model -> { model : Model, command : Cmd Msg }
update msg model =
when msg is
MessageReceived message ->
{ model = { model | message = message }
, command = Cmd.none
}
subscriptions : Model -> Sub Msg
subscriptions _ =
fromJs MessageReceived

Compile your program to a javascript file:

Terminal window
gren make Main --output=main.js

Create an html file that starts your program like this:

<html>
<head>
<script src="main.js"></script>
</head>
<body>
<div id="myapp"></div>
<script>
var app = Gren.Main.init({
node: document.getElementById('myapp'),
});
// receiving messages from gren:
app.ports.toJs.subscribe(function(message) {
alert(message);
});
// sending messages to gren:
app.ports.fromJs.send("Hello from JS!");
</script>
</body>
</html>

Node Ports

To use ports in a node program, you’ll need to define it with Node.defineProgram like we did in the webserver section of the book. Then define and use your ports in Gren the same way as described in the browser section above.

Then compile it with an explicit output target:

Terminal window
gren make Main --output=main.js

Giving the target a .js extension tells the compiler that you want a module to be included in other javascript instead of a program that runs directly. That lets us wire up the ports in a new Javascript file that looks like this:

index.js
const main = require("./main.js");
const app = main.Gren.Main.init({});
// receiving data from gren:
app.ports.toJs.subscribe(function(data) {
console.log(`Got data from Gren: ${data}`);
});
// sending data to gren:
app.ports.fromJs.send("Hello from JS!");

Now you can run it with node:

Terminal window
node index.js

Task ports

Commands and subscriptions are useful for one-way communication, but can be a little finicky when you want to request some data from JavaScript. Since the 0.6.0 version of the compiler, you have one more option for JavaScript interop: Task ports.

Task ports allow you to call a async JavaScript function as if it were a Gren Task.

You start by defining the port:

src/Main.gren
port jsIntToString : Int -> Task Json.Value String

This creates a function that takes an Int and returns a Task. If the Task fails, we’ll have a Json.Value describing the error. This could be anything, like a JavaScript Error object. If the Task succeeds, on the other hand, then we know that we have a String.

A full nodejs example might look like:

port module Main exposing (main)
import Init
import Node exposing (Environment)
import Stream.Log
import Task exposing (Task)
import Json.Decode as Json
main : Node.SimpleProgram a
main =
Node.defineSimpleProgram init
port jsIntToString : Int -> Task Json.Value String
init : Environment -> Init.Task (Cmd a)
init env =
jsIntToString 42
|> Task.andThen (Stream.Log.line env.stdout)
|> Task.onError (\err -> Stream.Log.line env.stdout (Debug.toString err))
|> Node.endSimpleProgram

We’ll need to compile it with an explicit output target:

Terminal window
gren make Main --output=main.js

Then we need to wire up the port in a new Javascript file:

index.js
const main = require("./main.js");
const app = main.Gren.Main.init({
taskPorts: {
jsIntToString: async function(int) {
if (int === 42) {
return "What was the question?";
} else {
return int + "";
}
}
}
});

Now you can run it with node:

Terminal window
node index.js

For bonus points, try to throw an exception in the JavaScript port definition and see what happens.