LoginSignup
3
3

More than 5 years have passed since last update.

A simple introduction to using Elm ports

Last updated at Posted at 2015-09-23

!!! OUT OF DATE !!! This is out of date, since everything here is written for 0.16 and below. I guess the code here with just Signal.map/mergeMany should work with the equivalent subscription versions kind of, but of course, you can't write programs in this fashion anymore in 0.17.

(and using Elm with webpack)

There are a variety of writeups online about them that go into a lot of technical and design explanations for how ports work and why they are the way they are. This post seeks to simply look at how you use ports in Elm and some of their characteristics that might aid/hinder you.

This post is part of my ongoing efforts to provide googleable resources for "obscure" technologies so that others wanting to get into them quickly will be able to do so. (especially in the case of Elm, where you quite often end up reading source code a lot...)

Objective

We'll make a simple JS + Elm application that "fetches" a list of files from JS and then displays them in the Elm application. The Elm application will communicate with the JS application through "ports" for get the new files and requesting a new list of files.

Setup

Three npm dependencies:

"dependencies": {
  "elm-simple-loader": "^0.1.0", // a webpack loader for Elm source code that I wrote so we can just boot up webpack like any normal JS project
  "rx": "^3.1.2", // sane reactive programming in JS. helps us not have to write a bunch of buggy boilerplate and gives us "Observables" which are like Elm.Signals
  "webpack": "^1.12.2" // our javascript bundling program.
}

Three elm dependencies:

"dependencies": {
  "elm-lang/core": "2.1.0 <= v < 3.0.0",
  "evancz/elm-html": "4.0.1 <= v < 5.0.0"
},

You could also just run elm package install -y and then install evancz/elm-html separately. Up to you.

Then webpack.config.js:

var webpack = require('webpack');

module.exports = {
  entry: './src/js/app.js',
  output: {
    path: './build',
    filename: 'app.js'
  },
  module: {
    loaders: [
      {
        loader: 'elm-simple-loader',
        test: /\.elm$/,
        exclude: /node_modules/
      }
    ]
  }
};

Then this folder structure:

▾ src/
  ▾ elm/
      App.elm
  ▾ js/
      app.js

Annotated Code

Elm side Github link

module App where

import Html exposing (..)
-- we'll be using onClick to fire off our outgoing port
import Html.Events exposing (onClick)

-- types important to our simple application that displays a list of files
type alias File = String
type alias Files = List File

-- the model of our application. simple, right?
type alias Model = {
  files: Files, -- the files we get fetched from the JS side
  times: Int -- the number of times we've gotten data that we'll update with files
}

-- generic Action type that we'll use
type Action =
  NoOp
  | UpdateRequest

-- our first port: the outgoing port
-- first, we need a mailbox if we're going to have a signal and address.
-- we'll declare a mailbox of Action
updateRequestMailbox : Signal.Mailbox Action
-- we'll then instantiate one with a NoOp action by default
updateRequestMailbox = Signal.mailbox NoOp
-- the "port" keyword is used to designate signals that are exposed in the compiled JS.
-- they have the restriction of requiring concrete types, so we'll just emit strings
port updateRequests : Signal String
-- and so for our output stream, we will just map over each Action that comes in
-- and just send down the string "updateRequest" instead.
port updateRequests =
  Signal.map (\_ -> "updateRequest") updateRequestMailbox.signal

-- our input port will be initialized and pushed to from the JS end,
-- so all we need to do is declare the shape of data that will come down the port.
port newFiles : Signal Files

-- declare our updateFiles function. this will send down "folder" functions that
-- will transform our old model into a new model.
updateFiles : Signal (Model -> Model)
-- so in the definition we see that we will map over the newFiles signal
-- and then update the model with new files and update the times property.
updateFiles =
  Signal.map
    (\files -> (\model -> { model | files <- files, times <- model.times + 1 }))
    newFiles

