Playing around with Elm SPAs and structuring them for larger scale applications.
Everything is set up in package.json, meaning you just have to run,
$ npm i
npm run dev
starts a local development server with hot reloadingnpm run build
builds a production optimized file in dist/elm.min.js with a sourcemapnpm run test:verify
runs the Elm doc/examples testsnpm run test
andnpm run test:watch
runs unit tests, with the latter running in watch mode
A brief overview of the structure inside the src/
folder:
Page
is where individual pages liveLayout
is where the document/page layout is controlledApplication
is where we stitch together all the page views, updates and routesHelper
are for convenient helper functionsCore
should usually not be changed, and is responsible for stitching together everything behind the scenes
To pass environmental variables to Elm, we do this via a combination of Elm init flags and a pre-processor script for building the index.html
file.
This preprocessor simple runs a replacement of process.env...
variables in the original index.unprocessed.html
, and replaces them with actual values. Validation is performed to ensure no leftover process.env
are present after procesing the index file.
The environmental variables that populate process.env
both come from the environment, but also from a .env
/ .env.development
file (.env
is only when process.env.STAGE
is production/staging).
Finally, now that we have substituted our process.env
with actual values, they are then passed into Elm via Elm.Main.init({ flags: ... })
.
Adding a new environmental variable to be passed to Elm necessitates the following:
- Add a replacement target/value in
index-pre-process.js
- Set up the variable that takes the target, in
index.unprocessed.html
, and pass it to the Elm init - Make sure the new
env
variable is either in your environment or in.env
/.env.development
- Handle decoding the flag in Elm, inside
src/Application/Config.elm
The applications assumes that the access token lives in a cookie accessToken
(specified in index.unprocssed.html
), and that the API needs this passed to it via a header value as Authorization: Bearer xxx-xx-xxx-xxxx
.
Upon receiving a HTTP 401 Unauthorized, the application will redirect the user to the authentication flow. This is controlled by two variables:
AUTH_URL
specifying the URL of the auth serviceAUTH_CLIENT_ID
specifying the OAuth2 client id
Since this is an SPA, authentication is assumed to be handled elsewhere (e.g. a server/lambda function). For an OAuth2 flow, you'll need a server that has the client secret and can exchange the access code to an access token, upon being redirected back from the authentication service.
Translations are handled via the elm-i18next package. Translations are either dynamically fetched at app initialization and language change, or hardcoded into the application.
The current selected language is extracted first from a i18next
cookie, and secondly from the browser language, falling back to a default of en-US
. This is passed into Elm via init flags.
We assume that the translations are generated by locize, so we make our test structure in assets
conform to this. A brief overview of the important resources:
- Fetching namespace resource (translations):
https://api.locize.io/{projectId}/{version}/{language}/{namespace}
({version}
can belastest
) - Fetching available languages:
https://api.locize.io/languages/{projectId}
NOTE: We don't supported namespaces, since we don't want to dynamically build up necessary translations to fetch. It is assume that everything lives in the shared
namespace (can be changes in src/Application/I18n/Locize.elm
).
We'll need a project id though, which we'll pass to Elm via init flags, along with a translation endpoint, to have more control over that. The environment variables are:
I18N_URL
pointing to your endpoint for translations (e.g.https://api.locize.io
or/assets
for development)I18N_PROJECT
with the locize project id (we usei18n
for development, so that the path fits)
We use Elm GraphQL for making type-safe GraphQL calls in Elm. This works by generating all the code necessary for Elm, by introspecting your schema.
Generate the Elm code from your schema via:
$ GITHUB_TOKEN=xxxxxxxxxxxxxxxxxx npm run codegen:api
And the code will be put at src/Api/GitHub/
.
If you haven't set up a GITHUB_TOKEN
yet, you can easily do that, by following Authenticating with the GITHUB_TOKEN.
We use the material-components-web-elm, which integrates quite smoothly with Elm, and is actively developed. To figure out how to use the components, please refer to the library documentation.
Theming is supported via theme.scss
, which generates a theme file that overwrites the default colors of the Material Design CSS file we pull in, in assets/css
. You can build the sass with npm run build:theme
(it's a part of the build step also).
We use elm-test to run our unit tests, in the tests/
folder.
Run npx elm-test
to run the tests-
We use elm-verify-examples to ensure that code in documentation is kept up-to-date. This is similar to tools liek doc-tests in other languages.
A brief example, that'll be validated.
1+1 --> 2
Running npm run test:verify
(or npx elm-verify-examples --run-tests
) will both generate and validate these docs. Make sure you have added the fils to check in tests/elm-verify-examples.json
.
We use Github actions to build the project, run tests, and run doc tests in the CI pipeline. Check out the workflows in .github/workflows/
to see the specific workflows that are set up.
- Intro to Elm
- Elm Cheat Sheet
- Real World Elm SPA Example
- 5 Common JSON Decoders
- Beginning JSON Decoding
- Elm GraphQL (Motivation / Introduction and Book)
Tools:
- Handy JSON Decoder Generator (fine for the initial decoding)
- Managing elm.json
Testing:
Some base library functions that one should be familiar with: