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 it 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 a new directory called Ports
in beginning-elm
.
Add the following code to PortExamples.elm
.
module PortExamples exposing (..)
import Html exposing (..)
import Html.Events exposing (..)
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 Never Model Msg
main =
Html.program
{ 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 specific functions such as Random.generate
, Http.send
, and RemoteData.sendRequest
to create a command.
Random.generate : (a -> msg) -> Generator a -> Cmd msg
Http.send : (Result Error a -> msg) -> Request a -> Cmd msg
RemoteData.sendRequest : Request a -> Cmd (WebData a)
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
right above the update
function.
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
, Http.send
, and RemoteData.sendRequest
, 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 applying a port function is identical to that of a regular function. When the update
function receives the SendDataToJs
message, it’ll use 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
to JavaScript. Run the following command from the beginning-elm
directory in terminal.
$ elm-make Ports/PortExamples.elm --output Ports/elm.js
Unfortunately, the Elm compiler throws an error.
------------------------ BAD PORT -------------------- Ports/PortExamples.elm
You are declaring port `sendData` in a normal module.
23| port sendData : String -> Cmd msg
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All ports must be defined in a `port module`. You should probably have just one
of these for your project. This way all of your foreign interactions stay
relatively organized.
Detected errors in 1 module.
It’s saying that we should declare sendData
in a port module instead of a normal 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 modifying the first line in PortExamples.elm
to this:
port module PortExamples exposing (..)
Run the following command one more time from the beginning-elm
directory in terminal.
$ elm-make Ports/PortExamples.elm --output Ports/elm.js
The error should go away and you should see a file called elm.js
in the beginning-elm/Ports
directory. Now we need to write some JavaScript code to receive the string sent by the Elm app on the other end. Let’s create a new file called index.html
in the beginning-elm/Ports
directory and add the code below to it.
<!DOCTYPE html>
<html>
<body>
<div id="elm-code-is-loaded-here"></div>
<script src="elm.js"></script>
<script>
var element = document.getElementById("elm-code-is-loaded-here");
var app = Elm.PortExamples.embed(element);
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
.
var element = document.getElementById("elm-code-is-loaded-here");
Step 2: Embed the Elm app contained in the PortExamples
module into the div
from step 1.
var app = Elm.PortExamples.embed(element);
Step 3: Listen to the sendData
port using the subscribe
method. When the data arrives, print it to the browser console.
app.ports.sendData.subscribe(function(data) {
console.log("Data from Elm: ", data);
});
All the ports defined in our Elm app can be accessed through app.ports
. Finally, we’re ready to test. Open the Ports/index.html
file in a browser. After that open the browser console. Now if you click the Send Data to JavaScript button, 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 how to receive data from JavaScript.
Here is the entire code from PortExamples.elm
for your reference:
port module PortExamples exposing (..)
import Html exposing (..)
import Html.Events exposing (..)
type alias Model =
String
view : Model -> Html Msg
view model =
div []
[ button [ onClick SendDataToJS ]
[ text "Send Data to JavaScript" ]
]
type Msg
= SendDataToJS
port sendData : String -> Cmd msg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SendDataToJS ->
( model, sendData "Hello JavaScript!" )
init : ( Model, Cmd Msg )
init =
( "", Cmd.none )
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}