The Elm Adventure: Part 1 - Hero Editor

This post is the second in a series of posts that will show you how to create a clone of AngularJS’s Tour of Heroes app using Elm.

In the first post we created a basic “Hello, World” app, in this post we will expand on the initial code and implement a simple hero editor.

At the end of this post, this is what we will have built.

You can find the source for this post here.


Before we get started building our simple hero editor, let’s start our webpack-dev-server so that we can view the changes we make as we go along. From the command-line, in the folder where we have our Elm code, run the following command:

yarn client

The Elm architecture

In the first post we just put all our code in the Main.elm file, for the first iteration of our app this was fine as all we were doing was printing Hello World to the browser. But as the app keeps growing this will become un-maintainable so we need to organise our code in a better way and this is where the Elm architecture comes into play.

This is how the Elm site explains the Elm architecture:

The Elm Architecture is a simple pattern for architecting web apps. It is great for modularity, code reuse, and testing. Ultimately, it makes it easy to create complex web apps that stay healthy as you refactor and add features.

As you can see in Main.elm, the logic of an Elm app can be split up into 3 distinct parts:

  • Model — which holds the state of our application
  • Update — which handles updates to our state
  • View — which renders our state as HTML in the browser

Implementing the Elm architecture

Let’s start by creating a new folder, named App, where we’ll put the logical parts — Model, Update and View — of our application.

In this folder, create 4 new files:

  • Messages.elm — which will specify the different messages that can be used in our application
  • Models.elm — which will specify the different models that can be used in our application
  • Update.elm — which will specify the logic for updating the state in our application
  • View.elm — which will specify the views that we have in our application

Open Messages.elm in your editor and add the following module declaration at the top module App.Messages exposing (..).

Then copy the Msg type from Main.elm and paste it into Messages.elm which should now look like this:

module App.Messages exposing (..)
type Msg
= NoOp

At this point you will have be greeted with some errors that the type Msg cannot be found in webpack that we kicked off at the start of this post. To fix this we need to update Main.elm to import our new module, add the following import App.Messages exposing (Msg(..)), save the file and the errors should go away.

Now do the same for Models.elm, Update.elm and View.elm. The module declaration taking the same convention as in Messages, module App.<FileName> exposing (..).

With the refactoring completed our app should look like the following:

Messages.elm

module App.Messages exposing (..)
type Msg
= NoOp

Models.elm

module App.Models exposing (..)
type alias Model =
String

Update.elm

module App.Update exposing (..)
import App.Messages exposing (Msg(..))
import App.Models exposing (Model)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )

View.elm

-- View.elm
module App.View exposing (..)
import Html exposing (Html, div, text)
import App.Messages exposing (Msg)
import App.Models exposing (Model)
view : Model -> Html Msg
view model =
div []
[ text model ]

Main.elm

module Main exposing (..)
import Html exposing (Html, program, div, text)
import App.Messages exposing (Msg(..))
import App.Models exposing (Model)
import App.Update exposing (update)
import App.View exposing (view)
init : ( Model, Cmd Msg )
init =
( "Hello World, from Elm!", Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
main : Program Never Model Msg
main =
program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}

And, if everything has gone the way it should, we should have no errors in webpack and our Hello World web app should still be running successfully and displaying Hello World, from Elm!


Building the hero editor

The first thing we need to do is to update our model, the model will hold the app title and our hero. So let’s modify the Model type in Models.elm.

type alias Model =
{ title: String
, hero: Hero
}

Here we also introduce a new type for our Hero which consists of an id and a name:

type alias Hero =
{ id: Int
, name: String
}

We also add an initial model, instead of hard coding it in our init function.

initialModel : Model
initialModel =
{ title = "Tour of Heroes"
, hero =
{ id = 1
, name = "Windstorm"
}
}

Now we need to update our init function in Main.elm to accept our initialModel. First we include it in the import statement from our App.Models module:

import App.Models exposing (Model, initialModel)

And then we update the init function to pass the initialModel:

init : ( Model, Cmd Msg )
init =
( initialModel, Cmd.none )

At this point, we will have an error reported by the Elm compiler about a mismatch in the argument we are passing to the text function in our view.

Module build failed: Error: Compiler process exited with error Compilation failed
-- TYPE MISMATCH --------- ./src/elm-aspnet-core-tour-of-heroes-app/App/View.elm
The argument to function `text` is causing a mismatch.
17|               text model
^^^^^
Function `text` is expecting the argument to be:
String
But it is:
Model
Detected errors in 1 module.

Let’s update our view so that we display the app title and a simple form showing the hero’s name:

view : Model -> Html Msg
view model =
div []
[ h1 [] [ text model.title ]
, h2 [] [ text model.hero.name, text " details!" ]
, div []
[ label [] [ text "id: " ]
, text (toString model.hero.id) ]
, div []
[ label [] [ text "name: " ]
, input
[ value model.hero.name
, onInput ChangeName
]
[]
]
]

And we need to update our imports so that we can use the new functions for building our HTML:

import Html exposing (..)
import Html.Attributes exposing (value)
import Html.Events exposing (onInput)
import App.Messages exposing (Msg(..))
import App.Models exposing (Model)

To respond to updates of the hero name, we’ve included a new concept here, events, here we’ve added the onInput event to our textbox. When the name is updated the onInput event will send off a ChangeName message that will be picked up by our update function.

The ChangeName message takes a String, the value of the textbox, so we need to add this to our Msg type as follows:

type Msg
= ChangeName String
| NoOp

And we need to change our update function to respond to our new ChangeName message, when it receives the message it will update the name of the hero, then update the hero on our model and return the new, updated model and re-render the view.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ChangeName newName ->
let
hero = model.hero
updatedHero =
{ hero | name = newName }
updatedModel =
{ model | hero = updatedHero }
in
( updatedModel, Cmd.none )
NoOp ->
( model, Cmd.none )

Let’s make our web app look a bit better, add a new file styles.css at the root of your app folder next to your index.html file and paste in the following styles:

/* Master Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2,
h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body,
input[text],
button {
color: #888;
font-family: Cambria, Georgia;
}
a {
cursor: pointer;
cursor: hand;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
/* Navigation link styles */
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited,
a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
}
/* items class */
.items {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 24em;
}
.items li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.items li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.items li.selected {
background-color: #CFD8DC;
color: white;
}
.items li.selected:hover {
background-color: #BBD8DC;
}
.items .text {
position: relative;
top: -3px;
}
.items .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}

To include our new styles, we need to add an import to our index.js file:

import './styles.css';

To enable webpack to load css files, we need to install a couple of new dependencies for loading stylesheets, so run the following command from the command-line:

yarn add --dev style-loader css-loader

Once the loaders have finished installing, we need to add a new rule to the module.rules section of our webpack.config.babel.js file:

...the rest of webpack.config.babel.js
module: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
...the other rules we already had
...the rest of webpack.config.babel.js

For webpack to pickup the new loader settings you’ll need to restart the running webpack-dev-server. So in your command prompt, press Ctrl+C and then run yarn client again.

If everything has gone according to plan, you should now see the following in your browser:

Hopefully this is what you will see if you’ve made it this far!

And when you update the hero name, and it should update the title as well.

Updating the hero name also updates the title.

That concludes this second post where we built a simple editor for our hero.

In the next post, we’ll build a master/detail page with a list of heroes and allow a user to select a hero and display it’s details.