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 | |
} |
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 |
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> |
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
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) |
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 |
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 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" ] |
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)" ] [] ] | |
) |
And we're done!
If you want to have a look at the app's current state: check this out
Top comments (0)