Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active March 21, 2019 17:37
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save evancz/8521339 to your computer and use it in GitHub Desktop.
Save evancz/8521339 to your computer and use it in GitHub Desktop.
Example usage of Elm "ports" that uses signals, non-signals, records, and tuples. Build the Elm code with "elm --only-js Shanghai.elm" and include all of the JS in the HTML document.
// initialize the Shanghai component which keeps track of
// shipping data in and out of the Port of Shanghai.
var shanghai = Elm.worker(Elm.Shanghai, {
coordinates:[0,0],
incomingShip: { name:"", capacity:0 },
outgoingShip: ""
});
function logger(x) { console.log(x) }
shanghai.ports.totalCapacity.subscribe(logger);
// send some ships to the port of Shanghai
shanghai.ports.incomingShip.send({
name:"Mary Mærsk",
capacity:18270
});
shanghai.ports.incomingShip.send({
name:"Emma Mærsk",
capacity:15500
});
// have those ships leave the port of Shanghai
shanghai.ports.outgoingShip.send("Mary Mærsk");
shanghai.ports.outgoingShip.send("Emma Mærsk");
module Shanghai where
import Either (..)
import Dict
port coordinates : (Int,Int)
port incomingShip : Signal { name:String, capacity:Int }
port outgoingShip : Signal String
ships = merge (Right <~ incomingShip) (Left <~ outgoingShip)
updateDocks ship docks =
case ship of
Right {name,capacity} -> Dict.insert name capacity docks
Left name -> Dict.remove name docks
dock = foldp updateDocks Dict.empty ships
port totalCapacity : Signal Int
port totalCapacity = lift (sum . Dict.values) dock
<html>
<head>
<title>Embedding Elm in HTML!</title>
<script type="text/javascript" src="http://elm-lang.org/elm-runtime.js"></script>
<script type="text/javascript" src="build/Shanghai.js"></script>
</head>
<body>
<h1>Ports of Shanghai</h1>
<p>Check out the developer console. Try sending values to <code>shanghai.ports.incomingShip</code>.</p>
</body>
<script type="text/javascript" src="Ports.js"></script>
</html>
@cakesmith
Copy link

I found that I had to change Shanghai.elm to:

module Shanghai where

import Result (..)
import Signal (..)
import Dict
import List (sum)

 -- Input ports

port coordinates  : (Int,Int)
port incomingShip : Signal { name:String, capacity:Int }
port outgoingShip : Signal String

ships = merge (Ok <~ incomingShip) (Err <~ outgoingShip)

updateDocks ship docks =
    case ship of
      Ok {name,capacity} -> Dict.insert name capacity docks
      Err name -> Dict.remove name docks

dock = foldp updateDocks Dict.empty ships

 -- Output ports

port totalCapacity : Signal Int
port totalCapacity = (sum << Dict.values) <~ dock

@dpwiz
Copy link

dpwiz commented Mar 20, 2015

@cakesmith: Thanks for the heads up. While Result is definitely is a spiritual successor to Either I think it is more correct to update to a custom type like Action = Dock String Int | Undock String since going out of the port is not really an "error".

@spacious
Copy link

spacious commented Aug 1, 2015

Thank you very much for Elm - really enjoying it - anyone know what happened to elm --only-js??

@daniloisr
Copy link

@spacious try to use elm-make Shanghai.elm

@mgold
Copy link

mgold commented Dec 29, 2015

You'd now use elm make Shanghai.elm --output=elm.js, where the file extension controls whether HTML or JS is written.

@booch
Copy link

booch commented Feb 2, 2016

I had to change Shanghai.elm, mostly to replace the map operator in @cakesmith's version:

module Shanghai where

import Signal exposing (..)
import Dict
import List exposing (sum)

 -- Input ports

port coordinates  : (Int,Int)
port incomingShip : Signal { name:String, capacity:Int }
port outgoingShip : Signal String

ships = merge (Signal.map Ok incomingShip) (Signal.map Err outgoingShip)

