This document orients new contributors to the general structure of Exosphere's codebase. It is a continuous work in progress, and is not intended to be exhaustive -- the code itself is the source of truth.
If you are new to Elm as well as Exosphere, this guide pairs well with the official Elm Guide, particularly the Elm Architecture pages.
- Herein, "the application" or simply "the app" refers to the Exosphere Elm application that compiles to JavaScript and runs in the user's web browser.
Exosphere's Elm source code lives in the src/
directory. The main file is Exosphere.elm
. When you run the Elm compiler, you're pointing it there.
Go look at Exosphere.elm
, you'll see it has just one main
function. This function calls Browser.application
to start the app. It passes several arguments. The most important ones are:
- The
init
function builds the initial state of the app, i.e., it initializes the Model that is persisted in memory while the app is running. If there is an existing saved user session (in the browser's local storage),init
will decode that stored state and 'hydrate' the Model from it.init
also fires initialCmd
s (pronounced "commands"). (These make API calls (generally to an OpenStack back-end), and call out from Elm to JavaScript as needed.) You can see theinit
function insrc/State/Init.elm
. - The
view
function decides what to show to the user on the screen. It takes as input the application state (i.e. Model, specifically hereOuterModel
). It outputs HTML and CSS (along with a page title) that is rendered by the user's web browser, along with a page title. You can see the top-levelview
function insrc/View/View.elm
, though it imports many other modules that we'll visit later in the tour. - The
update
function is responsible for advancing the state of the app forward, in response to external events (like button clicks and results of API calls). It receives aMsg
(pronounced "message") from the Elm runtime, and the current state (Model) of the app. Then, it computes the new state of the app. It returns a new Model (representing the new state) and a newCmd
. ThatCmd
tells the Elm runtime to do things like make HTTP requests to external services, and call JavaScript outside the Elm app.
This uses the same basic architecture described in the Elm guide, but with more modules and helper code.
The main
function in Exosphere.elm
handles an OuterModel
defined in src/Types/OuterModel.elm
-- this model contains the entire state of the app for as long as it's running.
The OuterModel
splits up data between the SharedModel
(defined in src/Types/SharedModel.elm
) and a ViewState
(defined in src/Types/View.elm
), which often contains a page-specific model.
The SharedModel
models data that is shared between pages in the UI, i.e., data that aren't specific to whatever page is currently displayed. Examples of these are the current time, and whichever OpenStack projects the user is logged into.
The ViewState
is a custom type that stores which page the UI is currently displaying. Many of the variants of that custom type contain a page-specific model. The page-specific model stores data specific to that current page. Examples include the contents of any text boxes on the page, and whatever checkboxes are currently checked in a list.
The separation between SharedModel
and page-specific models allows each page to have its own nested architecture (Model
, Msg
, update
, and view
). This is a common pattern in larger Elm applications. It simplifies the app's outer-most data model, and reduces the need to think about plumbing for the entire app if you're only working on one page of the UI.
src/View/View.elm
contains the top-level view
function that's passed to the main function. view
handles the possibility of an invalid application state, but if it's valid, it calls a stack of functions that:
- Provide a page title (
viewValid
) - Set up a base elm-ui layout (
view_
) - Draw a page header and a main content container (
appView
) - Render a non-project-specific (
nonProjectViews
) or project-specific (projectContentView
) view, depending on the current view state.
From here, we call out from the top-level view code to individual pages. You'll see that View.elm
imports many modules starting with Page.
, and calls the view function for each of these pages.
Modules in src/Page
usually correspond to a specific page in the application. Most pages have their own Elm architecture -- each page has its own Model
and Msg
types, and init
, update
, and view
functions. Each page's architecture is nested inside of the overall app's architecture.
Further down the call stack, we have src/Style
code. This includes src/Style/Widgets
, self-contained UI widgets that can each be re-used on many pages. These widgets are generally stateless -- they don't have their own Elm architecture, just a view function that takes parameters and renders the widget. Widgets are also generally independent from the Exosphere codebase. Someday, they could live in a separate Elm package.
There are also helper functions in both src/View
and src/Style
. The distinction is that View
helpers are specific to Exosphere's codebase, while Style
helpers could be independent. You may find exceptions, but that is the intention going forward.