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://[email protected]/ianmjones/wp-cron-pixie.git"
},
"author": "Ian M. Jones <[email protected]>",
"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:
Model
is really a function that returns a record of typeModel
.init
doesn’t seem to actuallyreturn
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 Msg
s 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.
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 model
s 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!
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.