updateDocks ship docks =
    case ship of
      Ok {name,capacity} -> Dict.insert name capacity docks
      Err name -> Dict.remove name docks

dock = foldp updateDocks Dict.empty ships

 -- Output ports

port totalCapacity : Signal Int
port totalCapacity = Signal.map (sum << Dict.values) dock

@trotha01
Copy link

just for completeness, if elm make Shanghai.elm --output=elm.js is used to compile, index.html will need
<script type="text/javascript" src="elm.js"></script>

@shamansir
Copy link

shamansir commented Nov 15, 2016

The example with Elm v0.18, ports, headless program and sending a single value to JS from the start of the program (no Html at all): https://gist.github.com/shamansir/1e9b22851f1c5d97ad9e66682f95b754 (not reliable anymore)

Since you can not send some value just immediately from the headless program (or else subscriber won't receive it), I was required to trigger sending it with incoming port from JS, just after the subscription was performed.

@peerreynders
Copy link

An attempt at a remaster for 0.18:

port module Shanghai exposing (..)
{-  Shanghai.elm - Updated for 0.18
    Build with: $elm-make --warn --output Shanghai.js Shanghai.elm
 -}

import Json.Decode
-- https://github.com/elm-lang/elm-make/issues/127

import Dict exposing (Dict)

type alias Model =
    Dict String Int

type alias ShipInfo =
    { name : String
    , capacity : Int
    }

-- Browser-bound (-> Cmd msg)
port totalCapacity : Int -> Cmd msg

-- Elm-bound (-> Sub msg)
port incomingShip : (ShipInfo -> msg) -> Sub msg
port outgoingShip : (String -> msg) -> Sub msg

type Msg            -- see also "subscriptions" below
    = Dock ShipInfo -- ShipInfo as specified by incomingShip
    | Sail String   -- ship name as specified by outgoingShip

{-
    Specify which Msg data constructors to use for
    the various Elm-bound port values
-}
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ incomingShip Dock
        , outgoingShip Sail
        ]

tallyCapacity : Model -> Int
tallyCapacity  =
    List.sum << Dict.values

{-  1. "regular" boring version
    tallyCapacity model =
        List.sum (Dict.values model)
    2. Use backward function application (<|) to "avoid parentheses"
    tallyCapacity model =
        List.sum <| Dict.values model
    3. Use function composition (<<) for a "pointfree style"
       representation (parameter is implied and therefore "unseen")
    tallyCapacity  =
        List.sum << Dict.values
    4. Alternately with (>>) 
    tallyCapacity  =
        Dict.values >> List.sum
-}

dock : ShipInfo -> Model -> ( Model , Cmd Msg )
dock info model =
    let
        newModel = Dict.insert info.name info.capacity model
    in
        ( newModel, totalCapacity <| tallyCapacity newModel )

sail : String -> Model -> ( Model, Cmd Msg )
sail name model =
    let
        newModel = Dict.remove name model
    in
        ( newModel, tallyCapacity newModel |> totalCapacity )
        -- alternately used forward function application (|>) instead

{-  Alternately Platform.programWithFlags
    accepts initialization data to be passed
    to "init" function
-}
init : ( Model, Cmd msg )
init =
    ( Dict.empty, totalCapacity 0 )
--  ( Dict.empty, Cmd.none )
--  as an alternate if initial capacity report is not desired. 

-- Process the incoming values and dispatch the updated capacity
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Dock info ->
            dock info model

        Sail name ->
            sail name model

{-
  Platform.program is "headless" - it does not
  generate an HTML view
-}
main : Program Never Model Msg
main =
    Platform.program
        { init = init
        , update = update
        , subscriptions = subscriptions
        }
/* ports.js - Chrome 55.0.2883.95 (64-bit)

   It is assumed that "this.Elm" is set
   and valid for this IIFE (i.e. "Window.Elm.Shanghai"
   was set by Shanghai.js beforehand).
 */
