Ports
Ports are probably most commonly used for WebSockets
and . Let’s focus on the WebSockets
example.
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:
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.
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
--
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)
[ 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
?
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 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 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.
The messageReceiver
declaration lets us listen for messages coming in to Elm.
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.
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.
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 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.