How to Write Elm Ports in ReasonML
Tags: reason, elm, rescript, bucklescript
Published: 2020-09-09 17:33:34

---
title: How to Write Elm Ports in ReasonML
published: true
date: 2020-09-01 00:00:00 UTC
tags: ["reason", "elm", "rescript", "bucklescript"]
canonical_url: https://webbureaucrat.gitlab.io/posts/how-to-write-elm-ports-in-reasonml/
---

Recently I've published an npm package called [`bs-elm-es6`](https://npmjs.org/package/bs-elm-es6/) and put it into production on a couple of projects. It's documented briefly by its README, but I think it deserves a full post. This post will walk through how to set up ports both into and out of an elm 0.19 project using BuckleScript 7. (If you're curious, I'm deferring decisions about the rebrand/new syntax until we get the new npm package.)

### The Goal: shared control between ReasonML and Elm through ports

The final product is intended to be minimally reproducible and easy to understand, not necessarily useful. In this case, I think the best page to show the features of this very small library is a very small web app--an app with two text boxes that show the ReasonML app and the Elm app communicating in real time. You can find such an app [here](https://elmandbucklescript.gitlab.io/).

Take a moment to play around with the two text boxes. The first one lives in Reasonland, but on its input event, Reason sends its content into the Elm app. The second lives in Elmland, but on its input event,sends its input to the Reason scripts through another port. The result are two text boxes that always match.

Ordinarily, I would never have a textbox that lives outside the elm app--I'd give control of the whole view to Elm, but it's easy to imagine that the app instead has ports to something like an IndexedDB repository, or, in the case of my [Chicago area COVID-19 tracker](https://chicagotesout.netlify.app/), an HTTP call to some JSON data.

### Basic elm setup

Detailed instructions for how to write a basic elm project is out of scope for this kind of post, but I want some elm code here for completeness--so that I could fully reproduce this kind of project without having to flip back to the demo project's source code.

I'll start with two basic messages `SendString` and `UpdateString` that represent the two directions of information flow into and out of the app.

_Msg.elm_

```elm
module Msg exposing (..)

type Msg = SendString String | UpdateString String
```

If you're familiar with Elm ports already, you should be familiar with JSON encoding/decoding in Elm ports. This is out of the scope of what I'm trying to demonstrate, so strings here will be fine, but safely parsing JSON is a best practice and you do need to know about it for object types. For this, strings will suffice.

I also want two ports on this elm app, again representing the bidirectional flow of data into and out of this elm app.

_Ports.elm_

```elm
port module Ports exposing (..)

port toReason : String -> Cmd msg
port toElm : (String -> msg) -> Sub msg
```

And now draw the rest of the owl.

_Main.elm_

```elm
module Main exposing (main)

import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode
import Models exposing (Model)
import Msg exposing (..)
import Ports

main : Program () Model Msg
main = Browser.element 
{ init = init 
, subscriptions = subscriptions 
, update = update 
, view = view 
}

------------------------

init : () -> (Model, Cmd Msg)
init _ = ( Models.init , Cmd.none ) 

subscriptions : Model -> Sub Msg
subscriptions model = Sub.batch [Ports.toElm UpdateString]

update : Msg -> Model -> (Model, Cmd Msg)
update msg model = 
  case msg of 
    SendString str -> 
      { model | str = str } 
      |> \m -> ( m, Ports.toReason m.str ) 
    UpdateString val -> 
      { model | str = val } 
      |> \m -> (m, Cmd.none) 

view : Model -> Html Msg
view model = 
  div [class "elm-parent"] 
      [h2 [ class "h2"] 
        [text "Controlled by Elm"] 
        , input [placeholder "enter some text" 
        , type_ "text" 
        , onInput SendString 
        , value model.str] [] 
      ]
```

Again, I'm not going to go through every inch of this--I just want it here for reference. As you can see, the Messages are wired up in the `update` function and the `onInput` event, and the incoming port is wired up in the`subscriptions`.

### ReasonML project setup

Next up, initialize a new BuckleScript project,and go ahead and install [bs-elm-es6](https://npmjs.org/package/bs-elm-es6/) and add it to the`bs-dependencies`.

Finally, open an _[Index.re](http://index.re/)_ file and expose the module.

```ocaml
open ElmES6;
```

#### For completeness

Next, I'm going to define the logic surrounding the ports. I'll compose my ports from these functions.

Explaining this code in detail is out of scope for this post. Basically, all I'm doing is defining bindings for the basic DOM functionality I need likegetting and setting the value of an input and getting the `target` from aJavaScript `event`.

```ocaml
/* setup: simple JS dom interop */

[@bs.val] [@bs.scope "document"]
external getElementById: string => Dom.element = "getElementById";

[@bs.get] external getValue: Dom.element => string = "value";
[@bs.set] external setValue: (Dom.element, string) => unit = "value";

[@bs.set] 
external setOnInput: (Dom.element, Dom.event => unit) => unit = "oninput";

[@bs.get]
external getTarget: Dom.event => Dom.element = "target";

/* get input element */
let inputReason: Dom.element = getElementById("input-reason");
```

### Declare the ports as fields in a record

Initializing the elm app requires a type parameter in the form of a record in which each field represents a port in our elm app. The `ElmES6` package includes two types `Elm.sendable('t)` and `Elm.subscribable('t)` so that we can send information to our elm app and subscribe to information from it.

This app is a simple case with just two ports, but I'm going to take theliberty of defining a module for this type so I can move it to a new file laterif need be.

```ocaml
module Ports = 
{
  type t = 
  { 
    toElm: Elm.sendable(string), 
    toReason: Elm.subscribable(string) 
  };
};
```

### Get a reference to the elm app

Now that we have our type, we can get our app. This is should look familiar to anyone who's written elm (v0.19) ports in JavaScript. The `init` function takes a record which has a single field `node` of type `Dom.element`.

```ocaml
/* get app */
let app: Elm.app(Ports.t) = 
  Elm.Main.init({ node: getElementById("elm-target") });
```

The result is an `Elm.app` that gives us access to our ports, so let's use them.

### Wiring up the events

This looks like a lot, but all we're doing is taking the `Dom.element` named `inputReason` and settings its `oninput` event to a function of a `Dom.event`.

The `app` we got earlier has a member called `ports` (just like in elm-to-JavaScript ports), and the `ElmES6` package has a `send` binding, so we `send` `event.target.value`, just like we would in JavaScript.

```ocaml
inputReason 
  -> setOnInput(event => app.ports.toElm 
                           -> Elm.send(event 
                                         -> getTarget 
                                         -> getValue));
```

This next one is a little easier to follow. Here, I'm using the`subscribe` binding to set the value of `inputReason` whenever our elm app sends a value through the port.

```ocaml
app.ports.toReason 
  -> Elm.subscribe(str => setValue(inputReason, str));
```

Now compile to get _Index.bs.js_.

### Put it all together in the HTML markup

Now all that's left to do is to put it all together in our HTML markup.

```html
  <div class="div-reason-demo">
    <h2 class="h2">Controlled by ReasonML</h2> 
    <input class="input" id="input-reason" 
           placeholder="enter some text" 
           type="text" />
  </div> 
  <div id="elm-target"></div>
</div><!--end container div-->

<script src="scripts/elm/index.js"></script>
<script src="scripts/reason/src/Index.bs.js" type="module"></script>
```

This gives us everything our app is expecting: 1) an "input-reason" text box, 2) an "elm-target" div, and 3) references to our scripts.

That finishes our project! Again, a completed example can be found [here](https://elmandbucklescript.gitlab.io/), and [full source here](https://gitlab.com/elmandbucklescript/elmandbucklescript.gitlab.io).

Let me know if you have any questions!