Here's how to get two Elm apps on the same page sending and receiving data via Elm's ports system.

This isn't something you're going to want to do in every situation, but it's a useful way to learn how to use ports.

I'll start with an optional video walkthrough, but feel free to scroll down for the code instead.

Here's the code.

The first Elm app is a copy of the basic counter called Buttons from the Elm Guide.

It looks like this:

port module WidgetA exposing (main)

import Browser
import Html exposing (..)
import Html.Events exposing (onClick)


-- MODEL

type alias Model = Int

init : flags -> ( Model, Cmd Msg )
init flags =
  ( 0, Cmd.none )


-- VIEW

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , button [ onClick Increment ] [ text "+" ]
    ]


-- UPDATE

type Msg = Increment | Decrement

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    Increment ->
      let
          updated = model + 1
      in
      ( updated, countOutput updated )

    Decrement ->
      let
          updated = model - 1
      in
      ( updated, countOutput updated )


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }


port countOutput : Int -> Cmd msg

This app is almost identical to the version from the Guide. The first change is here:

port module WidgetA exposing (main)

When you're making an Elm module that uses ports, you need to explicitly tell the compiler by declaring it with port module.

The next change is here:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    Increment ->
      let
          updated = model + 1
      in
      ( updated, countOutput updated )

    Decrement ->
      let
          updated = model - 1
      in
      ( updated, countOutput updated )

Where the update function in the basic tutorial counter app simply returns the model — that is to say, the number being incremented or decremented — this app also includes a Cmd, and instead of the Cmd.none seen so often in Elm apps, the Cmd here is countOutput updated.

The last change in this file tells the compiler what countOutput is:

port countOutput : Int -> Cmd msg

It's a port which takes an Int. (Ports can only take types that JSON supports, i.e., strings, integers, floats, booleans, nulls, arrays, or JS objects made up of those types.)

The last big difference between this app called WidgetA and the Buttons app I based it on: WidgetA has no model in its view. Instead, the WidgetA view just shows the UI which you use to increment or decrement the counter. The goal here is to demo sending data from one Elm app to another, so I took that part of the view out of WidgetA and put it another app called WidgetB.

Here's that app:

port module WidgetB exposing (main)

import Browser
import Html exposing (..)
import Html.Events exposing (onClick)


type alias Model =
    Int


view : Model -> Html Msg
view model =
    div []
        [ text ("Data received from JavaScript: " ++ String.fromInt model)
        ]


type Msg
    = ReceivedDataFromJS Model


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( model, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions _ =
    countInput ReceivedDataFromJS


port countInput : (Model -> msg) -> Sub msg


init : () -> ( Model, Cmd Msg )
init _ =
    ( 0, Cmd.none )


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

Like WidgetA, WidgetB is cobbled together from the Buttons example, and a couple other great tutorial examples on elmprogramming.com.

You have to declare port module for this app, just like you did for the other one.

Declaring the port is almost the same as the first one too:

port countInput : (Model -> msg) -> Sub msg

The difference is that countInput is a port which returns a Sub.

That means that the WidgetB app needs to declare it in the subscriptions function:

subscriptions : Model -> Sub Msg
subscriptions _ =
    countInput ReceivedDataFromJS

Subscriptions are where you put code which handles input from outside the system. This subscription simply fires off a ReceivedDataFromJS message, which the update function responds to:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( model, Cmd.none )

The code doesn't do anything with the ReceivedDataFromJS type, going straight to the model instead, but obviously a UI with more interactivity would include ReceivedDataFromJS in a big case statement covering the other messages that the UI can send.

Anyway, with both widgets set up, you need HTML for the Elm apps to live in, plus a little glue code in JavaScript so the ports can communicate with each other.

Here's what that looks like:

<!DOCTYPE html>
<html>
<body>
    <div id="widget-a"></div>

    <div id="widget-b"></div>

    <script src="widget-a.js"></script>
    <script src="widget-b.js"></script>
    <script>
      var widgetA = Elm.WidgetA.init({
        node: document.getElementById("widget-a")
      });

      var widgetB = Elm.WidgetB.init({
        node: document.getElementById("widget-b")
      });

      widgetA.ports.countOutput.subscribe(function(data) {
        widgetB.ports.countInput.send(data);
      });
    </script>
</body>
</html>

Most of this is Elm boilerplate. Here's the important part:

widgetA.ports.countOutput.subscribe(function(data) {
  widgetB.ports.countInput.send(data);
});

The above code tells the first Elm app to send all of its output from countOutput to an anonymous function which directs that output into the input port countInput for the second Elm app. This is how you get widgetA.ports.countOutput flowing into widgetB.ports.countInput.

(Note: the naming here is a little awkward. I'm still figuring out the right way to name a port. In the video, I used count as the name for both ports, but on reflection, I like countOutput and countInput better, because they make it explicit that each app has its own unique set of ports.)

So there you have it: a simple demonstration of how to connect one Elm app to another using ports.