Passion projects and ruminations related to IT development.
Related
My exposure to frontend has been minimal to say the least. I am knowledgeable about HTML and even had some CSS adventures as have most other people. More than a decade ago I used jQuery to create a dropdown menu for a summer project. Some simple browser games using vanilla Javascript were made. And that’s about it. That is still enough exposure for a person like me to realize I would be setting myself up for a rather lengthy and unpleasant odyssey if I were to opt to polish up my Javascript skills or use one of many frameworks out there.
Why not ask ChatGPT for Javascript alternatives? Most recommendations were frameworks (Django, Ruby on Rails, …), but way down the list was a language called Elm. It describes itself as “A delightful language for reliable web applications”. I was not sure if reliable and frontend would ever be allowed in a same sentence. I decided to give it a try.
Turns out, I had a pleasant time using Elm. It fits my use case, which started out as “I don’t want to waste time debugging Javascript”. Elm catches a lot of errors with its functional style that one would make otherwise. The guides are among the best I have seen. Sure, it takes a good deal of hands on learning to get acquainted with the Elm architecture, which is a cycle involving the three concepts of view, update and model. Another steep learning curve is how to go from a simple app to a single page application (SPA), if an SPA is even the right choice. Still, the syntax is oddly reassuring once you get into it. Refactoring did actually feel safe and fast, and not like opening a Pandora’s box.
One of the most alluring aspects was the idea of custom types and how they can aid in modeling some complex behaviour. There is an excellent talk by Richard Feldman on this subject, which was an eye opener. I also appreciated the focus on data structures in this talk by Elm’s creator Evan Czaplicki.
Going from next to no knowledge to being able to come up with a working MVP does not happen overnight. If I were to sum up my learning path, it would be an interplay between guides and immediately applying them in a blank slate project. The blank slate projects cannot be underestimated. Guides are crutches, and crutches will not always be present. The more you rely on crutches, the greater the fall will be when you do arrive at the gates named “Well, now let’s use this for our case”.
As expected, we start with the Elm guides, and do every extra exercise mentioned in each part. Make that SVG clock, throw those double dice. Then make something yourself, a blank slate project. I made a simple TODO app, you can make it as complex as you like, as long as you start it from scratch. Think about your data structure, design your messages and link them with your view.
Now do something bigger. For example, I wanted to incorporate Auth0 into my app, to get better acquainted with OAuth2. So we search and find that some preliminary guide is already available. Alright, let’s copy it, make a simple button and use ports to direct the user to the universal login page. We code the bare essentials and find out it doesn’t work, it was written for Elm 0.18 and we are using version 0.19. Fine, let’s inspect the errors, do some console logging for the interop part and work our way through bit by bit, following the data through the app. A small handful of fixes got the job done. Store it somewhere alongside the observations. You just made progress.
We can make simple projects and use Auth0. Now is the time to think bigger. A full fledged SPA seems scary, if there a framework? Ah, there’s elm-spa, oh and a successor called elm-land created by Ryan Haskell-Glatz (not sure if the Haskell part is just a shoutout to the language). Alright, it has guides, let’s go over them. Do some extras. Try using elm-ui, and perhaps elm-json-decode-pipeline. Join the discord channel, search its history for issues you face. Check the Github issues, get out of your comfort zone. View some of the excellent talks present on @ElmEurope and @elmconf.
Once again, we incorporate Auth0, this time into elm-land. We face new challenges, but they are smaller thanks to the groundwork we laid earlier. We use elm-land’s Auth module, we add ports and functions to the Effect modules for signing in and out. Elm is helping us along the way, pointing out our errors. Again we have it working. Let’s start adding our API calls to our backend. Slowly but steadily you get into the flow, scaffolding is the name of the game. Take your time, some directions will be dead ends, but even what does not work out still amounts to precious knowledge.
We settled on using elm-land to skip the heavy lifting and rely on the expertise of people far better in this than we are. By making Auth0 work first with vanilla Elm and then elm-land we split the knowledge gaps into more manageable gaps to abridge. We used most of the features of elm-land, including the Shared, Effect and Auth modules. Also the layouts. We go a little deeper into some of the aspects below.
Among the additional modules are elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) for styling without CSS and elm-json-decode-pipeline for the many JSON messages that require encoding and decoding.
We have a simple homepage, a games page where you can create new games and see your history and a game page for an individual game where you solve questions and see the summary if the game is finished.
The product in development can be seen on Github and is hosted on Netlify. Here is the result.
I had an epiphany using Elm. You see, I was struggling and getting worried over my update function for the game page. Let me tell you what was bothering me.
The game page has a flow to it. When you create a game and go to its page, it will first load some game details (creation date, short hash, …), no questions yet. When the loading finishes it makes a decision. Has the game ended yet? If so, display a questions summary, indicating which questions the player answered correctly. If the game has more unanswered questions, get the latest unanswered one and pose it to the player. The player gets the question alongside 4 options. The player selects an option and presses submit. The backend API sends back some feedback, indicating whether the player answered correctly or not. A next button appears so the player can load the next question. If no questions remain, we load the summary as mentioned before.
How do I model this? What does my data structure look like? Someone accustomed to Javascript or one of the frameworks may work with a big JSON object, storing everything there and only parts depending on what state the application is in. Elm however, offers you custom types. Here’s what I settled on:
You can clearly see the progress I described here earlier. First we have nothing (Loading). Then we have some game details (Loaded). Then we have a question (LoadedQuestion) or a summary if the game is finished (GameOver). Then we have some feedback (LoadedFeedback). Then a new question (LoadedQuestion) or finished (GameOver).
At first I got anxious over what this meant for my update function. Will this not result in some huge cartesian product of my messages and my models states like so:
Things get bloated and ugly real fast. But then came the epiphany: that is what the view function is for. In the view you show UI elements dependent of the state of your model to progress to the next model state. And in doing so you pass the variables you know that new state needs. This means three things. First, your messages need to be smarter:
This time, relevant data is passed through the message itself. This means we no longer have to iterate over our model state in the update function:
We get the game data directly from the message. Thirdly, in the view function it means that only in the model states Loaded and LoadedFeedback, both of which have access to the game data, we construct the message NextQuestion passing it the game data. Nicely decoupled and the code is way cleaner. No need to overthink the update function. No need for a complex double loop in the update function.
As expected, this is where Elm ports come in. Using elm-land this means we have an interop.js
file:
Tokens and profiles are stored in local storage and three ports are used. Two outgoing ports to authorize and log out from the Elm app, and one ingoing port to receive the authentication result from Auth0 into the Elm app.
We defined the outgoing ports in Effect.elm
:
The Auth.Authentication
and Auth.Auth0
modules are straight up copies from the Auth0 documentation. The local storage data passed in the interop.js
files gets piped into Shared.elm
as app flags:
You also see we pass the outgoing ports from the Effect
module into the Auth.Authentication
module. The one ingoing port is also defined in Shared.elm
:
And we also link up the message from the Effect
module functions signIn
and signOut
here with the Auth.Authentication
module:
With the shared model being updated now when authentication comes in from the port we can use this in the Auth.elm
module to redirect when either the user is not logged in or when the token has expired:
To summarize, if the user is logged out and the user is visiting an “auth-only” pages they will be redirected to the sign in page. On the sign in page, we call Effect.signIn
which goes through the outgoing port and loads the Auth0 universal login page. The redirection URI will be the callback
page. We then use the Auth.Action.showLoadingPage
to display nothing until the token has been loaded. At that point the user is logged in and the app redirects to the homepage. In the future we will adapt this to redirect back to the “auth-only” page.
One last thing: in the Elm app we can be in a LoggedIn
state but with an expired token. We adapted the Api.elm
module from the elm-land documentation and included a custom error for unauthorized access (401 Unauthorized):
We subsequently use this error in our page modules to call Effect.signOut
. For example
In one of the first iterations we just generated a new question on the fly every time, having the user wait a few seconds for the response of the REST calls. In a later stage we create questions asynchronously in one go when the game was created, which takes about 2-3 minutes. This meant that as a player creates a game they can visit the game page without there being any questions stored yet on the backend side. We need to give some status update on the creation of the game questions and start loading the first question when they are available. I did not want to take the Process.sleep route and decided to simply leverage subscriptions depending on the model state:
Thus, every 30 seconds, the REST call for the game status is polled and we proceed to the loading of the first question once it is in “READY” status. As I am sure the code can be cleaned up further I figure this a road of less resistance to accomplish the waiting functionality.
The foundation has been laid for the quiz app frontend using Elm, incorporating Auth0 for authentication. While it is not the prettiest thing to look at in its current form, it is functional and working and Elm offers a reassurance that our app will have good uptime.