7.4

Protecting Boundaries between Elm and JavaScript

What happens if we send a number instead of a string from JavaScript to our Elm app? Let’s find out by modifying the callback function passed to app.ports.sendData.subscribe in Ports/index.html.

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

Reload the Ports/index.html file in a browser and click the Send Data to JavaScript button. Number 10 doesn’t appear on the page. To find out why open the browser console.

Unlike JavaScript, Elm is very strict about data types. Since we said the receiveData port will receive a value of type Model, which is just String behind the scenes, Elm doesn’t let any other types through that port.

port receiveData : (Model -> msg) -> Sub msg

This is a great news because Elm has our back if the JavaScript code misbehaves by trying to sneak a type in that we aren’t expecting. We can actually make this mismatched type detection process even better by displaying a helpful message to the users instead of just crashing our app.

Which Types are Allowed through Ports?

Before we improve our app by not letting it crash whenever a wrong type is received from JavaScript, let’s understand what types of data Elm allows through both incoming and outgoing ports.

Interacting with JavaScript code from an Elm app is very similar to how we interact with an HTTP server. Therefore, to keep things simple Elm prefers to stick with the JSON format when sending and receiving data from JavaScript as well.

JSON stands for JavaScript Object Notation and is derived from JavaScript. It’s not a coincidence that all of the valid types in JSON listed below — except null — are also available in JavaScript. This makes converting JSON values to JavaScript and vice versa incredibly easy.

In JavaScript, null is a value whose type is object. Unfortunately, this is a bug in the language according to Brendan Eich — the creator of JavaScript. Luckily, JSON sidestepped this bug by creating a separate type for null.

  • string - A string must be written in double quotes and looks very similar to an Elm string.

  • number - A number must be an integer or a float.

  • boolean - A boolean must be either true or false.

  • null - null is used to indicate an absence of a value.

  • Array - An array can contain any other JSON values including arrays themselves.

  • object - An object consists of key value pairs.

Despite its roots in JavaScript, JSON is language independent. In fact, it was originally created to simplify the interchange of data between applications regardless of which language they were written in. Now you can see why the designers of Elm chose JSON for communicating with JavaScript code.

The table below shows the mapping between Elm and JSON values when we send and receive data from JavaScript via ports.

Elm Value Elm Type JSON Value JSON Type JavaScript Type
"Hello JavaScript" String "Hello JavaScript" string string
10 Int 10 number number
3.14 Float 3.14 number number
True Bool true boolean boolean
Nothing Maybe a null null object
{ age = 25 } Record {"age" : 25} object object
( 9, "Pam", False ) Tuple [9, "Pam", False] array Array
[ 1, 2 ] List Int [1, 2] array Array
[ "Mr", "Robot" ] List String ["Mr", "Robot"] array Array

Decoding JSON Values Received from JavaScript

By default, the runtime is in charge of converting data between Elm and JavaScript. For example, when receiving data from JavaScript, the runtime first converts the JavaScript values into JSON and then decodes the JSON into corresponding Elm values. That’s why we didn’t have to decode the data coming from JavaScript ourselves. Elm knew how to translate it properly just by looking at the type of our incoming port function.

port receiveData : (Model -> msg) -> Sub msg

A downside of letting the runtime do the decoding behind the scenes is that if the JavaScript code sends an incorrect type, our app simply crashes. A better approach is to do the decoding ourselves. That way if anything goes wrong, we’ll be able to show a proper error message to the user.

Modifying Incoming Data’s Type

Let’s start by changing the type of receiveData incoming port from Model to Value in PortExamples.elm.

port receiveData : (Value -> msg) -> Sub msg

The Value type represents a JSON value. It’s defined in the Json.Encode module, but the Json.Decode also makes it available through the use of type alias.

type alias Value =
    Value

This way we don’t have to import Json.Encode if all we’re doing is use the Value type. Import the Json.Decode module in PortExamples.elm.

port module PortExamples exposing (..)
.
.
import Html.Events exposing (..)
import Json.Decode exposing (Value, string, decodeValue)

We’ve also exposed the string decoder and the decodeValue function. We’ll use them later.

Handling the ReceivedDataFromJS Message Differently

Now that the type of our incoming data has changed from Model to Value, we need to modify the ReceivedDataFromJS data constructor in PortExamples.elm.

type Msg
    = SendDataToJS
    | ReceivedDataFromJS Value

This means we can’t simply return the incoming data as the model inside the ReceivedDataFromJS data -> branch in update anymore.

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

Let’s modify that branch so that we can decode the incoming JSON value.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        ReceivedDataFromJS value ->
            case decodeValue string value of
                Ok data ->
                    ( { model | dataFromJS = data }, Cmd.none )

                Err error ->
                    ( { model | errorMessage = Just error }, Cmd.none )

decodeString vs decodeValue

