Elm community is small, so we want to get as much feedback from exsisting community on how they do it, maybe motivate people to write simmilar blog posts to this one. Second thing is that maybe we can interest more people in elm. Our main focus right now is to find the best way to write elm apps and considering size of elm community discussion is limmited we couldn’t find much about that. So in the first part of the post we go over some basic structure of elm, while in the second we constrast two types of architecture we tried so far. This the first post in series where we are going through the way we use elm. I’ll share some links which will help you to get most out of this post.
https://guide.elm-lang.org/ https://www.slant.co/versus/111/380/~javascript_vs_elm https://medium.com/@_safhac_/how-we-failed-with-angular-elm-is-the-solution-c414f1794d41
File structure
+-- Main.elm
+-- Api.elm
+-- Forms.elm
+-- Buttons.elm
+-- Helper.elm
+-- Page.elm
+-- Route.elm
+-- Session.elm
+-- Settings.elm
+-- Page
| +-- Login.elm
Main
main : Program Value Model Msg
main =
Api.application Json.jsonDecUserStatusResponse
{ init = init
, onUrlChange = ChangedUrl
, onUrlRequest = ClickedLink
, subscriptions = subscriptions
, update = update
, view = view
}
Every Elm app needs to define a main value. That is what we are going to show on screen. The following functions help you define main. As you can see main needs 6 functions to define everything thats going on.
- init - sets the initial state of our Model. We will talk more in depth about Model so don’t worry.
- and 3. - define the behaviour to handle routing in our app, we won’t go in depth with this in this post.
- subscriptions -
- update - interface for manipulating with Model through Msg type, soon more.
- view - self evident, render our view.
Model
type Model =
{
bool : Bool
}
init : ( Model, Cmd Msg )
init =
( { bool = False
}
, Cmd.none
)
Model is dataset currently used in our application. In main defined above we have init function which assigns default Model state of our app.
Msg
changeBoolFunc : Model -> Bool -> Model
changeBoolFunc model newbool =
{ model | bool = newbool }
type Msg =
ChangeBool Bool
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
ChangeBool newbool ->
(changeBool model newbool, Cmd.none)
Msg is the interface for manipulating with Model. In code above what would your first thought be is that we define changeBoolFunc which changes model and that would be sufficient. But elm forces us to define Msg sumtype which then goes through update function which we’ve seen in main defined above. Update does model manipulation based on Msg recived. At first I thought this is unnecesary layer, but later I relaised that Model and Msg the way they are made my code so much better and more organized by default.
Why elm?
I was looking for a type safe alternative to JavaScript and JavaScript frameworks. There is always TypeScript to consider, but it’s still a layer over JavaScript and an attempt to fix JavaScript without changing too much. On the other hand, Elm is designed from the beginning as a typesafe, functional language which TypeScript tries to be by layering on. After an initial project written in Elm I discovered that while the boilerplate and the amount of code written is bigger than I would usually write in some other technologies, I had a really easy time debugging and fixing issues. A strict type system, immutable variables and the View -> Msg -> Model structure made debugging pretty uniform and all the bugs were easy to trace.
Msg, Model
`
import Page.Login as Login
-- Login.Msg
type Msg
= NoOp
-- Login.Model
type alias Model
= { fieldErrors : List String }
type Msg
= Ignored
| ChangedRoute (Maybe Route)
| ChangedUrl Url
| ClickedLink Browser.UrlRequest
| GotLoginMsg Login.Msg
| GotSession Session
type Model
= Redirect Session
| Login Login.Model
| UserManagment UserManagment.Model
In the code above we defined our Model with constructors for every page present in our app and the same for our Msg. Model represents our data and it’s the current state of the app, while Msg provides an interface for manipulating the data. This is the top most definition of Msg and Model types. Now we need to choose a Model of the current page and initialize it, so we can only worry about that part of app without worrying about the high level definition of all the page models.
updateWith : (subModel -> Model) -> (subMsg -> Msg) -> Model -> ( subModel, Cmd subMsg ) -> ( Model, Cmd Msg )
updateWith toModel toMsg model ( subModel, subCmd ) =
( toModel subModel
, Cmd.map toMsg subCmd
)
changeRouteTo : Maybe Route -> Model -> ( Model, Cmd Msg )
changeRouteTo maybeRoute model =
case maybeRoute of
Nothing ->
( model, Cmd.none )
Just Route.Login ->
Login.init
|> updateWith Login GotLoginMsg model
Next we need to “allow” to use page Msg while inside some page without worrying about high level definition of Msg.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model ) of
( Ignored, _ ) ->
( model, Cmd.none )
( ClickedLink urlRequest, _ ) ->
case urlRequest of
Browser.Internal url ->
( model
, Nav.pushUrl (Session.navKey (toSession model)) (Url.toString url)
)
Browser.External href ->
( model
, Nav.load href
)
( ChangedUrl url, _ ) ->
changeRouteTo (Route.fromUrl url) model
( ChangedRoute route, _ ) ->
changeRouteTo route model
( GotLoginMsg subMsg, Login login ) ->
Login.update subMsg login
|> updateWith Login GotLoginMsg model
(_, _) ->
(model, Cmd.none)
This way we can manipulate only the data and messages currently important for our page. After a page change we don’t have to worry about cleaning our model. A small problem occurs if we have some data that we want to preserve when changing the page (eg. current logged in user), but that is managed by sending parameters to the next page init function.
changeRouteTo : Maybe Route -> Model -> ( Model, Cmd Msg )
changeRouteTo maybeRoute model =
let
user =
case model of
UserManagment userModel ->
Just userModel.userData
_ ->
Nothing
in
case maybeRoute of
Nothing ->
( model, Cmd.none )
Just Route.Login ->
Login.init user
|> updateWith Login GotLoginMsg model
This code should be clear now, you initalize the model with a Maybe user parameter so you can transferthe current user data if exsits on current page whichever it is. Currently undefined part of this code is the Login.init function, so lets define that.
-- Page.Login file
type Model =
{ userData : Maybe UserData
, loginForm : { email : String, password : String }
}
init : Maybe UserData -> ( Model, Cmd Msg )
init maybeUserData =
( { userData = maybeUserData
, loginForm = { email = "", password = "" }
}
, Cmd.none
)
Now we are on the single page level where we define a model for that particular page and the init function which constructs the initial state of the app on that page. As we said previously if we want to keep any data from previous pages we can parameterize the init function with that data. Next we need to define the page messages.
-- Page.Login file
type Msg =
NoOp
| UpdateEmail
| UpdatePassword
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
UpdateEmail txt ->
({ model | loginForm = { email = txt, password = model.loginForm.password } }, Cmd.none)
UpdatePassword txt ->
({ model | loginForm = { email = model.loginForm.password, password = txt } }, Cmd.none)
Now that we briefly went over the high level architecture I would like to talk about why I think it is a good choice. Formerly, instead of a sum type of page models we had one record that represented the whole model, something like this:
type alias Model =
{
loginModel : Login.Model
}
An advantage of this approach is that we can have global model state which doesn’t reset on page change, but I think there are many flaws to this approach. For example you are manipulating huge data in which most of it is not important for current page and scope. Second thing is that you need to explicitly manipulate state and reset page models on page change, which is pretty elegantly done in the sum type approach. Also I think that the sum type approach deals with the global model state problem elegantly enough, so I can’t find enough reason to go with the old approach.
Next we will talk about the structure of one page module. Main high level logic we now understand, now we will examine how single pages are defined. Things every page needs to have defined are: model, init model function, msg type, update, view and helper functions if needed. We already went through model, init, msg and update aspects, so lets jump into view.
view : Model -> { title : String, content : Html Msg }
view model =
{ title = "Login"
, content =
div [ class "login-container" ]
[ viewForm model model.loginForm
]
}
The view function is nothing complicated, you need to define the title and the content of the page, pretty simple. Then we wrap the page view into the global view.
import Page.Login as Login
-- Main.elm
view : Model -> Document Msg
view model =
let
viewPage page toMsg config =
let
route =
case model of
UserManagment _ ->
Route.UserManagment { id = Nothing }
_ ->
Route.Login
{ title, body } =
Page.view (Session.viewer (toSession model)) page (getRouteFromModel model) config
in
{ title = title
, body = List.map (Html.map toMsg) body
}
in
case model of
Redirect _ ->
{ title = "Title"
, body = []
}
Login login ->
viewPage Page.Login GotLoginMsg (Login.view login)
-- Page.elm
type alias PageView =
{ title : String , content : Html msg }
Page.view : PageView -> Document msg
Page.view pageView =
{ title = pageView.title
, body =
[ pageView.content ]
}
The view function in the Main module serves as a wrapper that wraps the page’s Html Msg type into the global Msg type, so that you can use local page messages without worrying about wrapping them inside the local view function. Page.view function is a global wrapper which constructs the Document msg needed in App.init.
Ok, now we have some understanding of the basic structure. Next problem we need to solve is how to make reusable components that we can easily integrate into pages. For example forms are a part of the application which is used in many pages, of course we don’t want to define their model and messages for every page. So we wanted something like this.`
import Forms as Forms
-- Page.Login
type alias Model =
{ formsModel_ : Forms.FormsModel
}
init : ( Model, Cmd Msg )
init =
( { formsModel_ = Forms.initialFormsModel_
}
, Cmd.none
)
type Msg =
NoOp
| FormsOperations Forms.FormsOperation
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
FormsOperations op ->
Forms.initFormOperations_ update (\a -> FormsOperations a) model op
What are we doing here? We want to include the forms model and messages which are uniform for every page this package is included in. It would be great if there was a more elegant way to do this, for example a record in every page that describes all the packages we intend to use and the rest can be done automatically. We haven’t yet figured out a solution for that so every idea and contribution is welcome. Now we need to construct the forms package in a way that it knows how to manipulate its own model inside a bigger page model.
-- Forms.elm
import Update.Extra as UE
type alias FormsModel =
{ fieldErrors : List String
}
initialFormsModel_ =
{ fieldErrors = []
}
type FormsOperation =
MaybeAddError String
type alias Update a msg =
msg -> ModelWihForms a -> ( ModelWihForms a, Cmd msg )
-- Page model where forms package is included ex
-- (type alias Login.Model = {
-- formsModel_ : FormsModel
-- })
type alias ModelWihForms a =
{ a | formsModel_ : FormsModel }
updateFormsSubmodel_ : ModelWihForms a -> FormsModel -> ModelWihForms a
updateFormsSubmodel_ model newModel =
{ model | formsModel_ = newModel }
initFormOperations_ : Update a msg -> (FormsOperation -> msg) -> ModelWihForms a -> FormsOperation -> ( ModelWihForms a, Cmd msg )
initFormOperations_ update wrapper model operation =
let
formsModel =
model.formsModel_
in
case operation of
MaybeAddError fieldData ->
(if formsModel.isFormSubmittedFirstTime then
( updateFormsSubmodel_ model (maybeAddError formsModel fieldData), Cmd.none )
else
( model, Cmd.none )
)
|> UE.andThen update (wrapper (RemoveServerError fieldData.fieldName))
RemoveServerError error ->
( updateFormsSubmodel_ model { formsModel | serverErrors = List.filter (\b -> not (List.member error b)) formsModel.serverErrors }, Cmd.none )
Here is the basic structure of one package model. We need to define the model and messages like in any page, then we need to allow this package to interact with the data in the bigger model where the package is included. Anonymous records come in handy here. initFormOperations_ represents the update function in the package. The first argument is the update function from the page model which gives us the possibility to use Update.Extra helpers(andThen, sequence…). Then we have the second argument which serves as a wrapper from FormsOperation to the page’s Msg. The third argument is a model with a formsModel_ field, aka the page model with the forms package included like shown above. The last argument is the FormsOperation sum type which represents actions inside that form package. I think this structure can be the same for any package and this way we can cover many if not all problems regarding the global functionality need throughout our app. Another great thing about this approach is that you get something like global messages. Something like this:
import Forms
formLayout : (Forms.FormsOperation -> msg) -> Html msg
formLayout wrapper =
form [] [
input [ onInput (\val -> wrapper (MaybeAddError val)) ] []
]
If you provide a function that knows how to transform the package action into msg (Forms.FormsOperation -> msg) you can use that as some kind of a global messsage.
API communication
Regarding the backend API communication I think there is no need to go in depth because we are back to basics. We are basically using http as simple as possible. Although I would like to discuss why we returned to basics. Before we had some underlying structure that resulted in API calls looking something like this:
ApiMsg
(GetAllUnansweredCalls (SendingToServer ())
{ success =
\n ->
[ -- success msgs
]
, error =
\e ->
[ -- error msgs ]
, processing =
[ -- while processing msgs ]
}
)
This is pretty flexible because you can use it in the same form in layout and as extended functionality on other messages. I think the biggest advantage is that you can make an API call and define an event based on the response directly in the layout, but I don’t think that’s the place to do it. In one project some parts of the layout code got polluted by API calls, plus debugging got harder than it should be in Elm. Another thing is that this uses Update.Extra sequence and you need to take that into account too. All that said I don’t see a reason to go with this approach instead of a simpler one with less things in the background and more boilerplate for every call. The main advantage of Elm for me is that we trade more boilerplate for much easier debugging and problem solving, which goes hand in hand with a much simpler and transparent solution in most cases.
We are always looking for feedback and new ideas. Every project we try something new and sometimes it works, sometimes it doesn’t so every bit of help and experience shared would help a lot.