How to get websockets working in Elm 0.19

Here is a minimal working example of an interactive form to echo input from echo.websocket.org using 2 simple input/output ports for communicating with a JavaScript WebSocket object external to the elm 0.19 module:

File: echo.elm. Compile with: elm make echo.elm --output=echo.js

port module Main exposing (main)

import Browser
import Html exposing (Html)
import Html.Attributes as HA
import Html.Events as HE
import Json.Encode as JE

-- JavaScript usage: app.ports.websocketIn.send(response);
port websocketIn : (String -> msg) -> Sub msg
-- JavaScript usage: app.ports.websocketOut.subscribe(handler);
port websocketOut : String -> Cmd msg

main = Browser.element
    { init = init
    , update = update
    , view = view
    , subscriptions = subscriptions
    }

{- MODEL -}

type alias Model =
    { responses : List String
    , input : String
    }

init : () -> (Model, Cmd Msg)
init _ =
    ( { responses = []
      , input = ""
      }
    , Cmd.none
    )

{- UPDATE -}

type Msg = Change String
    | Submit String
    | WebsocketIn String

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Change input ->
      ( { model | input = input }
      , Cmd.none
      )
    Submit value ->
      ( model
      , websocketOut value
      )
    WebsocketIn value ->
      ( { model | responses = value :: model.responses }
      , Cmd.none
      )

{- SUBSCRIPTIONS -}

subscriptions : Model -> Sub Msg
subscriptions model =
    websocketIn WebsocketIn

{- VIEW -}

li : String -> Html Msg
li string = Html.li [] [Html.text string]

view : Model -> Html Msg
view model = Html.div []
    --[ Html.form [HE.onSubmit (WebsocketIn model.input)] -- Short circuit to test without ports
    [ Html.form [HE.onSubmit (Submit model.input)]
      [ Html.input [HA.placeholder "Enter some text.", HA.value model.input, HE.onInput Change] []
      , model.responses |> List.map li |> Html.ol []
      ]
    ]

Embed the compiled echo.js into echo.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Echo</title>
  <script src="echo.js"></script>
</head>
<body>
  <div id="elm-node"></div>
  <script>
    var app = Elm.Main.init({node: document.getElementById("elm-node")});
    var ws = new WebSocket("wss://echo.websocket.org");
    ws.onmessage = function(message)
    {
        console.log(message);
        app.ports.websocketIn.send(JSON.stringify({data:message.data,timeStamp:message.timeStamp}));
    };
    app.ports.websocketOut.subscribe(function(msg) { ws.send(msg); });
  </script>
</body>
</html>

This works on Firefox 60.2.0esr on Linux but has not been tested on other platforms.

Again, this is only a minimal example to demonstrate how to use ports with WebSockets for Elm 0.19. It does not included closing the WebSocket, error handling, etc. but hopefully this example can help you get started in that direction. It is expected that WebSockets will be directly supported by Elm again soon, so this is only a temporary work-around. If you don't need to upgrade to 0.19, then consider staying with 0.18 instead.


The websocket package is currently redesigned for Elm 0.19, see this issue:

This package has not been updated for 0.19 yet. I have heard lots of folks saying they need more features from this package, so I'd rather take that into consideration in the update rather than just doing the same stuff. I recommend using ports or 0.18 if you absolutely need this right this second.

EDIT: April 15, 2020 update

The package has been archived and the Readme file updated as follows:

The recommended way to use WebSockets with Elm for now is through ports. You can see a minimal example in the js-integration-examples repo [IMAGE CLIPPED]

History

We had a bare bones version of WebSockets in within Elm in versions 0.17 and 0.18, part of the introduction of subscriptions to Elm. But users found that the API was not able to cover a lot of situations they faced in practice. How can this work with Elixir Pheonix? Firebase? How can I use a different backoff strategy for reconnecting? How can I hear about when the connection goes down or comes back? How about sub-protocols?

In trying to expand the API to cover all the cases people were facing in practice, I came to think that it may not be possible with the current subscriptions infrastructure. (My feeling is that effect managers may not be a great fit for web sockets because they do not have great mechanisms for uniquely identifying resources. Do we have one connections or two? How do we tell the difference? If it requires function pointer equality, how can we make that reliable when an anonymous function is used?) I did not understand this problem as well in 2016, and I think it manifested most clearly with web sockets.

So facing the prospect of either (1) having an API that many eventually had to leave behind for ports or (2) recommending that people go with ports from the start, we figured that (2) was probably the best for someone new coming to Elm. That way they would hook up to their preferred web socket manager without the intermediate step of learning a promising but incomplete API.

Tags:

Websocket

Elm