Elm Frontend Tutorial

Overview

Due to the sheer number of options available in PLACE, operating it via the command-line interface, while possible, is cumbersome. In order to provide users with easy access to all PLACE options, a web interface has been developed. The web interface runs in JavaScript, making it well supported across operating systems. However, to ease in the development of JavaScript, the programming language Elm is highly recommended. Elm is a functional programming language with static type checking that can be easily compiled to JavaScript.

Brief Elm introduction

Elm is probably new to most users of PLACE and, being a functional language, it will appear different from many languages you have encountered in the past. Rest assured, though, that our goal is not to teach you to program in Elm, but rather to give you some simple tools for constructing basic user interfaces using Elm.

As the term implies, functional languages treat everything as a function. Look at a simple statement like:

x = 1

This does not create a variable named x and give it the value 1. Instead, this creates a function named x that takes no inputs and always produces 1. Thus we cannot change the value of x to something else. It is this differentiation that usually causes the most confustion, but you will get used to it quickly.

When a function take arguments, they are simply listed after the function name. Like this:

squareThis number = number * number

In this example, squareThis is the function name, number is the argument. In Python, this would essentially be the same code as this:

def squareThis(number):
    return number * number

Okay, that’s a good start. The only other thing I want you to know right now is that we often will explicitly list the types being used in a function. So, you will often see our squareThis function written like this:

squareThis : Float -> Float
squareThis number = number * number

The ‘type line’ just tells us that this function takes in one floating point value and produces a new one. To compare, our simple x = 1 line would have a type line like x : Int to tell us that it took in nothing and produced an integer.

My best advice at this point is just to move on. This won’t make complete sense now, but you will usually have plenty of examples to use so it doesn’t matter.

Installing Elm

You will need to install Elm in order to build your code at the end of this guide. You can start with the Elm install page. There is an installer for Windows and Mac. On Linux, installing it via NPM is recommended.

Optionally, it is a good idea to install elm-format which can be used to automatically be used to format your code into a standard format.

My personal installation steps (for Linux):
  1. install NPM
  2. install elm
  3. install elm-format
  4. install VS Code
  5. install Elm Language Support for VS Code

Important - PLACE currently has not been updated for the big changes in Elm 0.19. Until this happens, new plugins should use Elm 0.18. Installing the old version is usually as easy as using elm@0.18 instead of elm during installation.

Making HTML with Elm

The main reason PLACE uses Elm is to produce solid JavaScript, which will in turn produce HTML for the web interface. We will refer to the HTML as the view, and it is what the user will see in their web browser. Most of our Elm code will be used to build this.

In Elm, an HTML block has a special type, called Html Msg. As you can see, this type has two words instead of just one. When you have a type with multiple words, it is kind of like having all these types stuck together. What this means to us is that all the HTML generated by Elm will produce some HTML for the user (the HTML) to see and some messages (the Msg) used to manipulate what is displayed on the web page. For instance, when they see an HTML button, they can click it and generate a message.

The messages generated by the user will update a model that sits inside the code. In PLACE, this model is typically the values your module is looking for in the JSON config data. But if you haven’t read about that yet, don’t worry.

All the above concepts come together into an Html.program, which is just a way to wrap up all the things Elm needs into a nice little package.

If you feel like you need to learn more about Elm, you can go through their tutorial. Otherwise, I’m going to start talking about making a web interface for a PLACE module.

Template module

A lot of the Elm code will start the same way, so we have included a template to use when starting a new module. You can find it here.

The template file is designed to make creating a plugin easy, and follows a step-by-step format. This file will hopefully cover the majority of beginning use cases. Let’s quickly run through this file.

First, let’s talk about comments. In Elm, you make a comment using --. This is similar to the # in Python. Anything on the same line, following --, is ignored by Elm.

Module name

The first (non-commented) line defines the module. Specifying port at the beginning allows our interface to communicate with PLACE.

port module PLACETemplate exposing (main)

This is followed by standard imports, which use a similar syntax to Python.

import Html exposing (Html)
import Json.Decode as D
import Json.Decode.Pipeline exposing (hardcoded, optional, required)
import Json.Encode as E
import Metadata exposing (Metadata)
import Plugin exposing (Plugin)
import PluginHelpers

