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
}