Introduction
A frontend developer sometimes just wants to drop a JavaScript widget somebody else made into their application and move on. Maybe that widget is a slider, or a menu, or a progress bar, or a spinner, or a tab, or a tooltip that points in a cool way.. And sometimes that same frontend developer would like to write their application in Elm.. Should this developer wait to use Elm until the widget they want is rewritten in Elm? Should they rewrite everything that they need?
NoRedInk ran into this problem a few years ago with datepickers. Now, there are some Elm datetimepicker options, but at the time we needed prioritize building a datepicker from scratch in Elm against using the JS datepicker library we had been using before. We put building an Elm datepicker on the hackday idea pile and went with the JS datepicker library. Even with the complications of dates and time, using a JS datepicker in an Elm application ended up being a fine experience.
So our frontend developer who wants a JS widget? They can use it.
Readers of this post should have some familiarity with the Elm Architecture and with Elm syntax, but do not need to have made complex apps. This post is a re-exploration of concepts presented in the Elm guide (Introduction to Elm JavaScript interop section) with a more ~timely~ example (that is, we’re going to explore dates, datepickers, and Elm ports).
On Dates and Time
The local date isn’t just a question of which sliver of the globe on which one is located: time is a consideration of perception of time, measurability, science, and politics.
As individuals, we prefer to leave datetime calculations to our calendars, to our devices, and to whatever tells our devices when exactly they are. As developers, we place our faith in the browser implentations of functions/methods describing dates and times.
To calculate current time, the browser needs to know where the user is. The user’s location can then be used to look up the timezone and any additional time-weirdnesses imposed by the government (please read this as side-eyes at daylight saving–I stand with Arizona). When you run new Date()
in your browser’s JS console, apart from constructing the Date you asked for, you’re actually asking for your time as a function of location.
Supposing we now have a Date object that correctly describes the current time, we have the follow-up problem of formatting dates for different users. Our users might have differing expectations for short-hand formats and will have differing expecations for long-hand forms in their language. There’s definitely room to make mistakes; outside of programming, I have definitely gotten confused over 12 hour days versus 24 hour days and mm/dd/yyyy
versus dd/mm/yyyy
.
Okay, so computers need a way to represent time, timezones, daylight savings. We use the distance in seconds from the epoch to keep track of time. (If you read about the history of the Unix epoch, that’s not as simple as one might hope or expect either!) Then we need a language for communicating how to format this information for different locales and languages.
We can represent dates in simple and universial formats. We can use semantic and consistent (or close-to semantic and close-to consistent) formatting strings. We can be careful as we parse user input so that we don’t mix up month 2 or day 2. But it’s still really easy to make mistakes. It’s hard to reason about what is going, did go, or will go wrong; sometimes, when deep in investigating a timezone bug, it’s hard to tell what’s going right!
So suppose we’ve got ourselves a great spec that involves adding a date input to a pre-existing Elm app. Where do we start? What should we know?
It’s worth being aware that the complexity of date/time considerations of the human world haven’t been abstracted away in the programming world, and there are at times some additional complications. For example, the JavaScript Date API counts months from zero and days from one. Also worth noting: Dates in Elm actually are JavaScript Date Objects, and date Objects in JavaScript rely on the underlying JavaScript implementation (probably C++).
On Interop
The way that Elm handles interop with JavaScript keeps the world of Elm and the world of JavaScript distinct. All the values from Elm to JS flow through one place, and all the values from JS to Elm flow through one place.
Tradeoffs:
It’s possible to break your app
Suppose we have an Elm app that is expecting a user-named item to be passed through a port. Our port is expecting a string, but oops! Due to some unanticipated type coercion, we pass
2015
through the port rather than"2015"
. Now our app is unhappy–we have a runtime error:Trying to send an unexpected type of value through port
userNamedItem
: Expecting a String but instead got: 2015Your Elm apps have JS code upon which they are reliant
Often, this isn’t a big deal. We used to interop with JavaScript in order to focus our cursor on a given text input dynamically (Now, we use Dom.focus). It’s a nice UX touch, but our site still works without this behavior. That is, if we decide to load our component on a different page, but fail to bring our jQuery code to the relevant JS files for that page, the user experience degrades, but the basic functionality still works.
Benefits:
We can use JavaScript whenever we want to
If you’ve got an old JS modal, and you’re not ready to rewrite that modal in Elm, you’re free to do so. Just send whatever info that modal needs, and then let your Elm app know when the modal closes.
The single most brittle place in your code is easy to find
Elm is safe, JavaScript is not, and translating from one to the other may not work. Even without helpful error messages, it’s relatively easy to find the problem. If the app compiles, but fails on page load? Probably it’s getting the wrong information.
We keep Elm’s guarantees.
We won’t have to worry about runtime exceptions within the bulk of our application. We won’t have to worry about types being inconsistent anywhere except at the border of our app. We get to feel confident about most of our code.
So.. how do we put a jQuery datepicker in our Elm application?
For this post, we’ll be using the jQuery UI datepicker, but the concepts should be the same no matter what datepicker you use. Once the jQuery and jQuery UI libraries are loaded on the page and the basic skeleton of an app is available on the page, it’s a small step to having a working datepicker.
Our skeleton:
{- *** API *** -}
port module Component exposing (..)
import Date
import Html exposing (..)
import Html.Attributes exposing (..)
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = always Sub.none
}
init : ( Model, Cmd Msg )
init =
( { date = Nothing }, Cmd.none )
{- *** MODEL *** -}
type alias Model =
{ date : Maybe Date.Date }
{- *** VIEW *** -}
view : Model -> Html.Html Msg
view model =
div [ class "date-container" ]
[ label [ for "date-input" ] [ img [ alt "Calendar Icon" ] [] ]
, input [ name "date-input", id "date-input" ] []
]
{- *** UPDATE *** -}
type Msg
= NoOp
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! [ Cmd.none ]
Next up, let’s port out to JS. We want to tell JS-land that we want to open a datepicker, and then we also want to change our model when JS-land tells us to.
port module Component exposing (..)
import Date
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..) -- we need Events for the first time
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
init : ( Model, Cmd Msg )
init =
( { date = Nothing }, Cmd.none )
{- *** MODEL *** -}
type alias Model =
{ date : Maybe Date.Date }
{- *** VIEW *** -}
view : Model -> Html.Html Msg
view model =
div
[ class "date-container" ]
[ label [ for "date-input" ] [ img [ alt "Calendar Icon" ] [] ]
, input
[ name "date-input"
, id "date-input"
, onFocus OpenDatepicker
-- Note that the only change to the view is here
]
[]
]
{- *** UPDATE *** -}
type Msg
= NoOp
| OpenDatepicker
| UpdateDateValue String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! [ Cmd.none ]
OpenDatepicker ->
model ! [ openDatepicker () ]
UpdateDateValue dateString ->
{ model | date = Date.fromString dateString |> Result.toMaybe } ! []
{- *** INTEROP *** -}
port openDatepicker : () -> Cmd msg
port changeDateValue : (String -> msg) -> Sub msg
subscriptions : Model -> Sub Msg
subscriptions model =
changeDateValue UpdateDateValue
Note that here, we’re also carefully handling the string that we’re given from JavaScript. If we can’t parse the string into a Date, then we just don’t change the date value.
Finally, let’s actually add our Elm app and datepicker to the page.
$(function() {
elmHost = document.getElementById("elm-host")
var app = Elm.Component.embed(elmHost);
$.datepicker.setDefaults({
showOn: "focus",
onSelect: sendDate,
});
app.ports.openDatepicker.subscribe(function() {
$("#date-input").datepicker().datepicker("show");
});
function sendDate (dateString) {
app.ports.changeDateValue.send(dateString)
}
});
Checking this out in the browser (with a few additional CSS styles thown in):
All we have to do is embed our app, open the datepicker when told to do so, and send values to elm when appropriate! This is the same strategy to follow when working with any JS library.
Fancy Stuff
Storing the final-word on value outside of a UI component (i.e., the datepicker itself) makes it easier to handle complexity. At NoRedInk, engineers have built quite complicated UIs involving datepickers:
NoRedInkers changed the displayed text from a date-like string to “Right away”–and made /right away/i
an allowed input
We can check to see if the selected date is the same as now, plus or minus some buffer, and send a string containing that information to Elm. This requires a fair amount of parsing and complicates how dates are stored in the model.
A simplified version of a similar concept follows–we add some enthusiasm to how we’re displaying selected dates by adding exclamation marks to the displayed date.
Note that this introduces a new dependency for date formatting (rluiten/elm-date-extra).
...
import Date
import Date.Extra.Config.Config_en_us
import Date.Extra.Format
...
viewCalendarInput : Int -> Maybe Date.Date -> Html Msg
viewCalendarInput id date =
let
inputId =
"date-input-" ++ toString id
dateValue =
date
|> Maybe.map (Date.Extra.Format.format Date.Extra.Config.Config_en_us.config "%m/%d/%Y!!!")
|> Maybe.withDefault ""
in
div [ class "date-container" ]
[ label [ for inputId ] [ viewCalendarIcon ]
, input
[ name inputId
, Html.Attributes.id inputId
, value dateValue
, onFocus (OpenDatepicker inputId)
]
[]
]
...
We can make the value of the input box whatever we want! Including a formatted date string with exclamation marks on the end. Note though that if we make whatever is in our input box un-parseable for the datepicker we’re using, we’ll have to give it more info if we want it to highlight the date we’ve selected when we reopen it. Most datepickers have a defaultDate option, and we can use take advantage of that to handle this case.
Note that we’ve also generalized our viewCalendarInput
function. There are some other changes that we need to make to support having multiple date input fields per page–like having more than one date field on the model, and sending some way of determining which date field to update back from JS.
For brevity’s sake, we’ll exclude the code for supporting multiple date inputs per page, but here’s an image of the working functionality:
NoRedInkers created an autofill feature
Leveraging the type system, we can distinguish between user-set and automagically-set dates, and set a series of date steps to be any distance apart from each other by default. The fun here is in determining when to autofill–we shouldn’t autofill, for instance, after a user has cleared all but one autofilled field, but we should autofill if a user manually fills exactly one field.
We actually decided that while this was slick, it would create a negative user experience; we scrapped the whole autofill idea before any users saw it. While there was business logic that we needed to rip out in order to yank the feature, we didn’t need to change any JavaScript code whatsoever. Writing the autofill functionality was fun, and then pulling out the functionality went really smoothly.
NoRedInkers supported user-set timezone preferences
I recommend rluiten/elm-date-extra, which supports manually passing in a timezone offset value and using the user’s browser-determined timezone offset. Thank you to Date-related open source project maintainers and contributors!
Concluding
Someday the Elm community will have a glorious datepicker that developers use by default. For now, there are JavaScript datepickers out there available for use (and some up-and-coming Elm datepicker projects as well!). For now, for developers not ready to switch away from jQuery components, interop with JavaScript can smoothly integrate even very effect-heavy libraries.
There are components that don’t exist in Elm yet, but that shouldn’t stop us from using them in our Elm applications and it shouldn’t stop us from transitioning to Elm. Projects that need those components can still be written in beautiful, easy-to-follow, easy-to-love Elm code. Sure, it would be nice if it were all in Elm–for now, we can use our JavaScript component AND Elm!