Hi!
Long post ahead! Sorry! ♂
Intro
Before Christmas, I learnt about the 7GUIs UI programming benchmark, and it seemed quite interesting to try and do.
I looked around and the Elm implementation was quite old and incomplete, so I went on to work on it during the Christmas break for fun.
The benchmark consists of 7 distinct UIs that you have to build, where they give you a mockup, and a text specification of the behaviour. As you progress, the exercises increase in complexity and scope, the last one being the hardest.
Links:
- Live examples
- Github repo: joakin/elm-7guis
Commentary
Before continuing with my comments over the tasks and my implementations, I can say I recommend the tasks as a way to exercise different parts of elm and the web platform, and I found it an interesting and rewarding exercise. There are many ways to implement the same tasks, specially the more complex ones, so I’m sure someone else would do it differently and come up with interesting insights.
Setup
Here are some of my choices for setting up the project, with some reasoning:
Build and development tooling
Mainly based on a Makefile.
I wanted to avoid npm scripts, and JS bundler npm hell as much as possible, so all the build rules are in the Makefile instead. Nothing fancy. My disk appreciates it. For development, I used entr
as the file watcher with the watch
Makefile rule.
- Kind of unrelated but I’ve taken some time to get to learn better
make
(pdf book) after so many years using it just copy pasting and I have to say it is very useful
I did add a package.json to get the uglify-js
minifier which is invoked in the Makefile
. I did learn by reading this forum about elm-minify
and I will be migrating my projects to use it rather than manually configuring uglify. I think encapsulating the best minification practices from the community in that tool is a great idea.
Folder structure
-
src/
- Common modules (
Ui.elm
HTML helpers, and the main page index.html and index.css) -
tasks/{task-name}/
- Home for each tasks code. Make will look for a
Main.elm
inside each folder to compile code topublic/
- Home for each tasks code. Make will look for a
- Common modules (
-
public/
- Compiled and deployable assets. What gets deployed to github pages.
Tasks commentary
If you need to understand a task, look at the specifications.
1. Counter (source)
Welp, not much to comment here. We’ve seen counters to death so many times.
I can say that the task itself requires the counter number to be displayed on an input box, I guess because the tasks were quite Java/Swing oriented initially. In any case, I complied, using readonly True
so that it wasn’t modifiable.
A thing I used here that I picked up from some of @unsoundscapes’s examples is this kind of short hand main setup:
main =
Browser.sandbox
{ init = 0
, update = \_ count -> count + 1
, view = view
}
Very concise and nice if you ask me.
2. Temperature converter (source)
The classic, the bane of remote cross-continental teams chit-chat, celsius <-> farenheit converter
I didn’t over-engineer and kept the temperatures as String
, but if this were a bigger application you probably would want them to be custom types.
As something interesting, the requirement that when the input for a temperature stops being valid, the other input box won’t update or blank, but keep the last valid conversion value. I accomplished this by forking the logic to update with the valid conversion rules, or just as plain strings depending on the float parsing:
case String.toFloat value of
Just num ->
updateTemperature temperatureUnit num model
Nothing ->
updateAsString temperatureUnit value model
3. Flight Booker (sources)
Things start getting a bit more interesting here, in terms of requirements. Multi-page widget with form parsing, enabling/disabling parts and validation rules.
I did it mostly in one file, but ended up extracting a couple of data structures to make the code in Main.elm a bit nicer:
ValidatedDate.elm
Custom type that forces you to validate a date from string. Encapsulates the parsing from string, a bit of date logic, and date to string conversion.
I wasn’t ready for elm-parser yet, so went with elm-regex. And boy is it awkward… Specially if you want to use capture groups and more complex regexes…
FlightKind.elm
A very small enum data structure, a bit less noise for Main.elm
.
Main.elm
I chose to represent the pages as a custom type, and kept the from info in the model as is, even though the one-way makes no sense with the return flight, we do need to keep the disabled value visible, so made more sense to keep both from and to dates at all times:
type Model
= Form BookingForm
| Booked BookingForm
type alias BookingForm =
{ kind : FlightKind
, from : ValidatedDate
, to : ValidatedDate
}
Otherwise pretty normal Elm architecture stuff, with a few functions to help separate the views. I’m happy about how it turned out.
4. Timer (source)
This was generally a very weird task, and I had some problems understanding it. I guess it is there for testing crossing state lines and general cohesion and reactivity of all the UI, but it doesn’t make a lot of sense on its own.
It was quite straightforward to do, given the Elm architecture and how it is well set up for these kind of reactive tasks with subscriptions, update, and a view derived from the model.
I’d highlight on this one some HTML: The <input type="range">
and the <progress />
tags made it really easy to make this task work.
A small gotcha was formatting float numbers for the UI (given the IEEE 754 float weirdness), and couldn’t find an easy way to do so within Basics
. In JS I would do this with toFixed
. In the end I gave in and ended up using myrho/elm-round
. It is the only non-elm/__
package I used on all the tasks.
5. CRUD (source)
This one I found quite straightforward as well, with a bit of thought around how to lay out the UI.
This is where I wrote the small Ui.elm
, which is a few functions that use flexbox with some spacing for sort-of mini elm-ui. Just a few primitive functions, column
, row
, box
, and a spacer
. This kind of layout is easy and piece of cake for CSS grid, but I still don’t know that well enough to use it.
For the list of users I chose to use List ( Int, User )
, Int
being the id. Not the most correct or future proof but quite straight forward. I would definitely move this data structure out if this grew, along with the lastUserId
, to abstract the List
logic and the auto-incrementing id logic to ensure consistency and be able to change the implementation transparently.
6. Circle Drawer (source)
This was an interesting one. It is a canvas-like drawing thing with undo/redo and a modal window.
Welp, elm doesn’t have canvas right now, and I don’t know svg too well (and I’ve read bad things about the clicks and coordinates on svg), so I decided to go with plain div
s and CSS.
The history stuff was fun to program and test, undo/redo is always like magic, and with immutable data it is quite easy.
The modal part, is one where the web platform doesn’t deliver. We do have confirm and alert modals, but anything else you have to code custom, maybe with window.open
, or in-page. In my case I didn’t bother with draggable closable fake-windows, and didn’t want to mess with ports and a new elm app for a new window. So instead I just added a fixed container that shows when a circle is selected, below the canvas.
Another fun part was figuring out the mouse position on "canvas’ click for creating new circles. I knew how to do it semi-straightforward from a past side project, where I learnt about event.page{X,Y}
and event.target.offset{Top,Left}
, which can help if you don’t have fun nested scrollable containers on the page. You can see the decoders, which could help you depending on your situation.
Finally, a small weird thing. When you click and add a circle, and click it again to select it and show the modal at the bottom: The input range doesn’t really move when dragging it, but it does work and the events are triggered and values updated. I’m not really sure what I’m doing wrong here, given the other task had a slider that worked fine. I tried adding keyed
on a test but didn’t fix it. I looked at the devtools but couldn’t see anything weird, so I have zero idea about why it gets stuck, and couldn’t reproduce standalone on an ellie.
7. Final boss: Cells (source)
This was the hardest task, and I believe it is by a huge margin, compared to the other ones. It took me 5 or 10 times longer than the previous task to complete, and probably a bit more to be happy about the state of the code.
Basically, it is a toy spreadsheet, but with the core functionality implemented. You can double-click to edit cells.
Beware if you want to try it out, the problem statement is lightly specified, and it links to an article that has the full spec mixed with a Scala implementation. Through implementation I had to go read the spec, and some times the Scala version to understand the details in the behavior.
I took one bite at a time, and worked in this order:
- Get the full UI rendering
- Used absolutely positioned divs with strict height/width, just because. It has served me well.
- Add cell editing
- Fought with content-editable and input events, cried, sweated , and finally ended up adding a full-size input on the cell and did normal editing.
- Added cell content parsing
- Tried with regexes, cried a lot again. Decided to use elm-parser
- Tried with elm-parser. Cried more. Started to get some results. Then found bugs and had to learn more about
backtrackable
/commit
, couldn’t find anything useful. Cried more and added it randomly, and then got it working. To this day I’m still not sure if the parser is fully correct, but it seems to work fine- Writing parsers is fun, but I think we can do better explaining more than the basics. I want to do more elm-parser now.
- Added cell evaluation
- This was very cool, it was at the point where formulas started working, but re-evaluating the whole spreadsheet on every change/render. I didn’t see any slowdown on my desktop, but felt dirty stopping here
- Added dependency tracking
- Re-structured the parsing to extract dependencies and cache evaluation and refactored
Cell
- Added dependency tracking between cells
- Added cell evaluation propagation to the dependents
- Re-structured the parsing to extract dependencies and cache evaluation and refactored
- Cleanup, organize, refactor. Draw better boundaries between data structures.
Html.Lazy.lazy6
because why not
Structure
Ended up with the following files:
-
Position.elm
- Row/Column pair data structure, with some helpers and its parser
-
Cell/Parser.elm
- Cell content parser and types
-
Cell.elm
- Cell data structure with creation, manipulation, conversion and rendering functions
-
Matrix.elm
- A bi-dimensional matrix that uses
Position
. Hidden in it is the storage as a 1-dimension array. Used to store all the cells
- A bi-dimensional matrix that uses
-
Dependencies.elm
- Data structure to keep track of dependencies between
Position
s, and get the what depends on aPosition
- Data structure to keep track of dependencies between
-
Main.elm
- Connective tissue between the data structures, the DOM events, and the main update logic
Comments
This was a very interesting task, but way bigger than the previous ones. I ended up quite happy about the abstractions and data structures I ended up with, and there are a lot of interesting details (I think) on the implementation that I would love to post about and expand, but it would be too much for this post already.
I may make a few youtube videos or streams, or maybe try to apply to one of the conferences and see if I can talk about it.
Final thoughts
It was a very interesting exercise as a whole, and I learnt a bunch of things in the process. The exercises are not very well calibrated in terms of difficulty I feel, and the UIs are not the most usable or modern, but it is still useful.
I also feel like I am at a point where I’m comfortable with Elm and can play well to its strengths, which feels good. I enjoyed building, but also thinking about the data structures, finding the right (for me) boundaries when things hurt, and designing the code’s behavior.
But I also see a lot of things I don’t know about (parser combinators for example) or haven’t used in anger (elm-test, lazy, benchmarking, elm-ui) and I would like to do more of. I also haven’t used elm in any really big or production-level applications since 2 and a half years ago sadly.
Another insight I got from the task, is how much the web platform provides out of the box for building UIs and applications. It isn’t the most coherent or modern UI toolkit, but it is a real wonder, public and free. It would be terrible to see it become irrelevant in my opinion.
I hope the commentary has brought some value to you. I’m happy to chat and discuss about the tasks, explain any weird thing I’ve done in the implementations, and in general connect with anyone interested on this.
Have a nice day!