Super Simple Communication Between Browser Tabs for Elm

01 May 2017

tl;dr use ports and localstorage

Why

Some reasons why you may want to communicate between browser tabs:

  1. Keep data from an API in sync/cached
  2. Login a user in all open tabs when they login in one
  3. Store auth tokens
  4. Use one tab to control the UI in another

In the rest of this post, I will show you a super simple example of how to talk between tabs in Elm using ports and localstorage.

Example

Click the buttons to change the counter value - then open this page in another tab, you will see the counter update in both places.

How it works

Every time we change our model in Elm, we send it out a port in our update function.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    let
        newModel =
            updateHelper msg model
    in
        ( newModel, saveToStorage newModel )


port saveToStorage : Int -> Cmd msg

On the JS side, this port saves the model to localstorage.

  app.ports.saveToStorage.subscribe(function(m) {
    // save our model to local storage
    localStorage.setItem(lsKey, JSON.stringify(m));
  });

Still in JS land, we have an event listener that subsribes to localstorage: if our model is updated in localstorage, then we send it back into a port in our Elm app.

  window.addEventListener('storage', function (event) {
    if (event.key === lsKey) {
      // if the model changes, pass it into elm
      app.ports.fromStorage.send(event.newValue);
    }
  });

In elm, we then decode the value and put it in our model.

port fromStorage : (Maybe String -> msg) -> Sub msg


subscriptions model =
    fromStorage FromStorage


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    let
        newModel =
            updateHelper msg model
    in
        ( newModel, saveToStorage newModel )


updateHelper : Msg -> Model -> Model
updateHelper msg model =
    case msg of
        Increment ->
            model + 1

        Decrement ->
            model - 1

        FromStorage ms ->
            decodeLocalStorage ms


decodeLocalStorage : Maybe String -> Int
decodeLocalStorage ms =
    case ms of
        Nothing ->
            0

        Just s ->
            Json.Decode.decodeString Json.Decode.int s
                |> Result.withDefault 0

You can see the full source code in this gist. If you want to see a more complicated example, have a look at apostello.