In the Decoding JSON section, we used the decodeString function to first parse the raw string fetched from an HTTP server into JSON and then apply the string decoder to translate that JSON into an Elm string.

> decodeString string "\"Beanie\""
Ok "Beanie" : Result.Result String String

The figure below shows the process decodeString goes through while decoding values from a raw JSON string.

The code we just added to the ReceivedDataFromJS value -> branch in update doesn’t use decodeString. It uses decodeValue instead. That’s because the data coming from JavaScript is already a valid JSON. An HTTP server on the other hand sends a raw string which must be parsed first to make sure that it’s a valid JSON. That’s why we had to use decodeString in the Decoding JSON section. The decodeValue function skips the parsing altogether and focuses on translating a valid JSON into an Elm value as shown in the figure below.

Here is the comparison between decodeValue and decodeString’s type signatures.

Modifying the Model

Inside the ReceivedDataFromJS value -> branch of update, we are using a case expression to handle both success and failure scenarios.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        ReceivedDataFromJS value ->
            case decodeValue string value of
                Ok data ->
                    ( { model | dataFromJS = data }, Cmd.none )

                Err error ->
                    ( { model | errorMessage = Just error }, Cmd.none )

If decoding succeeds, we need to assign the decoded value to the dataFromJS field in our model. If it fails, we need to store the error in the errorMessage field. Let’s add those fields to the Model type in PortExamples.elm.

type alias Model =
    { dataFromJS : String
    , errorMessage : Maybe String
    }

We also need to modify init to comply with the new model structure.

init : ( Model, Cmd Msg )
init =
    ( { dataFromJS = ""
      , errorMessage = Nothing
      }
    , Cmd.none
    )

Displaying Error Message

The only thing left is to display the error message produced by decodeValue if decoding fails. Replace the view function in PortExamples.elm with the following code.

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


viewDataFromJSOrError : Model -> Html Msg
viewDataFromJSOrError model =
    case model.errorMessage of
        Just message ->
            viewError message

        Nothing ->
            viewDataFromJS model.dataFromJS


viewError : String -> Html Msg
viewError errorMessage =
    let
        errorHeading =
            "Couldn't receive data from JavaScript"
    in
        div []
            [ h3 [] [ text errorHeading ]
            , text ("Error: " ++ errorMessage)
            ]


viewDataFromJS : String -> Html msg
viewDataFromJS data =
    div []
        [ h3 [] [ text "Received the following data from JavaScript" ]
        , text data
        ]

Our strategy for displaying an error message here is very similar to the one used in the Handling HTTP Errors section.

Testing

We’re now ready to test. Recompile PortExamples.elm by running the following command from the beginning-elm directory in terminal.

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

Everything should compile fine. Reload the Ports/index.html file in a browser and open the browser console. Click the Send Data to JavaScript button. Elm now shows a friendly error message on the page itself instead of crashing the app and pointing out what went wrong in the console.

Let’s fix the error by sending a string instead from the JavaScript code in Ports/index.html.

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

Reload the Ports/index.html file and click the Send Data to JavaScript button. You should see Hey Elm!.

You may be wondering why we had to go through such an elaborate process to receive a simple string from JavaScript. Actually the process for receiving more complex data from JavaScript is also very similar. The only difference is that we’ll need to write decoders that are much more sophisticated than string.

Sending Complex Data to JavaScript

The process for sending complex Elm data to JavaScript is also quite similar to the one we went through in the Sending Data to Javascript section for sending a String.

Why don’t we create some complex data in our Elm app and try to send it to JavaScript to understand this process better? The data we want to send will look like the following when translated to JSON.

{
  "posts": [
    {
      "id": 1,
      "title": "json-server",
      "author": {
        "name": "typicode",
        "url": "https://github.com/typicode"
      }
    },
    {
      "id": 2,
      "title": "http-server",
      "author": {
        "name": "indexzero",
        "url": "https://github.com/indexzero"
      }
    }
  ],
  "comments": [
    {
      "id": 1,
      "body": "some comment",
      "postId": 1
    }
  ],
  "profile": {
    "name": "typicode"
  }
}

Let’s define a few type aliases that describe the structure of various objects in the JSON format shown above. Add the following code right below the Model type in PortExamples.elm.

type alias ComplexData =
    { posts : List Post
    , comments : List Comment
    , profile : Profile
    }


type alias Post =
    { id : Int
    , title : String
    , author : Author
    }


type alias Author =
    { name : String
    , url : String
    }


type alias Comment =
    { id : Int
    , body : String
    , postId : Int
    }


type alias Profile =
    { name : String }

Next add a new field called dataToJS to the Model type in PortExamples.elm.

type alias Model =
    { dataFromJS : String
    , dataToJS : ComplexData
    , errorMessage : Maybe String
    }

Now we need to update init to create an initial value for dataToJS. Replace the current implementation of init with the following in PortExamples.elm;

