DEV Community

leojpod
leojpod

Posted on • Originally published at Medium on

2

A few more steps with Elm: Mdl, architecture and "local" update

Picking up where I left off last time, I'll share my experience on moving from an ugly web-page to an elm-mdl based one and how to split the code and the update method (which doesn't seem that popular though)

Starting to play with elm-mdl

Although the module elm-mdl proved itself rather nice to use, setting it up wasn't as easy. I followed this post to set up my app. The first step was to create a "property" on my main model to plug-in the mdl model. This is where elm-mdl is gonna work its magic and keep up with the state of things across the app. elm-mdl also provide us with an "initial" state for its module.

alias Model =
{ ... -- your usual model definition goes here
, mdl :
Material.Model
-- use elm-mdl's definition of their model
}
model : Model
model =
{ ... -- your initialization goes here
, mdl =
Material.model
-- use elm-mdl's initial setup of the model
}
view raw model.elm hosted with ❤ by GitHub

Next thing that we need to do to get ready to play with elm-mdl is to "tag" elm-mdl's messages so that anything coming up for an elm-mdl function in your code will be redirected to elm-mdl for handling, be transformed into whatever you asked for and be returned to you in a better message. We will do that by adding this:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
... -- rest of our apps messages
Mdl mdlMsg ->
Material.update Mdl mdlMsg model
view raw state.elm hosted with ❤ by GitHub

You'll probably have to follow the same principle for the subscription if you start playing with Layout and things like that but elm-mdl explain it rather well and the previously mention post as well.

Then of course you need to remember to put these lines in your html file (and no I definitely did NOT scratched my head for a moment before I remember about it…)

<!DOCTYPE HTML>
<!-- MDL -->
<link href='https://fonts.googleapis.com/css?family=Roboto:400,300,500|Roboto+Mono|Roboto+Condensed:400,700&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.min.css" />
<body>
<div></div>
<script src="elm.js"></script>
<script>Elm.App.embed(document.querySelector("div"));</script>
</body>
view raw index.html hosted with ❤ by GitHub

Talking about the index.html file. I couldn't find a nice way to use a custom html file with elm-reactor, so instead I turned to elm-live for that. Bottom line: install it with npm or yarn and then use this line to start your app:

elm-live "your-file" --output=elm.js --open
Enter fullscreen mode Exit fullscreen mode

and that's it! Elm-live will open your browser and reload as you make your changes! Ain't it cool?

Splitting the app into "modules"

One thing bothered me with the code as it was: having all the code in a bunch of big files, no matter how clear they were, was still not "clean code" for me. So I went out and tried to find guidelines on how to split the code and found out this link. Well I had to pause a bit after reading it actually. Splitting the original code into different files to separate the responsibilities was rather easy. I got 4 files:

  • Types.elm: host the types used in our application (mainly Model and Msg)
  • State.elm: host the state of our application. i.e. how to initialize the application, how to update it
  • View.elm: host the HTML of our application
  • App.elm: ties it all together!

Now, that was enough for my small game but still it would have been cleaner to extract each part of the application in a separate module. So for the sack of the exercise (and for the peace of my mind…) I split up the application on 2 parts: the setup and the actual game. I will not go into the detail of the implementation of each module: it is more or less copied and paste from the previous implementation however linking the pieces together was rather interesting and we'll check that out here.

First let's look at the "main" type file:

import Setup.Types exposing (Setup, SetupMsg(..))
import Board.Types exposing (Board, BoardMsg)
-- MODEL
type alias Model =
{ board : Board, setup : Setup, mdl : Material.Model }
-- MESSAGE
type Msg
= BoardMsg BoardMsg
| SetupMsg SetupMsg
| Mdl (Material.Msg Msg)
view raw Types.elm hosted with ❤ by GitHub

What we see in here is more or less a natural definition of our model: a record that host the model definition of each of its submodule. Each module in turn is responsible for hosting whichever properties they need in their model. But the most interesting part (IMO at least) lies in the State.elm file where we will define the app's initial state and update function.

