8.2

Sending Data to JavaScript

In the Pure Functions section, we learned that JavaScript functions can cause side effects. Elm functions on the other hand are pure and as a result don’t cause any side effects. As nice as it is to have pure functions that are highly reliable, to do anything useful Elm programs must deal with the outside world which is riddled with side effects.

In the Elm Runtime to the Rescue section, we learned how Elm uses commands, subscriptions, and messages to properly manage side effects originated from interacting with the outside world.

The figure above shows that the way we interact with a JavaScript library is very similar to how we interact with an external service such as an HTTP server. In both cases we tell the Elm runtime to perform an operation by sending a command. When the operation is complete, the runtime sends a message back to our app.

To further understand this interaction, let’s create a simple Elm app that sends and receives data from JavaScript code. Create a file called PortExamples.elm inside the beginning-elm/src directory and add the code below to it.

module PortExamples exposing (main)

import Browser
import Html exposing (..)
import Html.Events exposing (onClick)


type alias Model =
    String


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendDataToJS ]
            [ text "Send Data to JavaScript" ]
        ]


type Msg
    = SendDataToJS


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendDataToJS ->
            ( model, Cmd.none )


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


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

The code above is quite simple. All it does is display a button titled Send Data to JavaScript. When that button is clicked, update receives the SendDataToJS message. To send data to JavaScript, we need to create a command inside the SendDataToJS -> branch in update.

Up until now we’ve been relying on a specific function such as Random.generate and Http.get to create a command.

Random.generate : (a -> msg) -> Generator a -> Cmd msg

Http.get : { url : String, expect : Expect msg } -> Cmd msg

To create a command for sending data to JavaScript we’ll have to use a different approach that involves a port. Let’s define a function called sendData below update in PortExamples.elm.

port sendData : String -> Cmd msg

Wait a minute. That doesn’t look like a function. Where is the function body? And what is the port keyword doing in front of the function name? The diagram below answers those questions.

There is something odd about sendData’s return type. If you compare its definition with the ones listed above for Random.generate and Http.get, you’ll notice that the type variable msg appears out of nowhere in sendData’s definition. Usually, if a type variable is included in a return type we can trace its origin back to a parameter, but that’s not the case here. sendData’s parameter is a simple String with no type variable.

What’s really going on is that unlike all other commands we’ve seen so far, the command generated by a port function doesn’t send a message back to the update function once the operation is complete. If you think about it, it actually makes sense not to send any messages back to our app. All we want to do is tell the Elm runtime to send some data to JavaScript. We’re not concerned with whether that data is indeed sent to JavaScript or not.

A command that doesn’t send any messages back to the app always has the type Cmd msg. If we had used Cmd Msg as the return type instead, we would be implying that the command sends a message of type Msg which is not true.

If you look inside the Platform.Cmd module, you’ll notice that the Cmd.none value we’ve been using throughout this book to represent the absence of a command also uses Cmd msg as the return type.

Cmd.none : Cmd msg

The difference between Cmd.none and the sendData port function is that the former doesn’t create any command, but the latter creates a command that doesn’t send any messages back to the app. Now that we know what sendData is and what it does, let’s use it in the update function to create a command.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendDataToJS ->
            ( model, sendData "Hello JavaScript!" )

The syntax for calling a port function is identical to that of a regular function. When update receives the SendDataToJs message, it calls sendData to create a command that sends the string "Hello JavaScript" to, you guessed it, JavaScript.

Next we need to compile the Elm code in PortExamples.elm. Run the following command from the beginning-elm directory in terminal.

$ elm make src/PortExamples.elm --output elm.js

Unfortunately, the compiler throws an error.

------------ BAD PORT ------------ src/PortExamples.elm
You are declaring port `sendData` in a normal module.

24| port sendData : String -> Cmd msg
         ^^^^^^^^
It needs to be in a `port` module.

What exactly is a port module? It’s a module whose declaration is prefixed with the keyword port. Let’s make PortExamples a port module by adding that keyword to the module definition in PortExamples.elm.

port module PortExamples exposing (main)

Run the following command one more time from the beginning-elm directory in terminal.

$ elm make src/PortExamples.elm --output elm.js

The error should go away and the elm.js file in beginning-elm should be overwritten with the compiled code for PortExamples. Now we need to write some JavaScript code to receive the string sent by Elm app on the other end. Let’s replace the contents of index.html located inside the beginning-elm directory with the following.

<!DOCTYPE html>
<html>
<body>
    <div id="elm-code-is-loaded-here"></div>
		
    <script src="elm.js"></script>
    <script>
      var app = Elm.PortExamples.init({
        node: document.getElementById("elm-code-is-loaded-here")
      });

      app.ports.sendData.subscribe(function(data) {
        console.log("Data from Elm: ", data);
      });
    </script>
</body>
</html>

The first thing we did in index.html is create a div that will load the Elm app. We then included the elm.js file which contains the compiled code. Finally, the last <script> section is where the code for receiving the string sent by our Elm app goes. Let’s go through that code step by step.

Step 1: Get hold of the div with id elm-code-is-loaded-here and embed the Elm app from PortExamples.elm into that div.

var app = Elm.PortExamples.init({
    node: document.getElementById("elm-code-is-loaded-here")
});

Step 2: Listen to sendData port using the subscribe function. When the data arrives, print it to the browser console.

app.ports.sendData.subscribe(function(data) {
    console.log("Data from Elm: ", data);
});

All ports defined in our Elm app can be accessed through app.ports. We’re now ready to test. Open index.html in a browser and go to the browser console. Now click the Send Data to JavaScript button and you should see Data from Elm: Hello JavaScript! in the console.

The following diagram illustrates the workflow we just implemented for sending data to JavaScript.

Summary

In this section, we learned how to send data to JavaScript from an Elm app using a port. Elm treats JavaScript just like an HTTP server. That’s why we need to use a command to send data to JavaScript too. Elm doesn’t allow any code to pass through a port. All we can send is data. In the next section, we’ll learn what subscriptions are. Here is the entire code from PortExamples.elm for your reference:

port module PortExamples exposing (main)

import Browser
import Html exposing (..)
import Html.Events exposing (onClick)


type alias Model =
    String


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendDataToJS ]
            [ text "Send Data to JavaScript" ]
        ]


type Msg
    = SendDataToJS


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendDataToJS ->
            ( model, sendData "Hello JavaScript!" )


port sendData : String -> Cmd msg


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


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }
Back to top
Close