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.

    1. port module Main exposing (..)
    2. import Browser
    3. import Html exposing (..)
    4. import Html.Attributes exposing (..)
    5. import Html.Events exposing (..)
    6. import Json.Decode as D
    7. -- MAIN
    8. main : Program () Model Msg
    9. main =
    10. Browser.element
    11. { init = init
    12. , view = view
    13. , update = update
    14. , subscriptions = subscriptions
    15. }
    16. -- PORTS
    17. port sendMessage : String -> Cmd msg
    18. port messageReceiver : (String -> msg) -> Sub msg
    19. -- MODEL
    20. type alias Model =
    21. { draft : String
    22. , messages : List String
    23. }
    24. init : () -> ( Model, Cmd Msg )
    25. init flags =
    26. ( { draft = "", messages = [] }
    27. , Cmd.none
    28. )
    29. -- UPDATE
    30. type Msg
    31. = DraftChanged String
    32. | Send
    33. | Recv String
    34. -- Use the `sendMessage` port when someone presses ENTER or clicks
    35. -- the "Send" button. Check out index.html to see the corresponding
    36. -- JS where this is piped into a WebSocket.
    37. --
    38. update : Msg -> Model -> ( Model, Cmd Msg )
    39. update msg model =
    40. case msg of
    41. DraftChanged draft ->
    42. ( { model | draft = draft }
    43. , Cmd.none
    44. )
    45. Send ->
    46. ( { model | draft = "" }
    47. , sendMessage model.draft
    48. )
    49. Recv message ->
    50. ( { model | messages = model.messages ++ [message] }
    51. , Cmd.none
    52. )
    53. -- SUBSCRIPTIONS
    54. -- Subscribe to the `messageReceiver` port to hear about messages coming in
    55. -- from JS. Check out the index.html file to see how this is hooked up to a
    56. --
    57. subscriptions : Model -> Sub Msg
    58. subscriptions _ =
    59. messageReceiver Recv
    60. -- VIEW
    61. view : Model -> Html Msg
    62. view model =
    63. div []
    64. [ h1 [] [ text "Echo Chat" ]
    65. , ul []
    66. (List.map (\msg -> li [] [ text msg ]) model.messages)
    67. [ type_ "text"
    68. , placeholder "Draft"
    69. , onInput DraftChanged
    70. , on "keydown" (ifIsEnter Send)
    71. , value model.draft
    72. ]
    73. []
    74. , button [ onClick Send ] [ text "Send" ]
    75. ]
    76. -- DETECT ENTER
    77. ifIsEnter : msg -> D.Decoder msg
    78. ifIsEnter msg =
    79. D.field "key" D.string
    80. |> 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:

    1. app.ports.sendMessage.subscribe(function(message) {
    2. socket.send(message);
    3. });

    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.

    1. socket.addEventListener("message", function(event) {
    2. app.ports.messageReceiver.send(event.data);
    3. });

    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 a port module. It is probably best to organize all your ports into one port 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.