Elm ports is one of the main ways to interact with JavaScript. However I feel like I need to tell you that if you can get away with it, you should just use flags. Flags pass in some initial value to Elm to start with and is the easiest way no doubt. However if you want more back and forth communication ports is the way.
First, here’s the picture:
Ports are basically the pub-sub pattern (also called The Observer Pattern). A port is the connecting pipe between Elm and JavaScript.
Each port is only one direction. Either Elm ->JavaScript or JavaScript -> Elm. So naturally if you want two way communication you just have to make two.
If you’re going to follow along follow these steps (or perhaps just look at the code)
Step 1: Setup an Main.elm
file with the full Html.program
Step 2: A .js
file that can be blank for now
Step 3: An index.html
file that includes both elm.js
(compiled elm file) and the whatever.js
file that contains the code you want to interact with.
Annotate the module by putting a port
at the beginning
port toJs : String -> Cmd msg
This will creates a function called toJs
that creates Cmd
s (commands). If you remember you can return Cmd
as the second part of the tuple in the update
function! Let’s do that.
update msg model =case msg of
SendToJs str ->
( model, **toJs** _str_ )
This will magically send _str_
to JavaScript land! Well only to those who subscribe to it. To subscribe to it you do:
var node = document.getElementById('view');var app = Elm.Main.embed(node);
// receive something from Elmapp.ports.toJs.subscribe(function (str) {console.log("got from Elm:", str);});
In JS land you send something via the send
method on the port. Like so:
var node = document.getElementById('view');var app = Elm.Main.embed(node);app.ports.toElm.send("undefined is not a function");
Naturally JS says it’s customary phrase.
So we define a new port:
port toElm : (String -> msg) -> Sub msg
This creates a function, that takes a function that can turn strings JS sends and turns it into a Msg
(again can be any value that’s compatible between the two) This one returns a Sub
instead of a Cmd
so we need to subscribe to in Elm:
subscriptions : Model -> Sub Msgsubscriptions model =toElm UpdateStr
Basically this says:
whenever JS sends me something over the
_toElm_
port send the_UpdateStr_
message to the_update_
function.
We can handle this Msg
just like any other in our update function:
update msg model =case msg ofUpdateStr str ->( { model | message = str }, Cmd.none )
Basically I’m just storing the string JS gave me to a part of my model.
While I’ve used String
to keep this simple but this is dangerous.
If JavaScript, (in it’s infinite wisdom), decides to send something that’s not a string (like null, undefined, NaN, rubber duck, a function, etc.) then Elm will throw a run-time exception and stop it’s run loop.
This means Elm will not be able to accept any input or respond in any way. It’s dead.
The way to make your elm code bullet proof again to change the type of the port to a Value
instead. This means that you’ll have to write a Json.Decoder for it. This way if JS decides to send you a bad value it will just return an Err
instead of throwing an exception.
See the diff here
import Json.Encode exposing (Value)import Json.Decode as Decode
-- Farther down in the file.... --
port toElm : (Value -> msg) -> Sub msg
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msgsubscriptions model =toElm (decodeValue)
decodeValue : Value -> MsgdecodeValue x =letresult =Decode.decodeValue Decode.string xincase result ofOk string ->UpdateStr string
Err \_ ->
UpdateStr "Silly JavaScript, you can't kill me!"
Here’s the picture again:
Here’s a link to the working repo: https://github.com/justgage/complete-elm-port-example