Writing Elm Ports in ReScript - 0.3
Tags: bucklescript, elm, reason, rescript
Published: 2022-04-19 19:32:22

---
title: Writing Elm Ports in ReScript - 0.3
published: true
date: 2022-04-07 00:00:00 UTC
tags: ["bucklescript", "elm", "reasonml", "rescript"]
canonical_url: https://webbureaucrat.gitlab.io/articles/writing-elm-ports-in-rescript-0.3/
---

_This is an update to a previous article following a breaking change in the `res-elm` binding. In short, the `init` function has been broken up into`init` and `initWithOptions` to allow for Elm initialization flags and Elm web applications._

Recently I've published an npm package called [res-elm](https://npmjs.org/package/res-elm/) 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 ReScript.

### The Goal: shared control between ReScript 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 ReScript app and the Elm app communicating in real time. You can find such an app [in this live demo](https://elmandrescript.gitlab.io/).

Take a moment to play around with the two text boxes. The first one lives in ReScriptland, but on its input event, ReScript sends its content into the Elm app. The second lives in Elmland, but on its input event, sends its input to the ReScript 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, in the case of my [Chicago area COVID-19 tracker](https://chicagotestout.gitlab.io/), 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'll need it for complex data types.

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_

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

port toReScript : 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 --subscribe to incoming string
              ]

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        SendString str -> { model | str = str }
                          |> \m -> ( m, Ports.toReScript m.str ) --call outgoing port
        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`.

### ReScript project setup

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

Finally, open an _Index.res_ file and expose the module.

```ts
open Elm;
```

#### 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 like getting and setting the value of an input and getting the `target` from a JavaScript `event`.

```ts
/* setup: simple JS dom interop */
@val @scope("document")
external getElementById: string => Dom.element = "getElementById"

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

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

@get external getTarget: Dom.event => Dom.element = "target"

/* get input element */
let inputReScript: Dom.element = getElementById("input-rescript")
```

### 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 `res-elm` 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 the liberty of defining a module for this type so I can move it to a new file later if need be.

```ts
module Ports = {
  type t = {
    toElm: Elm.sendable<string>,
    toReScript: 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 (v 0.19) ports in JavaScript. The `init` function takes a record which has a single field `node` of type `Dom.element`.

```ts
/* get app */

let app: Elm.app<Ports.t> =
  Elm.Main.init({ node: Some(getElementById("elm-target")),
                  flags: None
                });
```

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`inputReScript` and setting 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 `Elm` package has a `send` binding, so we `send` `event.target.value`, just like we would in JavaScript.

```ts
inputReScript 
  -> 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 `inputReScript` whenever our elm app sends a value through the port.

```ts
app.ports.toReScript -> Elm.subscribe(str => setValue(inputReScript, 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-rescript-demo">
    <h2 class="h2">Controlled by ReScript</h2>
    <input class="input" id="input-rescript"
           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/rescript/src/Index.bs.js" type="module"></script>
```

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

That finishes our project! Again, a completed example can be found [on my demo site](https://elmandrescript.gitlab.io/), and [full source here](https://gitlab.com/elmandrescript/elmandrescript.gitlab.io). Let me know if you have any questions!