; (function() {

  function connectElmWorker(elm) {
    function logDockedCapacity (value) {
      console.log(
        '[' + (new Date).toISOString() + ']: ' + value
      );
    }

    /*  "Elm.Shanghai" as in "module Shanghai"
           from file "Shanghai.elm""
           compiled into file "Shanghai.js.
     */
    let elmWorker = elm.Shanghai.worker();

    // log docked capacity changes
    elmWorker.ports.totalCapacity.subscribe(logDockedCapacity);

    return elmWorker;
  }

  function dock(worker, name, capacity) {
    worker.ports.incomingShip.send({
      name: name,
      capacity: capacity
    });
  }

  function sail(worker, name) {
    worker.ports.outgoingShip.send(name);
  }

  function scheduleActions(shanghai) {
    function dockAt(worker, name, capacity){
      return dock.bind(null, worker, name, capacity);
    }
    function sailFrom(worker, name) {
      return sail.bind(null, worker, name);
    }
    function at(delay, func) {
      setTimeout(func, delay);
    }

    const mary = 'Mary Mærsk',
          emma = 'Emma Mærsk';

    // send some ships to the port of Shanghai
    at(500, dockAt(shanghai, mary, 18270));
    at(1000, dockAt(shanghai, emma, 15500));

    // have these ships leave the port of Shanghai
    at(1500, sailFrom(shanghai, mary));
    at(2000, sailFrom(shanghai, emma));
  }


  const moduleName = 'shanghaiPorts';
  let elmWorker = connectElmWorker(this.Elm);
  scheduleActions(elmWorker);

  if (typeof this[moduleName] === 'undefined') {
    this[moduleName] = {
      dock: dock.bind(null, elmWorker),
      sail: sail.bind(null, elmWorker),
      shanghai: elmWorker
    };
  }

}).call(this);
<!DOCTYPE html>
<!-- shanghai.html -->
<html>
  <head>
    <title>Embedding Elm in HTML!</title>
  </head>
  <body>
    <h1>Ports of Shanghai</h1>
    <p>Check out the developer console, Try sending values to <code>shanghai.ports.incomingShip</code></p>
    <script type="text/javascript" src="Shanghai.js"></script>
    <script type="text/javascript" src="ports.js"></script>
    <script type="text/javascript">
      ; (function() {

        // Allow access to "shanghai" via the developer console
        const propName = 'shanghai';
        let module = this['shanghaiPorts']; 

        if (typeof module === 'object'
            && propName in module
            && typeof this[propName] === 'undefined') {

            this[propName] = module[propName];

        } else {
          console.log(
            'Any one or more of the following PROBLEMS were encountered:\
            \n  - "shanghai" is ALREADY DEFINED.\
            \n  - "shanghaiPorts" is NOT DEFINED or isn\'t an "object".\
            \n  - "shanghaiPorts.shanghai" is NOT DEFINED.'
          );
        }

        /*  In the developer console try:
          shanghai.ports.incomingShip.send({ name: 'Mary Mærsk',capacity: 18270 });
          shanghai.ports.incomingShip.send({ name: 'Emma Mærsk', capacity: 15500 });
          shanghai.ports.outgoingShip.send('Mary Mærsk');
          shanghai.ports.outgoingShip.send('Emma Mærsk');

            Or:
          shanghaiPorts.dock('Mary Mærsk', 18270);
          shanghaiPorts.dock('Emma Mærsk', 15500);
          shanghaiPorts.sail('Mary Mærsk');
          shanghaiPorts.sail('Emma Mærsk');
         */

      }).call(this);
    </script>
  </body>
</html>

@shamansir
Copy link

I messed up my example, sorry, @peerreynders yours is awesome!

@cdaringe
Copy link

what's not clear to me in the docs or this example is when you have a Main "app" already, but then want to add a port module within that existing app. That is to say, all of the examples i've yet seen are as though the port module is also the entrypoint itself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment