Ports
Ports allow communication between Elm and JavaScript.
Ports are probably most commonly used for WebSockets
and localStorage
. Let's focus on the WebSockets
example.
Ports in JavaScript
Here we have pretty much the same HTML we have been using on the previous pages, but with a bit of extra JavaScript code in there. We create a connection to wss://echo.websocket.org
that just repeats back whatever you send it. You can see in the live example that this lets us make the skeleton of a chat room:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Elm + Websockets</title>
<script type="text/javascript" src="elm.js"></script>
</head>
<body>
<div id="myapp"></div>
</body>
<script type="text/javascript">
// Start the Elm application.
var app = Elm.Main.init({
node: document.getElementById('myapp')
});
// Create your WebSocket.
var socket = new WebSocket('wss://echo.websocket.org');
// When a command goes to the `sendMessage` port, we pass the message
// along to the WebSocket.
app.ports.sendMessage.subscribe(function(message) {
socket.send(message);
});
// When a message comes into our WebSocket, we pass the message along
// to the `messageReceiver` port.
socket.addEventListener("message", function(event) {
app.ports.messageReceiver.send(event.data);
});
// If you want to use a JavaScript library to manage your WebSocket
// connection, replace the code in JS with the alternate implementation.
</script>
</html>
We call Elm.Main.init()
like in all of our interop examples, but this time we are actually using the resulting app
object. We are subscribing to the sendMessage
port and we are sending to the messageReceiver
port.
Those correspond to code written on the Elm side.
Ports in Elm
Check out the lines that use the port
keyword in the corresponding Elm file. This is how we define the ports that we just saw on the JavaScript side.
port module Main exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as D
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- PORTS
port sendMessage : String -> Cmd msg
port messageReceiver : (String -> msg) -> Sub msg
-- MODEL
type alias Model =
{ draft : String
, messages : List String
}
init : () -> ( Model, Cmd Msg )
init flags =
( { draft = "", messages = [] }
, Cmd.none
)
-- UPDATE
type Msg
= DraftChanged String
| Send
| Recv String
-- Use the `sendMessage` port when someone presses ENTER or clicks
-- the "Send" button. Check out index.html to see the corresponding
-- JS where this is piped into a WebSocket.
--
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DraftChanged draft ->
( { model | draft = draft }
, Cmd.none
)
Send ->
( { model | draft = "" }
, sendMessage model.draft
)
Recv message ->
( { model | messages = model.messages ++ [message] }
, Cmd.none
)
-- SUBSCRIPTIONS
-- Subscribe to the `messageReceiver` port to hear about messages coming in
-- from JS. Check out the index.html file to see how this is hooked up to a
-- WebSocket.
--
subscriptions : Model -> Sub Msg
subscriptions _ =
messageReceiver Recv
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text "Echo Chat" ]
, ul []
(List.map (\msg -> li [] [ text msg ]) model.messages)
, input
[ type_ "text"
, placeholder "Draft"
, onInput DraftChanged
, on "keydown" (ifIsEnter Send)
, value model.draft
]
[]
, button [ onClick Send ] [ text "Send" ]
]
-- DETECT ENTER
ifIsEnter : msg -> D.Decoder msg
ifIsEnter msg =
D.field "key" D.string
|> D.andThen (\key -> if key == "Enter" then D.succeed msg else D.fail "some other key")
Notice that the first line says port module
rather than just module
. This makes it possible to define ports in a given module. The compiler gives a hint about this if it is needed, so hopefully no one gets too stuck on that!
Okay, but what is going on with the port
declarations for sendMessage
and messageReceiver
?
Outgoing Messages (Cmd
)
The sendMessage
declaration lets us send messages out of Elm.
port sendMessage : String -> Cmd msg
Here we are declaring that we want to send out String
values, but we could send out any of the types that work with flags. We talked about those types on the previous page, and you can check out this localStorage
example to see a Json.Encode.Value
getting sent out to JavaScript.
From there we can use sendMessage
like any other function. If your update
function produces a sendMessage "hello"
command, you will hear about it on the JavaScript side:
app.ports.sendMessage.subscribe(function(message) {
socket.send(message);
});
This JavaScript code is subscribed to all of the outgoing messages. You can subscribe
multiple functions and unsubscribe
functions by reference, but we generally recommend keeping things static.
We also recommend sending out richer messages, rather than making lots of individual ports. Maybe that means having a custom type in Elm that represents everything you might need to tell JS, and then using Json.Encode
to send it out to a single JS subscription. Many people find that this creates a cleaner separation of concerns. The Elm code clearly owns some state, and the JS clearly owns other state.
Incoming Messages (Sub
)
The messageReceiver
declaration lets us listen for messages coming in to Elm.
port messageReceiver : (String -> msg) -> Sub msg
We are saying we are going to receive String
values, but again, we can listen for any type that can come in through flags or outgoing ports. Just swap out the String
type with one of the types that can cross the border.
Again we can use messageReceiver
like any other function. In our case we call messageReceiver Recv
when defining our subscriptions
because we want to hear about any incoming messages from JavaScript. This will let us get messages like Recv "how are you?"
in our update
function.
On the JavaScript side, we are able to send things to this port whenever we want:
socket.addEventListener("message", function(event) {
app.ports.messageReceiver.send(event.data);
});
We happen to be sending whenever the websocket gets a message, but you could send at other times as well. Maybe we are getting messages from another data source as well. That is fine, and Elm does not need to know anything about it! Just send the strings through the relevant port.
Notes
Ports are about creating strong boundaries! Definitely do not try to make a port for every JS function you need. You may really like Elm and want to do everything in Elm no matter the cost, but ports are not designed for that. Instead, focus on questions like “who owns the state?” and use one or two ports to send messages back and forth. If you are in a complex scenario, you can even simulate Msg
values by sending JS like { tag: "active-users-changed", list: ... }
where you have a tag for all the variants of information you might send across.
Here are some simple guidelines and common pitfalls:
Sending
Json.Encode.Value
through ports is recommended. Like with flags, certain core types can pass through ports as well. This is from the time before JSON decoders, and you can read about it more here.All
port
declarations must appear in aport module
. It is probably best to organize all your ports into oneport module
so it is easier to see the interface all in one place.Ports are for applications. A
port module
is available in applications, but not in packages. This ensures that application authors have the flexibility they need, but the package ecosystem is entirely written in Elm. We think this will create a stronger ecosystem and community in the long run, and we get into the tradeoffs in depth in the upcoming section on the limits of Elm/JS interop.Ports can be dead code eliminated. Elm has quite aggressive dead code elimination, and it will remove ports that are not used within Elm code. The compiler does not know what goes on in JavaScript, so try to hook things up in Elm before JavaScript.
I hope this information will help you find ways to embed Elm in your existing JavaScript! It is not as glamorous as doing a full-rewrite in Elm, but history has shown that it is a much more effective strategy.