The last import, PluginHelpers, is a PLACE library of helpful functions. These are used to simplify the process of writing new plugins for PLACE.

At this point, we start getting into the actual code.

Common metadata

Each PLACE plugin has some metadata associated with it, so these common values have been gathered into their own record. This ensure that you don’t forget to specify something needed by PLACE.

common : Metadata
common =
    { title = "PLACE Template" ---------------- the title to display in the PLACE web application
    , authors = [ "Dr. A. Place" ] ------------ list of all authors/contributors
    , maintainer = "Mo Places" ---------------- who is currently maintaining the plugin
    , email = "moplaces@everywhere.com" ------- email address for the maintainer
    , url = "https://github.com/palab/place" -- a web URL for the plugin
    , elm =
        { moduleName = "PLACETemplate" -------- the name of this Elm module
        }
    , python =
        { moduleName = "place_template" ------- the name of the Python module used by the server
        , className = "PLACETemplate" --------- the name of the Python class within the Python module
        }
    , defaultPriority = "10" ------------------ the default priority of this plugin
    }

Model

The model is the data structure that contains variable values needed to run your device on PLACE. There is no limit to the number of values in the model, but the template only supports the basic types: string, bool, int, and float. This should be enough for most PLACE plugins.

type alias Model =
    { --   plot : Bool
    -- , note : String
    -- , samples : String
    -- , start : String
    --
    null : () -- you can remove this null value (it's just a placeholder)
    }

After creating the model for the plugin, we will create an instance of the model containing all the default values. PLACE will use these values if the user has not entered a value or if they have entered an invalid value.

default : Model
default =
    { --   plot = True ---------- Bool
    -- , note : "no comment" -- String
    -- , samples : "10000" ---- Int (as String)
    -- , start : "2.5" -------- Float (as String)
    --
    null = () -- you can remove this null value (it's just a placeholder)
    }

Messages

Now that we have a model, we need to have a way to work with the model. Elm uses messages to do this. Each message defines an action you would like to perform with the data model. The default messages are provided in the template, but you will need to add additional messages to modify the values added to the data model in the previous step.

type Msg
    = -- TogglePlot -------------- Bool message
    -- | ChangeNote String ----- String message
    -- | ChangeSamples String -- Int (as String) message
    -- | ChangeStart String ---- Float (as String) message
    --
    Null -- you can remove this Null message (it's just a placeholder)

Each message is essentially a function and can take additional arguments. TogglePlot doesn’t take any arguments because it just flips a boolean value back and forth. ChangeNote takes a String as an argument because the value will be typed by the user into the web interface.

After creating all the messages, we need to tell PLACE what to do when it receives a message. Messages will be received whenever the user changes something in the web interface, and our job is to update the model with the change the user made. It is very important that the model is always the same as what is displayed to the user, otherwise, things will get very confusing!

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- TogglePlot ->
        --     ( { model | plot = not model.plot }, Cmd.none ) -- update Bool
        -- ChangeNote newNote ->
        --     ( { model | note = newNote }, Cmd.none ) ---------------- update String
        -- ChangeSamples newSamples ->
        --     ( { model | samples = newSamples }, Cmd.none ) ---------- update Int (as String)
        -- ChangeStart newStart ->
        --     ( { model | start = newStart }, Cmd.none ) -------------- update Float (as String)
        --
        Null ->
            -- you can remove this Null message (it's just a placeholder)
            ( model, Cmd.none )

View

The view is what the user actually sees on the webpage. PLACE will take care of constructing most of this for us, but we need to tell it which types of user interactions we want to have displayed and which values in the model these interactions will change.

Because HTML can be complicated, a lot of work has been done recently to streamline this process. The PluginHelpers module contains lots of helper functions for creating nice user interactions. There are helper functions for checkboxes, integers, floats, and strings. There is also an interface for dropdown menus, which allows for selecting from a limited number of strings. Check PluginHelpers.elm in the place/elm/plugins/helpers directory for the latest offering.

userInteractionsView : Model -> List (Html Msg)
userInteractionsView model =
    [-- PluginHelpers.checkbox "Plot" model.plot TogglePlot --------------------------- Bool
    -- , PluginHelpers.stringField "Note" model.note ChangeNote ---------------------- String
    -- , PluginHelpers.integerField "Number of samples" model.samples ChangeSamples -- Int (as String)
    -- , PluginHelpers.floatField "Start time" model.start ChangeStart --------------- Float (as String)
    --
    -- Dropdown Box (for Strings with limited choices)
    -- , PluginHelpers.dropDownBox "Shape" model.shape ChangeShape [("circle", "Circle"), ("zigzag", "Zig Zag")]
    --
    -- Note that in the dropdown box, you must also pass the choices. The first
    -- string in each tuple is the value saved into the variable and the second
    -- is the more descriptive string shown to the user on the web interface.
    ]

We can see from the function definition that this function is provided with the current data model, named model, and produces a list of Html Msg (essentially meaning it produces both HTML and messages).

JSON

The last piece of the puzzle is JSON, or JavaScript Object Notation. JSON is a common text format for sending data over a network. This is how the Elm frontend of PLACE communicates with the Python backend of PLACE… with JSON. Because of this, we need to let PLACE know how to encode and decode the values in our model. This may sound complicated, but it basically amounts to telling Elm what text string you want to use to access your value in your Python backend code.

encode : Model -> List ( String, E.Value )
encode model =
    [ -- ( "plot", E.bool model.plot ) --------------------------------------------------------- Bool
    -- , ( "note", E.string model.note ) ----------------------------------------------------- String
    -- , ( "samples", E.int (PluginHelpers.intDefault default.samples model.samples) ) -- Int (as String)
    -- , ( "start", E.float (PluginHelpers.floatDefault default.start model.start) ) ---- Float (as String)
    --
    ( "null", E.null ) -- you can remove this "null" field (it's just a placeholder)
    ]


decode : D.Decoder Model
decode =
    D.succeed
        Model
        -- |> required "plot" D.bool ------------------------------------------- Bool
        -- |> required "note" D.string ----------------------------------------- String
        -- |> required "samples" (D.int |> D.andThen (D.succeed << toString)) -- Int (as String)
        -- |> required "start" (D.float |> D.andThen (D.succeed << toString)) -- Float (as String)
        --
        -- you can remove this "null" field (it's just a placeholder)
        |> required "null" (D.null ())

So, despite having a lot of code, we are basically telling place to store the value in model.plot under the key plot. Nothing mysterious going on. But, this section is imporant beacuse this is where you link the Elm values to your Python values.

Building Elm into JavaScript

Elm code cannot actually be executed directly. It must be transpiled into JavaScript code, when can then be executed by your browser. In our case, we will just give the JavaScript to PLACE and it will build it into the rest of the PLACE web interface.

Where to put your .elm file

As of PLACE 0.8, all the Elm source code lives in place/elm/plugins. At some point in the future, PLACE plugins will need to be maintained individually and will be installed into PLACE using the web interface. However, this isn’t likely to happen in the near future, since the number of PLACE modules is still quite small.

Adding the plugin to the build script

There is currently a build script which ensures that all the Elm modules are build and put into the correct location. This file is located at place/elm/elm-make-all.sh. It is intended as a Linux shell script, so if you are wanting to build the Elm code on another platform, you will probably need to do it manually, but the script is actually very simple, so it shouldn’t be difficult to follow.

It should be noted that the code can be build on any platform and the resulting JavaScript should be the same. JavaScript is designed to be platform independent. So, you don’t need to build the JavaScript files on a Windows system in order to use them on a Windows system.

Adding the plugin to the webpage

This step will also likely be automated in the future, but for the time being it is still necessary to add a couple lines of code in order to let PLACE know the new plugin is installed.

The file you are looking for is place/placeweb/plugins.py. Again, this is a really easy to follow file. You just need to create an entry for your plugin that matches the format of one of the others. If you don’t do this step, you will not see your plugin in the dropdown menu on the webpage.

Running the build

Assuming you have done everything correctly, you should not be able to navigate to the place/elm directory and run ./elm-make-all.sh. If you have correctly installed Elm, this should build all the plugins, including your new one, and put them into the correct directory to be served to the web interface.

If Elm gives you errors, they are usually pretty good at directing you to the problem. You will need to correct all the errors and rerun the build.

When the build completes successfully, you usually want to restart the server and hard refresh (ctrl+click refresh) the PLACE webpage. You should now see your new plugin in PLACE!