This post is over 6 months old. Some details, especially technical, may have changed.

window.prompt() in Elm

I say "standard stuff" a few times in this article. When I say that I assume, like me, you've dipped your toes into Elm. If that is not the case and you want to change that situation then I recommend go give Pragmatic Studios your money. I was in no way paid or influenced to say that BTW.

Having spent last night watching the Pragmatic Studios Elm: Building Reactive Web Apps I am now a fully certified expert in Elm and so it's time to start writing articles about it. Of course this is nonsense but some of my adventures have inspired me to write an article or two that may be useful to others (and my future self).

I recently documented a little hobby project I worked on for keeping score on games of Zombie Dice. The app was written in ClojureScript and after I had wrapped my head around Elm I decided to see what the same app would look like in Elm. This article isn't about that, but rather a small part of it that I found quite challenging.

Unlike ClojureScript (and Clojure) whose philosophy is to embrace the underlying host environment Elm abstracts it away. It's not gone completely it's just not as prevalent or easy to access. In my Zombie Dice Score Card app adding new players uses the standard JavaScript host function window.prompt to capture the name and I wanted to replicate this functionality in my Elm implementation.

Basic Implementation

My first pass at this resulted in a working implementation (well not my actual first pass it was many, many passes before I even figured out what the hell I was doing). While it wasn't going to be practical for my actual needs it did form the basis of something useful that I could build upon.

The key is to using ports. A port acts as a bridge between Elm and JavaScript. They either go in to Elm or out of Elm. You declare a port in Elm like this,

port suppliedNames : Signal String

This is port is an innie. You can tell it is an innie because you supply no actual definition. Instead we can send things to it from the JavaScript side.

app.ports.suppliedNames.send("James");

Assuming, in our HTML file, our Elm app is created and assigned to a variable app we will have this ports object that lists all the ports we expose from our app. This will have our suppliedNames port which will have a method of send that we can use to signal values through that port.

One other thing is that these innies must be given an initial value when the app is constructed or you'll get an exception. We can do it like this,

var app = Elm.fullscreen(Elm.Confirm, { suppliedNames: "" });

So when we boot our app we pass in an initial value.

Lets look at another port,

port totalCapacity : Signal String
port totalCapacity = map toString somethingElse

This one is an outtie. This is used to signal things out of our Elm app into JavaScript. In this case it sends out the sum of some data structure as and when it changes. We can listen to these signals in JavaScript like this,

app.ports.totalCapacity.subscribe(function(x) {
  console.log(x)
});

Outtie ports have a subscribe method generated for them that takes a function accepting the payload. In our case we simply log it.

Now onto our specific use case. My needs are slightly different to the samples because while they broadcast data out of the Elm side I just want some sort of trigger to say "go ahead and open a prompt". This is 100% impure in that it is only used for side effects and I've found that with Elm being a rather pure language this sort of requirement feels kind of awkward. Thats not a criticism, just an observation. You should expect bad things to feel awkward, it helps you minimise them.

The JavaScript/HTML side of our application is fairly unsurprising (assuming you've not skipped the last few paragraphs).

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
  	  <script type="text/javascript" src="confirm.js"></script>
  </head>
  <body>
  </body>
  <script type="text/javascript">
    var app = Elm.fullscreen(Elm.Confirm, { suppliedNames: "" });
    app.ports.prompt.subscribe(function(){
        var value = window.prompt("Players name?");
        app.ports.suppliedNames.send(value);
    });
  </script>
</html>

Here we,

  • boot our app passing in a blank value for suppliedNames
  • subscribe to a prompt port which will display the window.prompt
  • send the captured value from window.prompt back into our app

The Elm side of things is where the real stuff happens,

module Confirm where

import Html        exposing (..)
import Html.Events exposing (..)
import Signal      exposing (..)

-- SIGNALS 
type Action = NoOp | Prompt

actions : Signal.Mailbox Action
actions =
    Signal.mailbox NoOp

-- PORTS --
port suppliedNames : Signal String

port confirm : Signal ()
port confirm =
  actions.signal
    |> filter (\s -> s == Prompt) NoOp
    |> map (always ())

-- VIEWS --
view name =
  div []
    [ text name,
      button [ onClick actions.address Prompt ]
        [ text "Set Name"]]

-- APP -
main : Signal Html
main = Signal.map view suppliedNames

Given there is a fair few things happening here lets have a look at the interesting bits.

First of all we set up the various operations our application will perform and create a Mailbox that can be used to send actions to. If you've ever tinkered with Elm this should be pretty common,

type Action = NoOp | Prompt

actions : Signal.Mailbox Action
actions =
    Signal.mailbox NoOp

The Prompt action is the one we want to use to trigger our window.prompt call via a port. Next we declare our actual ports,

port suppliedNames : Signal String

port prompt : Signal ()
port prompt =
  actions.signal
    |> filter (\s -> s == Prompt) NoOp
    |> map (always ())

suppliedNames is our innie. This will receive names we've created later on. prompt is our outtie. What we do is

  • take the signal from our Mailbox
  • filter everything except the Prompt actions
  • map these values an always return a unit value

Why unit? Well we don't care about the actual value we just want to reach out to the JavaScript when we get a Prompt action through our mailbox. Any value is meaningless so lets just go for the most meaningless one we can find.

Then in our view we have a button that sends a Prompt action to our Mailbox,

button [ onClick actions.address Prompt ]

Again this should be fairly unsurprising to the Elm-ites (or whatever the collective term is) reading this.

Finally we have our main that wires it all together,

main = Signal.map view suppliedNames

Again - standard stuff.

Notes

As the last title indicated this is just a basic implementation. It requires a bit more work to integrate into an application that requires a more complicated model but its a good starting point. I know this because I've already begun extending it to create the Elm version of my Zombie Dice Score Card which I'll dissect in another article soon.

Now I'm not saying this is idiomatic Elm, I'm not even saying this is a recommended way to do things but it works and "feels" good enough to me. If you want to comment/fix/critique/whatever my work then you can get me on Twitter and let fly the dogs of conversation.

Published in Elm on July 28, 2015