init : ( Model, Cmd Msg )
init =
let
( subModuleInitModel, subModuleInitCommand ) =
SubModule.State.init
-- repeat for any submodule you have
model = Model subModuleInitModel ... -- init your model here
cmd = Cmd.batch [ subModuleInitCommand, ... ] -- init your commands here
in
( model, cmd )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
BoardMsg boardMsg ->
Board.State.update boardMsg model.board
|> (\( newBoard, _ ) -> ( { model | board = newBoard }, Cmd.none ))
SetupMsg setupMsg ->
Setup.State.update setupMsg model.setup
|> (\( newSetup, cmd ) -> ( { model | setup = newSetup }, Cmd.none ))
SomeOtherModuleMsg theActualMsg ->
SomeOtherModule.State.update theActualMsg model.someOtherModule
-- here we just feed the submodule's update function our app's "instance" of the model
|> (\( newSubModuleModel, someAssociatedCommand) ->
( { model | someOtherModule = newSubModuleModel}, someAssociatedCommand)
Mdl mdlMsg ->
Material.update Mdl mdlMsg model
view raw State.elm hosted with ❤ by GitHub

As you can see on the init, this code is rather simple yet verbose. For each direct child-module, our current modul need to pull the init state and add it to it's own init. This could be automatically written though, I hope to have some time to dig in Atomist to try making that happen. Then the update method is just a dispatcher that forward each message to its rightful submodule. It makes it rather easy to communicate between each submodule as they just need to trigger the right message to send something to another module: in this app for instance, the setup send message to let the board module now when it should be ticking or not. The counter point to that is that it requires our view to send Msg (i.e. the main message type).

The same observation can be seen on the Cmd part of our update method in the submodule. One could be tempted to "tag" the command like we do for the elm-mdl messages but then it would prevent or at least complicate the process of sending command to other modules.

Anyhow, enough of that. Have a look at the code if you've questions, suggestions I'll be happy to talk :)

A quick stop via SVG!

Before closing this post, let's have a look at how manipulating SVG is seamless in Elm. For those who checked the code of my first post about elm you've seen something like this:

the ugly state in which I left things last time.

The view code that was responsible for that horror looking table was this:

boardDisplay : Board -> Html BoardMsg
boardDisplay board =
table []
((tr []
((td [] [])
:: ((List.range 0 9)
|> List.map (\col -> td [] [ text (toString col) ])
)
)
)
:: (List.indexedMap rowView board)
)
rowView : Int -> List Cell -> Html BoardMsg
rowView idx row =
tr [] ((td [] [ text (toString idx) ]) :: (List.map cellView row))
cellView : Cell -> Html BoardMsg
cellView cell =
case cell of
Empty ->
td [] [ text "." ]
Dead ->
td [] [ text "-" ]
Alive ->
td [] [ text "X" ]
view raw View.elm hosted with ❤ by GitHub

Switching to SVG was seamless: in Elm SVG nodes are manipulated exactly like HTML ones! (provided you remember to elm package elm-lang/svg).

import Svg exposing (Svg, svg, rect, circle)
import Svg.Attributes exposing (x, cx, y, cy, r, fill, width, height, viewBox)
boardDisplay : Board -> Html BoardMsg
boardDisplay board =
svg
[ viewBox "0 0 100 100"
]
(rect [ fill "#FFFFFF", x "0", y "0", width "100", height "100" ] []
:: List.concat (List.indexedMap rowView board)
)
rowView : Int -> List Cell -> List (Svg BoardMsg)
rowView idx row =
List.concat (List.indexedMap (cellView idx) row)
cellView : Int -> Int -> Cell -> List (Svg BoardMsg)
cellView rowIdx colIdx cell =
rect [ x (toString (colIdx * 10)), y (toString (rowIdx * 10)), width "10", height "10", fill "#FFFFFF" ] []
:: (case cell of
Empty ->
[]
Dead ->
[ circle [ cx (toString (colIdx * 10 + 5)), cy (toString (rowIdx * 10 + 5)), r "5", fill "rgb(0, 121, 107)" ] [] ]
Alive ->
[ circle [ cx (toString (colIdx * 10 + 5)), cy (toString (rowIdx * 10 + 5)), r "5", fill "rgb(0, 150, 136)" ] [] ]
)
view raw View.elm hosted with ❤ by GitHub

And we're done!

If you want to have a look at the app's current state: check this out

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs