Building Reactive WordPress Plugins – Part 3 – Elm

In this final part of the Building Reactive WordPress Plugins series I’ll be exploring using Elm to write the front end of WP Cron Pixie, the small WordPress dashboard widget for showing what’s in the WordPress cron.

Why Elm?

I guess I should straight up admit that the whole reason this series of articles exists is so that I get to write about Elm!

Elm is a functional language that compiles to JavaScript. Elm is designed as a general purpose language, JavaScript is just the first compile target. As such it is closer to the imperative Dart programming language than it is to CoffeeScript or TypeScript, which are very much “JavaScript improved”.

While I’ve talked about how awesome Vue.js is in my previous article, it is still a framework for JavaScript, and I fully admit that while I’ve worked with JavaScript for many years, it is far from being a language I enjoy writing front end web code in.

On the other hand, I find Elm a joy to write code in. Here are some of the reasons, borrowed from the Elm Guide:

  • No runtime errors in practice. No null. No undefined is not a function.
  • Friendly error messages that help you add features more quickly.
  • Well-architected code that stays well-architected as your app grows.
  • Automatically enforced semantic versioning for all Elm packages.

Elm is an early proponent of reactive style development and acknowledged as one of the influences for the popular Redux library that is often used to handle state for React apps.

Elm has matured since it was initially designed by Evan Czaplicki as his thesis in 2012. Lead by Evan, the project has settled on a way to structure apps called The Elm Architecture. I’ll let you go read up on it in full, but the basics are that you have a Model that holds state data, an update function that modifies that Model, and a view function that lays out the app based on the current Model. Everything is strung together by the Elm runtime, with efficient usage of a Virtual DOM implementation to keep things snappy in the browser.

Before we go any further, it should be noted that I’m a beginner when it comes to writing Elm and functional code in general. So experienced Elm and functional language savvy developers please excuse anything that could be done in more idiomatic Elm, and feel free to share any tips and tricks in the comments. I’m eager to learn how to write better Elm.

The Plan

At the moment WP Cron Pixie uses multiple *.vue files that compile down to a single build.js. We’re going to get rid of all the component files and start off with a single CronPixie.elm file, with a single main.js file to bootstrap the application, again compiling the two down to a single build.js for more efficient delivery.

Setting Up

First we’re going to need to install Elm. Personally I’m using Homebrew for installing most command line software on my Mac these days, so for me it’s a simple:

$ brew install elm

However, you can install via either the Mac or Windows installers, or through npm anywhere it runs, including on Linux.

Let’s create a “Hello World” app to make sure we have the Elm compiler up and running. Add the following to a HelloWorld.elm file.

module HelloWorld exposing (..)

import Html exposing (text)

main =
    text "Hello World"

Then compile it with…

$ elm package install
Some new packages are needed. Here is the upgrade plan.

  Install:
    elm-lang/core 4.0.5
    elm-lang/html 1.1.0
    elm-lang/virtual-dom 1.1.1

Do you approve of this plan? [Y/n] 
Starting downloads...

  ● elm-lang/html 1.1.0
  ● elm-lang/virtual-dom 1.1.1
  ● elm-lang/core 4.0.5
Packages configured successfully!
$ elm make HelloWorld.elm
Success! Compiled 31 modules.                                       
Successfully generated index.html

As you can see, this new project required some core libraries to be downloaded and installed for this project. Subsequent usage of elm make in this project wouldn’t need to download these (very small) libraries.

You’ll now have what seems like a much larger index.html than you might first expect. It’s large because it is a complete application with the full Elm runtime library and application code. Open it up in a browser and you’ll simply see “Hello World”.

With a verified working Elm compiler we now have everything we need to update WP Cron Pixie to use Elm rather than Vue.js for its frontend.

Backend Changes

Because we’re going to be using Elm’s own HTML library for the bulk of our HTML, all we need to be output by the backend PHP is a div to hang our app off of. We therefore need to update the dashboard_widget_content() function in class-cron-pixie.php

/**
 * Provides the initial content for the widget.
 */
public function dashboard_widget_content() {
    ?>
    <!-- Main content -->
    <div id="cron-pixie-main"></div>
    <?php
}

That’s a bit simpler than our Vue.js version’s HTML, and way simpler than the Backbone.js HTML.

We’re also going to put the main.css file back that we had before the Vue.js update as we’re not going to be mixing our CSS with our layout and code any longer. This means we need to enqueue it again from the PHP backend by adding the following to the enqueue_scripts function.

wp_enqueue_style(
    $script_handle,
    plugin_dir_url( $this->plugin_meta['file'] ) . 'css/main.css',
    array(),
    $this->plugin_meta['version']
);

There is just one other change that we need to make to the back end, which may be totally down to my relative inexperience with Elm. As far as I can tell, you can’t easily POST nested data from Elm to a PHP backend. The easiest solution is to JSON encode structures that have children like our model data does. This means that in the ajax_events function that processes a posted Event to run its job “now”, we need to decode the JSON from the $_POST variable.

$event = json_decode( stripcslashes( $_POST['model'] ), true );

That’s it for the backend, let’s write some JavaScript!

Frontend JavaScript

What? I thought we were writing our frontend in Elm, not JavaScript?

Well, sure, if we were writing all the frontend in Elm we could just elm make and plonk the index.html in the right place. However, we’re going to be integrating an Elm application into an already existing HTML and JavaScript laden frontend, so we need a little bit of JavaScript to insert our Elm app in the right place. Lets strip our main.js down to the bare essentials that we need to bootstrap our Elm app.

var Elm = require( './CronPixie' );
var $mountPoint = document.getElementById( 'cron-pixie-main' );
var app = Elm.CronPixie.embed(
  $mountPoint,
  {
    strings: CronPixie.strings,
    nonce: CronPixie.nonce,
    timer_period: CronPixie.timer_period,
    schedules: CronPixie.data.schedules
  }
);

That’s our entire JavaScript, we won’t be writing any more, yay!

As you can see, it’s pulling in the main bulk of our application with the require( './CronPixie' ) statement, meaning we’ll need to compile our Elm code down to a CronPixie.js file next to main.js. We’ll come to that in a bit.

Otherwise, the JS is finding the div we created with an id of “cron-pixie-main”, and then using that as the mount point for our embedded Elm app.

Our backend PHP is also exposing some useful JavaScript variables that we’d like to use in our Elm app, such as translatable strings, security nonce (number used once), default interval to poll for the data, and an initial data set containing the cron schedules. We pass them into the Elm program as an object.

Frontend Elm

The entire frontend Elm code is in CronPixie.elm. As this is a relatively small application, there wasn’t any need to split it up into multiple files, but The Elm Architecture has a robust module system that can aid in function reuse.

First Steps

I started off with something akin to the HelloWorld app we wrote above.

module CronPixie exposing (..)

import Html exposing (text)

main =
    text "Awesomeness Goes Here!"

You’ll notice that I named the module “CronPixie” so that it has a unique name. In the above main.js file we reference this module name when embedding the Elm app into the HTML. In this way you could imagine Elm also embedding a module called “WPMDBPro” without clashing with other Elm apps.

It’s also important when writing modules for reuse to use a unique and descriptive name. For example, if I split up the Elm source into modules I might have modules called “Schedule” or “Event” and so on. There might even be some value in calling them “Cron.Schedule” and “Cron.Event”. However, as I felt absolutely no pain while working with “CronPixie” holding all my Elm code, I decided not to split it up.

I used the Elm Package Manager to install the core HTML library. This also creates the elm-package.json file and generally initialised the Elm project.

$ elm package install elm-lang/html

Compiling to JavaScript just requires specifying the output filename.

$ elm make src/elm/CronPixie.elm --output=src/js/CronPixie.js

Because our widget is already enqueuing build.js, I compiled main.js to build.js (remember, main.js pulls in CronPixie.js automatically through its use of require) using the same browserify command we set up for the previous version.

$ browserify -e src/js/main.js -o src/js/build.js

With that working, and the widget now showing “Awesomeness Goes Here!” as its content, I cleaned up the package.json file to remove the Vue.js dependencies.

$ npm uninstall vueify vue-resource vue --save-dev
$ npm uninstall --save-dev babel-core babel-preset-es2015 babel-plugin-transform-runtime babel-runtime

As the package.json file was already set up with build-js and watch-js scripts entries that could be run via npm run to reduce keystrokes and automate the compiles, I added equivalents for building Elm source files.

You saw the contents of the build-elm script above, but what about watch-elm? Well in that case we need to use chokidar-cli to monitor for when *.elm files are updated and then run build-elm.

Chokidar is used by things like brunch, gulp, browserify and webpack as the core of their file watching mechanisms.

$ npm install -g chokidar-cli

With that installed, I updated the scripts section of package.json.

{
  "name": "wp-cron-pixie",
  "version": "1.2.0",
  "description": "A little dashboard widget to view the WordPress cron.",
  "main": "src/js/main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build-js": "browserify -e src/js/main.js -o src/js/build.js",
    "watch-js": "watchify -v -e src/js/main.js -o src/js/build.js",
    "build-elm": "elm make src/elm/CronPixie.elm --output=src/js/CronPixie.js",
    "watch-elm": "chokidar '**/*.elm' -c 'npm run build-elm'",
    "watch": "npm run watch-js & npm run watch-elm"
  },
  "repository": {
    "type": "git",
    "url": "git+https://ianmjones@github.com/ianmjones/wp-cron-pixie.git"
  },
  "author": "Ian M. Jones <ian@ianmjones.com>",
  "license": "GPL-2.0",
  "bugs": {
    "url": "https://github.com/ianmjones/wp-cron-pixie/issues"
  },
  "homepage": "https://github.com/ianmjones/wp-cron-pixie#readme",
  "devDependencies": {}
}

Did you notice that I snuck in a way to watch for and compile both the Elm and and JavaScript source when it changes?

$ npm run watch

Walkthrough

Now that you know how to compile the frontend source for WP Cron Pixie, I’m going to walk through each of the sections in the CronPixie.elm source.

There is absolutely no way that I have the space to teach you Elm and functional style development, even if I was remotely experienced enough to do so (I’m an enthusiastic beginner). So, I’ll try my best to point out the main features and how each section hangs together with the others.

There are 7 main sections to the file:

  • Imports – Where we import the core and community modules.
  • Model – Defines the structure of the state data.
  • Messages – Defines the types of messages that flow through the app.
  • View – The functions that build the HTML based on the current state.
  • Update – Where the messages arrive and the state data is updated.
  • Subscriptions – Where the app subscribes to events and potentially reacts by sending command messages.
  • Main – Where it all begins.

Imports

At the top of the source file we define the module’s name and any other modules we would like to import so that we can use their functions.

module CronPixie exposing (..)

import Html exposing (Html, div, text, h3, ul, li, span)
import Html.Attributes exposing (class, title)
import Html.Events exposing (..)
import Html.App
import Date
import Date.Format
import Time exposing (Time, second)
import String
import List exposing (head, tail, reverse)
import Maybe exposing (withDefault)
import Task
import Http exposing (stringData, multipart)
import Json.Decode exposing (..)
import Json.Encode as Json

I apologise for this not being as pretty as it could be, but I’ve left it as it was built up during development. You can kind of see the order that I tackled building the app from the order of the import statements.

It’s pretty simple stuff though. Some modules are simply imported by name and therefore I’d use their functions via their module name, e.g. String.join.

Others have functions that I used quite often, so I exposed those functions so that I could shorten the code a little, e.g. Html.Attributes.class has been exposed so that I can simply use class.

And a couple of modules have had all their functions exposed for direct usage as I used a good bunch of them. For example I ended up using a lot of functions from Json.Decode.

For Json.Encode I just gave it a new name of Json so that I could shorten the code a little.

While that covers the beginning of CronPixie.elm, let’s move on to the logical beginning, the main function.

Main

This is where Elm learns about which functions we’re wanting to use to handle different parts of The Elm Architecture.

main : Program Flags
main =
    Html.App.programWithFlags
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

There are simpler versions that can be used, such as App.beginnerProgram that just wants to know what you’ve called your model, view and update functions. However, because I wanted to pass in some already existing JavaScript variables, I used App.programWithFlags because it produces a Program with Flags. I also needed to be able to use subscriptions, which we’ll discuss later.

You’ll notice that main is written twice. That is because I chose to give the Type Annotation for the function. It’s a way of telling the compiler what Types you expect a function to take and return, so that it can check them against what it infers from the code. In this case main just returns a Program Flags and has no parameters. In later code you’ll see annotations that specify the parameters too.

The function Html.App.programWithFlags is called with a single argument, a Record that specifies the init, view, update and subscriptions function names. In my app those function names just so happen to be called exactly the same, e.g. the function used to build the view is called “view”.

If you’re used to seeing commas at the end of vertical argument lists in JavaScript, PHP and many other languages, you might be wondering why the commas are at the beginning of lines in my Elm code. This is a code style that Evan started to use and the Elm community has agreed to retain as it tends to reduce the chance of forgetting to add or remove commas during refactoring. I personally love this style, and adopted it approximately 20 years ago while writing Informix 4GL!

One function we’ve not mentioned before is init, let’s move on to the section that defines it and see what it does.

Model

Elm is a strongly typed language, which provides for a lot of safety when refactoring, and believe me, saved me many headaches when working on this iteration of the plugin. Not once did I get a “Undefined is not a function” error while developing in Elm, unlike with the JavaScript based Backbone.js and Vue.js iterations. That’s because the compiler checks all the types of what the functions accept and return and ensures there’s no miss-match. You’ll also see later that things like if statements are structured in such a way as to not allow possible problems with incorrectly handled data.

We’re going to model the data we expect to handle in the application.

type alias Model =
    { strings : Strings
    , nonce : String
    , timer_period : Float
    , schedules : List Schedule
    }

This is the top level container for our state data, we’re calling it Model, and it’s an alias for a record that holds our translation strings, nonce string, timer_period and collection of schedules.

type alias Strings =
    { no_events : String
    , due : String
    , now : String
    , passed : String
    , weeks_abrv : String
    , days_abrv : String
    , hours_abrv : String
    , minutes_abrv : String
    , seconds_abrv : String
    , run_now : String
    }

Strings is also a type alias for all the translation strings we’re going to use in the application.

type alias Schedule =
    { name : String
    , display : String
    , interval : Maybe Int
    , events : Maybe (List Event)
    }

Schedule is a type alias for a record that contains the cron schedule information. You’ll notice that it may have an events element that contains a list of Event records.

type alias Event =
    { schedule : String
    , interval : Maybe Int
    , hook : String
    , args : List ( String, String )
    , timestamp : Int
    , seconds_due : Int
    }

Event is yet another type alias for a record that holds an individual cron event’s information.

type alias Divider =
    { name : String
    , val : Int
    }

Divider is a convenience type that I defined for handling the data we later use in the view functions to construct the display intervals for how long an event has until it is due (e.g. “3h 40m 5s”).

type alias Flags =
    { strings : Strings
    , nonce : String
    , timer_period : String
    , schedules : Value
    }

Flags defines the shape of the data we expect to receive into the app from JavaScript as returned by the main function.

init : Flags -> ( Model, Cmd Msg )
init flags =
    ( Model flags.strings flags.nonce (decodeTimerPeriod flags.timer_period) (decodeSchedules flags.schedules), Cmd.none )

init is a function that initializes the Model with any default data, and potentially sends off a Command Message to the update function should something need to happen at program startup.

In this case, init takes in Flags as a variable called flags, assigns each of the elements to a new Model, and returns that with the default Cmd.none to say there is no further action to be taken on startup.

It’s here that you notice two things:

  1. Model is really a function that returns a record of type Model.
  2. init doesn’t seem to actually return anything.

There are no explicit return statements in Elm. Each function is an expression whose result is automatically returned. Think back to your basic math.

z = x + y

If x = 1 and y = 2, you would easily deduce that z = 3. It’s no different in Elm. A function is an expression that if you call with certain parameters will naturally return a result.

add x y = x + y

Call add 1 2 and you’d expect 3 as the answer.

Of course, init looks like it’s returning two results, a Model and a Cmd Msg, but it is really a single Tuple.

Just so you know, EVERYTHING is a function in Elm. Once you get to grips with that, a lot of things start to make sense.

Messages

Here we define the type of messages that can be handled by the update function.

type Msg
    = Tick Time
    | FetchSucceed (List Schedule)
    | FetchFail Http.Error
    | RunNow Event
    | PostSucceed String
    | PostFail Http.Error

type Msg defines a brand new type. It’s not just a Record, String, Int or whatever, given a distinct name, no, this is a brand new type with a structure as defined by you (well, me in this case).

This particular flavour with all the pipes is called a Union type. It defines that a Msg could be something we call a Tick type with a Time payload (Time is defined in the core Time module as really being a Float). But Msg could instead contain something we call FetchSucceeed which would expect to have an associated List of Schedule records. And so on for the other types we define in the union type.

This means we can accept or return a Msg in functions that match any of the unioned types we specify, but nothing else. In this way the compiler can check the types of data moving through the functions as Msgs but we have flexibility as to what we produce in certain scenarios.

You’ll see the Msg type being used a lot in the remaining functions I have to show you.

View

Ok, lets define our HTML output with the view function we promised main would be a good fit for it.

view : Model -> Html Msg
view model =
    div []
        [ h3 []
            [ text "Schedules" ]
        , ul [ class "cron-pixie-schedules" ]
            (List.map (scheduleView model) model.schedules)
        ]

The view function takes in data that should adhere to our Model, and then work with it to build the HTML.

At the top level we use the div function to create a <div></div> that anchors our output. Square brackets are a shorthand way of defining a List. The div function, and pretty much all of the functions that are used to create HTML elements, takes two arguments, the attributes for the element and the content for the element. So, in the above call to the div function it gets an empty list of attributes and a h3 and ul tag as its content.

The h3 tag also has no attributes (doesn’t need an id or any extra classes). It does however include some (HTML escaped) text to give the widget its “Schedules” header.

The ul is given the class “cron-pixie-schedules”, which is used by the CSS. To generate the list of li elements to be the content for the <ul></ul> tag, the list of schedules from the supplied model data is mapped onto a function called scheduleView.

scheduleView : Model -> Schedule -> Html Msg
scheduleView model schedule =
    li []
        [ span [ class "cron-pixie-schedule-display", title schedule.name ]
            [ text schedule.display ]
        , eventsView model schedule.events
        ]

scheduleView takes in the full model data plus a single Schedule, returning some HTML.

As you probably expected it uses a function called li to generate the <li></li> tag, which in this case includes a span which has a class and title attribute assigned and includes the text from the supplied schedule’s display value.

As a schedule may need to show a bunch of child cron events, the li also calls the eventsView function with the model and schedule’s events value.

