8.4

Receiving Data from JavaScript

Earlier we learned how to send data from Elm to JavaScript. In this section, we’ll go the other way and send data from JavaScript to Elm. The good news is we can use a port for that too. Let’s add a new port function called receiveData right below the update function in src/PortExamples.elm.

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

Note: Going forward we’ll refer to the port for sending data to JavaScript as an outgoing port and the port for receiving data from JavaScript as an incoming port.

The following diagram shows the difference between an outgoing and incoming port.

Unlike the command (returned by the outgoing port), we actually want the subscription (returned by the incoming port) to send a message to our app whenever the JavaScript code sends some data.

We’ll be storing whatever value JavaScript sends in our model. Let’s add a new data constructor called ReceivedDataFromJS to the Msg type in PortExamples.elm that takes the model as an input and returns a message.

type Msg
    = SendDataToJS
    | ReceivedDataFromJS Model

Next we need to handle ReceivedDataFromJS in update.

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

        ReceivedDataFromJS data ->
            ( data, Cmd.none )

All we’re doing here is return whatever data JavaScript sends as the model. Now we need to display the model. Modify the view function in PortExamples.elm like this:

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendDataToJS ]
            [ text "Send Data to JavaScript" ]
        , br [] []
        , br [] []
        , text ("Data received from JavaScript: " ++ model)
        ]

Next we need to ask the Elm runtime to listen to the data coming from JavaScript by giving it the subscription returned by the receiveData port function. Create a function called subscriptions right below update in PortExamples.elm.

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

Remember, the runtime passes the model to subscriptions. Since we don’t need anything in the model to create a subscription, we’re ignoring that parameter with _.

If you pay close attention to the return type of subscriptions, you’ll notice that it uses Msg whereas the return type of receiveData port uses msg. Shouldn’t they both match? Not in this case. Let’s try changing the return type of subscriptions in PortExamples.elm to msg and see what happens.

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

Run the following command from the beginning-elm directory in terminal to compile PortExamples.elm.

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

You should see the following error.

---------------- TYPE MISMATCH ---------------- src/PortExamples.elm
Something is off with the body of the `subscriptions` definition:

36|     receiveData ReceivedDataFromJS
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This `receiveData` call produces:

    Sub Msg

But the type annotation on `subscriptions` says it should be:

    Sub msg

Hint: Your type annotation uses type variable `msg` which means ANY type of
value can flow through, but your code is saying it specifically wants a `Msg`
value. Maybe change your type annotation to be more specific? Maybe change the
code to be more general?

The error message tells us exactly why we must use Sub Msg as subscriptions’s return type. Let’s change the return type back to Sub Msg.

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

What about receiveData’s return type? Can we change it to Sub Msg so they both match? Let’s try it.

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

Once again run the following command from the beginning-elm directory in terminal to compile PortExamples.elm.

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

Now you should see a different error.

------------------ BAD PORT ----------------- src/PortExamples.elm
There is something off about this `receiveData` port declaration.

31| port receiveData : (Model -> msg) -> Sub Msg
         ^^^^^^^^^^^
To receive messages from JavaScript, you need to define a port like this:

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

Now every time JS sends an `Int` to this port, it is converted to a `msg`. 
And if you subscribe, those `msg` values will be piped into your `update`
function. The only thing you can customize here is the `Int` type.

As it turns out, Elm expects us to use a particular format when defining a port and that format requires us to use a type variable msg instead of a concrete type such as Msg. Let’s change receiveData’s return type back to Sub msg.

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

msg is less restrictive than Msg. That’s why Elm allows us to pass a data constructor with the type Model -> Msg to receiveData even if it’s expecting Model -> msg. The only thing remaining on the Elm side is to assign the subscriptions function to the subscriptions field in main.

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

Recompile PortExamples.elm by running the following command from the beginning-elm directory in terminal.

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

Everything should compile fine. Now we need to write some JavaScript code for sending data to Elm. Modify the callback function we passed to app.ports.sendData.subscribe in beginning-elm/index.html like this:

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

We can access all ports defined in our Elm app through app.ports. To send data to Elm, we need to use the send function instead of subscribe. Open index.html in a browser and click the Send Data to JavaScript button. You should see Hey Elm! on the page.

The following diagram illustrates the entire workflow for sending and receiving data from JavaScript.

Summary

In this section, we learned how to receive data from JavaScript using an incoming port. The incoming port returns a subscription which is given to the Elm runtime when the app is being initialized. Once the app is fully running, the runtime will start listening for any data coming from JavaScript. When data arrives, the runtime will route it to our update function by sending a message. 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" ]
        , br [] []
        , br [] []
        , text ("Data received from JavaScript: " ++ model)
        ]


type Msg
    = SendDataToJS
    | ReceivedDataFromJS Model


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

        ReceivedDataFromJS data ->
            ( data, Cmd.none )


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


port sendData : String -> Cmd msg


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


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


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