-- our update folder will just take the folders and apply them to our model
update : (Model -> Model) -> Model -> Model
update folder model =
  folder model

-- our updateRequestButton will handle onClick events by sending a message
-- to updateRequestMailbox address with the Action. Give the Html.Events
-- source a red if you're curious about the details.
updateRequestButton : Html
updateRequestButton =
  button
    [
      onClick updateRequestMailbox.address UpdateRequest
    ]
    [text "request update"]

-- the following views are boring
fileView : File -> Html
fileView file =
  div [] [text file]

filesView : Files -> Html
filesView files =
  div [] (List.map (\n -> fileView n) files)

view : Model -> Html
view model =
  div []
    [
      h3 [] [text "Muh Filez:"],
      updateRequestButton,
      h4 [] [text ("times updated: " ++ (toString model.times))],
      filesView model.files
    ]

-- the initial model that we'll be using
model =
  {
    files = [],
    times = 0
  }

-- * if you're unfamiliar with the threading macro, basically it works as this:
-- * (a |> max b) == (max b a)
-- * so below is like (a |> fn1 b |> fn 2 c) == (fn 2 c (fn 1 b (a)))
-- * the former is much easier to read, i think.
-- so below we merge our folder functions, then we pass that into an updater and
-- then our view mapper is there to produce a Signal Html for our main function.
main =
  Signal.mergeMany
    [
      updateFiles
    ]
  |> Signal.foldp update model
  |> Signal.map view

JS side Github link

var Rx = require('rx');

// bring in our Elm application through webpack!
var Elm = require('../elm/App.elm');

// basically an event bus, but is an Observable we can subscribe to and whatnot
var filesData$ = new Rx.Subject();

// default files listing
var files = [
  'file1',
  'file2',
  'file3',
  'file4',
  'file5'
];

function loadFilesData() {
  console.log('pretending to fetch data...');
  // every time we "fetch" data, let's say it updated.
  files.push('file' + files.length);
  // fire off the files data stream with the new files
  filesData$.onNext(files);
}

function init() {
  var appContainer = document.getElementById('app');
  console.log('embedding our Elm application!');
  /**
   * embed our elm app in our container.
   * you might remember that this is what i was talking about in our Elm code,
   * where you need to provide an initial value for these Signals.
   * this is the one we control from the JS side, so we need to provide it from
   * this side to make it work correctly.
   *
   * we also get the App instance returned when we call this, so
   * we need to use to access our ports.
   */
  var elmApp = Elm.embed(Elm.App, appContainer, {
    newFiles: [] // need to provide initial values to our listening port!
  });

  filesData$.subscribe(function (files) {
    console.log('new data came down in our stream!');
    console.log('sending data down our newFiles port...');
    // so let's send the new files down the port
    // in elm we had `port newFiles`, so this way we access this
    // from JS is to do the following:
    elmApp.ports.newFiles.send(files);
  });

  // just like above, we access the output port from JS similarly,
  // but we will subscribe to the output from this.
  // all we really care about is the event instead of the value,
  // so we'll just kick of loadFilesData as a result.
  elmApp.ports.updateRequests.subscribe(function (value) {
    console.log('update request came from our updateRequests port!');
    loadFilesData();
  });

  loadFilesData();
}

init();

Building and Running

So now you can just run webpack from your project root and everything builds!

You'll probably need a index.html like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Elm Simple Ports Example</title>
  </head>

  <body>
    <div id="app"></div>
    <script src="build/app.js"></script>
  </body>
</html>

open index.html and check it out!

Conclusion

And so using ports with Elm really isn't that difficult. Now we can code a bunch of JS for the part that needs to integrate to whatever else we have (like legacy code, other JS libs, whatnot) and still have the guarantee of the Elm side of our application!Screen Shot 2015-09-24 at 12.29.39 AM.png

See the repository here: https://github.com/justinwoo/elm-simple-ports-example
Demo here: http://justinwoo.github.io/elm-simple-ports-example

References

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3