eventsView : Model -> Maybe (List Event) -> Html Msg
eventsView model events =
    case events of
        Just events' ->
            ul [ class "cron-pixie-events" ]
                (List.map (eventView model) events')

        Nothing ->
            text ""

In the eventsView function we come across the Maybe type for real. And for the first time we also use some branching logic to decide what to do based on some of the input data by using a case statement.

A Maybe allows you to specify that a value may be present, or there might be nothing. It is defined as…

type Maybe a
    = Just a
    | Nothing

Meaning there will just be a value to be returned, or “Nothing”.

The case statement runs through a list of match expressions until it finds the first one that matches the state of the variable it is testing. In this case we’re testing whether the events variable has a value that can be used to generate a ul full of events, or just return some empty text because events has nothing in it.

eventView : Model -> Event -> Html Msg
eventView model event =
    li []
        [ span [ class "cron-pixie-event-run dashicons dashicons-controls-forward", title model.strings.run_now, onClick (RunNow event) ]
            []
        , span [ class "cron-pixie-event-hook" ]
            [ text event.hook ]
        , div [ class "cron-pixie-event-timestamp dashicons-before dashicons-clock" ]
            [ text " "
            , span [ class "cron-pixie-event-due" ]
                [ text (model.strings.due ++ ": " ++ (due event.timestamp)) ]
            , text " "
            , span [ class "cron-pixie-event-seconds-due" ]
                [ text ("(" ++ (displayInterval model event.seconds_due) ++ ")") ]
            ]
        ]

By now you should be getting the hang of what these view functions are like, so I’ll keep the description of eventView shorter.

It takes the model and event data, and spits out a bunch of span and div elements with filled in text.

There are two things to look out for however.

One of the attributes for the span that shows the “controls-forward” icon is onClick (RunNow event). This creates a click handler that will send a RunNow type Msg with the current Event as its payload. This is how the sequence for updating an event as “due now” so that the WordPress cron runs it starts.

For the first time we’re concatenating strings together with the ++ operator. That’s important. Don’t try and use + as it’s for math only.

due : Int -> String
due timestamp =
    timestamp * 1000 |> toFloat |> Date.fromTime |> Date.Format.format "%Y-%m-%d %H:%M:%S"

The due function returns a string containing the due date for a cron event when given the Integer timestamp in seconds since the UNIX epoch.

The seconds are converted to milliseconds by multiplying by 1000, with the result of the calculation piped to a function called toFloat that converts the Integer to a Float. The result of which is piped into the fromTime function from within the Date module in order to convert it to a bonafide Date that can be piped into the Date.Format module’s format function to produce the final String we want.

package.elm-lang.org. Elm’s package manager enforces semantic versioning, meaning it will not let an author publish a package with just a patch level version bump if it detects that the API for the package has changed enough to warrant a minor or major version change. The Elm Package Manager is a huge asset to the Elm community.

To add the elm-date-format package to my project, all I had to do was:

elm package install mgold/elm-date-format

And then add an import at the top of CronPixie.elm:

import Date.Format

With that I had full access to all the functions within the Date.Format module:

intervals : Model -> List Divider
intervals model =
    [ { name = model.strings.weeks_abrv, val = 604800000 }
    , { name = model.strings.days_abrv, val = 86400000 }
    , { name = model.strings.hours_abrv, val = 3600000 }
    , { name = model.strings.minutes_abrv, val = 60000 }
    , { name = model.strings.seconds_abrv, val = 1000 }
    ]

intervals is almost a constant. If it wasn’t for needing to set the name for each of the records from translatable strings, it could have been a function that simply produces a constant result.

Here you see one way of creating records from known values. However, because those records conform to the definition of the Divider type, I could have assigned their values with statements like Divider model.strings.seconds_abrv 1000.

The intervals function is used in the displayInterval function.

displayInterval : Model -> Int -> String
displayInterval model seconds =
    let
        -- Convert everything to milliseconds so we can handle seconds in map.
        milliseconds =
            seconds * 1000
    in
        if 0 > (seconds + 60) then
            -- Cron runs max every 60 seconds.
            model.strings.passed
        else if 0 > (seconds - model.timer_period) then
            -- If due now or in next refresh period, show "now".
            model.strings.now
        else
            divideInterval [] milliseconds (intervals model) |> List.reverse |> String.join " "

To create a string such as “3h 40m 5s” from a number of seconds, displayInterval uses a local variable to hold the seconds converted to milliseconds, and then depending on how many seconds there are decides on whether to display text such as “passed” or “now”, or divide the seconds up into weeks, days, hours, minutes and seconds using the divideInterval function.

divideInterval : List String -> Int -> List Divider -> List String
divideInterval parts milliseconds dividers =
    case dividers of
        e1 :: rest ->
            divideInterval' parts milliseconds (head dividers) (withDefault [] (tail dividers))

        _ ->
            parts


divideInterval' : List String -> Int -> Maybe Divider -> List Divider -> List String
divideInterval' parts milliseconds divider dividers =
    case divider of
        Just divider' ->
            let
                count =
                    milliseconds // divider'.val
            in
                if 0 < count then
                    divideInterval ((toString count ++ divider'.name) :: parts) (milliseconds % divider'.val) dividers
                else
                    divideInterval parts milliseconds dividers

        Nothing ->
            parts

divideInterval and its sub-processing cousin divideInterval' function work together to pull out the parts that make up the interval string.

Update

The update function is like the nerve centre for the application, taking in messages emitted from the view and other places, updating the Model in response, and potentially sending out further messages.

At its core, my update function is just a case statement that matches on the different types of messages that we defined in the Msg type. The beauty of this setup is that if you accidentally miss a match off from the case statement, the Elm compiler will politely explain the error of your ways.

wp-cron-pixie-elm-compiler-error

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Tick newTime ->
            ( model, getSchedules model.nonce )

        FetchSucceed schedules ->
            ( { model | schedules = schedules }, Cmd.none )

        FetchFail err ->
            ( model, Cmd.none )

        RunNow event ->
            let
                dueEvent =
                    { event | timestamp = (event.timestamp - event.seconds_due), seconds_due = 0 }
            in
                ( { model | schedules = List.map (updateScheduledEvent event dueEvent) model.schedules }, postEvent model.nonce dueEvent )

        PostSucceed schedules ->
            ( model, Cmd.none )

        PostFail err ->
            ( model, Cmd.none )

Given that update uses the same pattern as init does, I’ll not go into much detail. One thing that should probably be mentioned is what is happening with the model in code like the following.

FetchSucceed schedules ->
    ( { model | schedules = schedules }, Cmd.none )

Here we are creating a new record (remember, everything is immutable in Elm) from the model record, substituting the models schedules element for the contents of the schedules variable that was passed along with the FetchSucceed message.

In languages such as PHP that allow mutability you could use something like…

$model->schedules = $schedules;

But then you’ve altered $model everywhere, and that could have side effects that you might not expect. With immutability when model is created with its data you have confidence that it is the same from that moment on and no function you call will alter the data at all. The only way to alter data is to pass it into a function that returns a new variable. So many errors are averted simply by having immutability and therefore virtually no side effects.

getSchedules : String -> Cmd Msg
getSchedules nonce =
    let
        url =
            Http.url "/wp-admin/admin-ajax.php" [ ( "action", "cron_pixie_schedules" ), ( "nonce", nonce ) ]
    in
        Task.perform FetchFail FetchSucceed (Http.get schedulesDecoder url)

When update gets a Tick message it calls getSchedules with the current nonce. This function creates a URL for the “cron_pixie_schedules” action that our backend code will respond to with a JSON encoded list of cron schedules.

To perform the GET request, the Http.get function is called and given a function that provides a JSON decoder as well as the URL to request data from. As the request could fail or succeed, it is wrapped in a Task that denotes what messages to send depending on the result.

schedulesDecoder : Decoder (List Schedule)
schedulesDecoder =
    list scheduleDecoder


scheduleDecoder : Decoder Schedule
scheduleDecoder =
    object4 Schedule ("name" := string) ("display" := string) (maybe ("interval" := int)) (maybe ("events" := (list eventDecoder)))


eventDecoder : Decoder Event
eventDecoder =
    object6 Event (oneOf [ "schedule" := string, succeed "false" ]) (maybe ("interval" := int)) ("hook" := string) ("args" := eventArgsDecoder) ("timestamp" := int) ("seconds_due" := int)


eventArgsDecoder : Decoder (List ( String, String ))
eventArgsDecoder =
    oneOf
        [ keyValuePairs string
        , succeed []
        ]

The above Decoder functions provide ways of decoding the structured JSON that we get from the PHP backend. Elm has some basic abilities to convert JSON values into Elm values, but to get our uniquely structured JSON into our structured Elm values we naturally have to specify the mappings and their types.

There’s some finessing of the data in some cases, such as trying to get a “schedule” value from an Event, but falling back to a “false” String when one isn’t supplied (e.g. for the “Once Only” schedule that is constructed for non-repeating cron events).

decodeSchedules : Value -> List Schedule
decodeSchedules json =
    let
        result =
            decodeValue schedulesDecoder json
    in
        case result of
            Ok schedules ->
                schedules

            Err error ->
                []


decodeTimerPeriod : String -> Float
decodeTimerPeriod string =
    let
        result =
            String.toFloat string
    in
        case result of
            Ok float ->
                float

            Err error ->
                5.0

Related to the Decoder functions are the couple of functions above called decodeSchedules and decodeTimerPeriod. These simply decode passed in JSON data and return a list of schedules or properly cast timer period respectively. They are both used from the init function to handle the JSON data injected into the app at startup via the optional flags.

updateScheduledEvent : Event -> Event -> Schedule -> Schedule
updateScheduledEvent oldEvent newEvent schedule =
    case schedule.events of
        Just events ->
            { schedule | events = Just <| List.map (updateMatchedEvent oldEvent newEvent) events }

        Nothing ->
            schedule


updateMatchedEvent : Event -> Event -> Event -> Event
updateMatchedEvent match newEvent event =
    if match == event then
        newEvent
    else
        event

The updateScheduledEvent and updateMatchedEvent work together to update a specific event record nestled in a schedule so that the displayed event properly reflects that it has just been set as “due”. This happens in response to a “RunNow” message having been sent after a click on a “Run Now” icon.

postEvent : String -> Event -> Cmd Msg
postEvent nonce event =
    let
        url =
            "/wp-admin/admin-ajax.php"

        eventValue =
            Json.object
                [ ( "hook", Json.string event.hook )
                , ( "args", Json.object (List.map (\( key, val ) -> ( key, Json.string val )) event.args) )
                , ( "schedule", Json.string event.schedule )
                , ( "timestamp", Json.int event.timestamp )
                ]

        body =
            multipart
                [ stringData "action" "cron_pixie_events"
                , stringData "nonce" nonce
                , stringData "model" (Json.encode 0 eventValue)
                  -- , stringData "model" (Json.encode 0 eventValue)
                ]
    in
        Task.perform PostFail PostSucceed (Http.post string url body)

When a RunNow message is sent to the update function it not only returns a new Model with the associated Event set as due to update the display, it also uses the postEvent function to POST the newly updated Event data to the backend.

The PHP backend expects an “action”, “nonce” and “model” to be sent in the body of the POST request, so postEvent constructs a multipart body with the required data. The model is encoded as a JSON string so that its structure can be preserved and unpacked on receipt.

Subscriptions

The last bit of code we need to talk about is how we generate the Tick message that the update function responds to by calling the getSchedules function to request and and display updated data every 5 seconds.

subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every (model.timer_period * second) Tick

The Time.every function publishes the current time as a timestamp at whatever interval you ask of it. We ask it to tell us the current timestamp every model.timer_period seconds (which equates to 5 seconds), and send out a Tick message with the value in response. That’s it. Elm does the stringing together of all the things to ensure that subscriptions are processed and their emitted messages sent to the update function.

Does It Still Work?

Of course it does!

wp-cron-pixie-demo-elm

The only discernable difference with the previous versions is that I’m now formatting the due dates in a slightly more compact and fully numeric SQL-like format.

You can find the full source to the Elm version of WP Cron Pixie on GitHub.

Wrap Up

I thoroughly enjoyed writing this version of the plugin, more than either of the others. For me, Elm has brought functional development to the web in a way that is very approachable, practical, and relatively easy to grasp.

The simple flow of data through functions with no unplanned side effects reduces the likelihood of getting yourself into a tangled knot like you might with imperative languages. The language features coupled with the compiler is wonderful in the way that it picks up on potential bugs and politely refuses to complete (with helpful suggestions on ways to fix the problem).

Above all, it feels pretty fast to develop with and refactoring is a breeze due to the confidence installed by the type system and compiler.

If you’d like to learn more about Elm, I’ve already scattered this article with links to the excellent Elm Guide. I also thoroughly recommend the Elm In Action book by Richard Feldman, which is currently in “Early Access” and shaping up nicely. For a great free video course, check out James Moore’s Elm For Beginners.

Who’s up for developing their next frontend project in Elm? Let me know in the comments.

About the Author

Ian Jones

Ian is always developing software, usually with PHP and JavaScript, loves wrangling SQL to squeeze out as much performance as possible, but also enjoys tinkering with and learning new concepts from new languages.

  • Edvinas Gurevičius

    simple plugin would be more pleasant, but thanks 😉