init : ( Model, Cmd Msg )
init =
    ( initialModel, Cmd.none )


initialModel : Model
initialModel =
    { dataFromJS = ""
    , dataToJS = complexData
    , errorMessage = Nothing
    }


complexData : ComplexData
complexData =
    let
        post1 =
            Author "typicode" "https://github.com/typicode"
                |> Post 1 "json-server"

        post2 =
            Author "indexzero" "https://github.com/indexzero"
                |> Post 2 "http-server"
    in
        { posts = [ post1, post2 ]
        , comments = [ Comment 1 "some comment" 1 ]
        , profile = { name = "typicode" }
        }

Since we want to send more complex data to JavaScript, we need to change the type of sendData outgoing port from String to ComplexData in PortExamples.elm.

port sendData : ComplexData -> Cmd msg

We also need to modify the SendDataToJS -> branch in update to use the dataToJS field in our model instead of "Hello JavaScript!".

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

        ReceivedDataFromJS value ->
            ...

We’re now ready to recompile PortExamples.elm. Run the following command from the beginning-elm directory in terminal.

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

Everything should compile fine. Let’s make a minor change to the callback function in Ports/index.html so that it prints a raw JSON string to the browser console instead of a JavaScript value. That’ll make the output more readable.

<script>
    .
    .
    app.ports.sendData.subscribe(function(data) {
        console.log("Data from Elm: ", JSON.stringify(data));
        app.ports.receiveData.send("Hey Elm!");
    });
</script>

The JSON.stringify function converts a JavaScript value to a JSON string. Reload the Ports/index.html file in a browser and open the browser console. Click the Send Data to JavaScript button and you should see a JSON string in the console.

The runtime translated the individual Elm types contained in complexData to corresponding JavaScript types. All of that translation happened behind the scenes. We didn’t have to do anything other than give the outgoing port function the value we wanted to send.

Summary

One of the biggest advantages of using Elm is that it guarantees there won’t be any type errors when an app is running. To fulfill that guarantee, Elm must reject all incorrectly typed values from entering the app. The runtime will throw an error as soon as it detects an incorrect type trying to sneak through the incoming port.

It’s not a good practice to let Elm crash the app if decoding fails. We can prevent that by decoding the incoming data ourselves. This also allows us to display a friendly message to the users if decoding does fail.

Here is the entire code from PortExamples.elm for your reference:

port module PortExamples exposing (..)

import Html exposing (..)
import Html.Events exposing (..)
import Json.Decode exposing (Value, string, decodeValue)


type alias Model =
    { dataFromJS : String
    , dataToJS : ComplexData
    , errorMessage : Maybe String
    }


type alias ComplexData =
    { posts : List Post
    , comments : List Comment
    , profile : Profile
    }


type alias Post =
    { id : Int
    , title : String
    , author : Author
    }


type alias Author =
    { name : String
    , url : String
    }


type alias Comment =
    { id : Int
    , body : String
    , postId : Int
    }


type alias Profile =
    { name : String }


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


viewDataFromJSOrError : Model -> Html Msg
viewDataFromJSOrError model =
    case model.errorMessage of
        Just message ->
            viewError message

        Nothing ->
            viewDataFromJS model.dataFromJS


viewError : String -> Html Msg
viewError errorMessage =
    let
        errorHeading =
            "Couldn't receive data from JavaScript"
    in
        div []
            [ h3 [] [ text errorHeading ]
            , text ("Error: " ++ errorMessage)
            ]


viewDataFromJS : String -> Html msg
viewDataFromJS data =
    div []
        [ h3 [] [ text "Received the following data from JavaScript" ]
        , text data
        ]


type Msg
    = SendDataToJS
    | ReceivedDataFromJS Value


port sendData : ComplexData -> Cmd msg


port receiveData : (Value -> msg) -> Sub msg


subscriptions : Model -> Sub Msg
subscriptions _ =
    receiveData ReceivedDataFromJS


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

        ReceivedDataFromJS value ->
            case decodeValue string value of
                Ok data ->
                    ( { model | dataFromJS = data }, Cmd.none )

                Err error ->
                    ( { model | errorMessage = Just error }, Cmd.none )


init : ( Model, Cmd Msg )
init =
    ( initialModel, Cmd.none )


initialModel : Model
initialModel =
    { dataFromJS = ""
    , dataToJS = complexData
    , errorMessage = Nothing
    }


complexData : ComplexData
complexData =
    let
        post1 =
            Author "typicode" "https://github.com/typicode"
                |> Post 1 "json-server"

        post2 =
            Author "indexzero" "https://github.com/indexzero"
                |> Post 2 "http-server"
    in
        { posts = [ post1, post2 ]
        , comments = [ Comment 1 "some comment" 1 ]
        , profile = { name = "typicode" }
        }


main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }
Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close