Elm: Web Development without Html, CSS or JS

Part 1: Getting Things Up and Running

Introduction

Webdevelopment is cumbersome. Different and fast changing technologies, unclear standards that not every player respects, questionable technical decisions for very import parts of the ecosystem probably are the reason for this.

Elm tries to go a different route. As a compiled language it is free to leave the mistakes of the past behind. In combination with the library elm-ui one can achieve complete independence from Html, CSS and Javascript.

But Elm is also something new and very unfamiliar and thus the barrier to entry is quite high. This tutorial will help you understand how Elm works and how you can leverage it to build fast webapps that are reliable and maintainable.

We have been using Elm in production for about two years now and are very happy with it. Some projects would not have been possible at their respective budget without it.


This is a multipart tutorial that will show you how to set up Elm, understaand it’s architecture and learn it’s strong and weak sides.

Part 1: Getting things up and running

This part covers:

You can follow the code on GitHub.

First things first:

Install Elm

For windows and mac please use the respective installers.

You can install elm on other systems through npm:

npm install -g elm

Check if Elm is correctly installed by running elm --version.

0.19.0

Lets create an empty Elm project:

cd ~/Documents
mkdir -p elm-tutorial/part1
cd elm-tutorial/part1
elm init

This will create an elm.json file and a src directory. For now thats all we need to know, later we will have a closer look at the elm.json.

If you do not want to install Elm as of now, you can also use Ellie an Elm online editor.

Setting up an editor

For the sake of this tutorial we will only explain how to setup VS Code with Elm. In case you want to use a different editor check out the list of available plugins in the official Elm guide.

If you haven’t already installed VS Code check out how to do it here.

In VS Code, use the open folder menu to navigate to the elm-tutorial/part1 folder we created in the previous section. Navigate to the extensions menu and install the elm extension. At the time of writing it has version 0.25.0.

How to use the compiler

Let’s find out how to use the compiler. Add a Main.elm file in the src directory and fill it with:

module Main exposing (main)

import Browser
import Html exposing (h1, text)


main =
    Browser.sandbox
        { init = ()
        , update = \_ _ -> ()
        , view = \_ -> h1 [] [ text "Hello World!" ]
        }

No need to understand anything at the moment. This is just to a minimal example to be able to show you what you can do with the compiler.

You can now invoke the compiler to generate a HTML-file and have a look at it:

# in ~/Documents/elm-tutorial/part1
elm make src/Main.elm
firefox index.html

Tada! Hello world!

There are other ways to use the compiler but we will touch that later.


Functional Programming: Basics

Let’s extend our Main.elm to be a bit more meaningful and understandable:

module Main exposing (main)

import Browser
import Html exposing (Html, h1, text)
import Html.Events exposing (onClick)


init : Model
init =
    { name = "Hans"
    , age = 37
    }


type alias Model =
    { name : String
    , age : Int
    }


type Msg
    = Click


update : Msg -> Model -> Model
update msg model =
    case msg of
        Click ->
            { model | age = model.age + 1 }


view : Model -> Html Msg
view model =
    h1 [ onClick Click ]
        [ text
            ("Hello "
                ++ model.name
                ++ ". You seem to be "
                ++ String.fromInt model.age
                ++ " years old."
            )
        ]

main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

If you have never seen functional code, this probably seems alien to you. So let’s go through it line by line.

Modules

module Main exposing (main)

This tells the compiler, that the module in this file is called Main and that we only expose one function to the outside world (main). The name of the module and the filename have to be identical.

If any other module wanted to use other functions or types from our module, we would have to list them in the exposing brakets. E.g.:

module Main exposing (Model, main, init)

You can also tell the compiler to expose everything with the following:

module Main exposing (..)

Imports

Imports work in a similar manner. When a module is imported all it exposes can be accessed through the module name. In our example:

import Browser

-- later

Browser.sandbox

You can also directly import functions or types with the exposing syntax. Here we do so with h1 and text.

Also it is possible to alias modules. For example we could import Browser as B like that:

import Browser as B

-- later

B.sandbox

Functions

If you are not familiar with functional programming the function syntax is probably gonne be the most confusing at the beginning.

A function has two parts:

  1. The declaration: init: Model
  2. The definition: init = { name = "Hans", age = 37 }

The declaration just states the types and is completely optional.

Examples

five: Int
five = 5

The declaration states, that the return type is an Int. And we define the return value of five to be 5.

double: Int -> Int
double x =
  x + x

Lot’s of new syntax here. Int -> Int says that when we give the function an Int it gives us back an Int.

In the definition we give a name to our first parameter: x. We add x and x. The result will automatically be returned.

Here is a typescript implementation as a comparison:

function double(x: number): number {
    return x + x;
}

With more parameters functions look like this:

add: Int -> Int -> Int
add x y =
  x + y

Init

As we now know, init takes no arguments and returns a value of type Model. Model is constructed by record syntax, which is similar to the way to declare record type.


Types

There are three different kinds of types in Elm: custom types, record types and tuples. We will deal with custom types and tuples later and will for now only explain record types.

Record types, like Model, have named fields which can be trivially accessed and updated. You can update fields by using the update syntax:

{ record | field = value }

Check out the update function. Here we increment model.age by one with the following statement:

{ model | age = model.age + 1 }

And this also shows you how to access record fields: Simply use a dot: model.age as in

h1 [] [ text ("Hello " ++ model.name ++ ". You seem to be " ++ toString model.age ++ " years old.") ]

Updating the Model

In Elm all state lies in one model, irrespective of the complexity of the app. For our case this is just a simple record with a name and an age. We will deal with the problem of how to handle larger and more complex models in a later part of this tutorial.

For now, it is important to realize another fact: the model can only be changed in one place: the update function.

In our case, we want to increment the age value when Click is triggered:

Click -> { model | age = model.age + 1 }

Please ignore possibly confusing syntax for now.

Emmitting Html

Even if we won’t write Html ourselves, of course the basis of every webapp is Html. Elm takes a different approach than most other frameworks or tools in the area.

Instead of templating, i.e. having Html with interspersed code, in Elm Html is build with functions.

The h1 in our view is just a function. It takes as argument a list of attributes and a list of child elements and returns value of type Html msg.

The runtime then makes sure that the Html we built with functions actually get inserted in the DOM.

Wiring up the runtime

The Elm runtime needs to know a few things about our application to wire it up correctly. You can find all ways to wire up your application in the Browser-package.

Here we use the sandbox function for the most simple of wiring up.

The sandbox expects three arguments: init,view and update. We already discussed theses functions and can now just wire them up:

Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Don’t get confused: the init, update and view on the right-handside are the functions we just implemented and discussed.

Ways to get the app running

We already learned how to use elm make. Now lets check out a different way to get the app running. Navigate to the project directory and execute

elm reactor
Go to <http://localhost:8000> to see your project dashboard.

Now navigate to http://localhost:8000/src/Main.elm in your browser. You should see this:

Elm Tutorial Part1: Hello World!

You can click on the text and, as expected, the age will increment.


Community Packages

Elm, like most modern languages shippes with a package-manager. Direct and indirect dependencies are listed in the elm.json we generated right at the beginning of this tutorial.

You can use the package-catalog to search for packages and read their documentation.

We want to teach you how to write web-apps without HTML, CSS and JS. But in the first example, even though we do not write Html directly, we still use h1 which is clearly Html. To get rid of Html completly we a need different way to generate Html than the previously used elm/html package.

We will use mdgriffith/elm-ui instead.

To add it to elm.json run

elm install mdgriffith/elm-ui

Now we can use everything elm-ui provides.

elm-ui

elm-ui liberates you from using Html and Css. It operates on a higher level of abstraction. Our code until now can be easily rewritten to use elm-ui:

module Main exposing (main)

import Browser
import Html exposing (Html)
import Element exposing (el, text, paddingXY)
import Element.Events exposing (onClick)
import Element.Font as Font
import Element.Region as Region

init : Model
init =
    { name = "Hans"
    , age = 37
    }


type alias Model =
    { name : String
    , age : Int
    }


type Msg
    = Click


update : Msg -> Model -> Model
update msg model =
    case msg of
        Click ->
            { model | age = model.age + 1 }


view : Model -> Html Msg
view model =
    Element.layout []
        (el
            [ Region.heading 1
            , Font.size 32
            , Font.bold
            , paddingXY 8 20
            , onClick Click
            ]
            (text
                ("Hello "
                    ++ model.name
                    ++ ". You seem to be "
                    ++ String.fromInt model.age
                    ++ " years old."
                )
            )
        )


main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

As you probably expected, only the view function changes. Element.layout is a function to transform elm-ui-Views back to Html.

Otherwise we are doing nothing fancy here.

Start the reactor and visit localhost:8000/src/Main.elm. The result is stunningly identical.

Showing off what elm-ui can do

Just as a little tidbit at the end of this tutorial we will show off a little something elm-ui is great at. Putting things where you want them to be. Vertical and horizontal centering is far from trivial in CSS. With elm-ui it’s just a centerX and centerY

Change view to

view : Model -> Html Msg
view model =
    Element.layout []
        (el
            [ Element.centerX
            , Element.centerY
            , Region.heading 1
            , Font.size 36
            , Font.bold
            , onClick Click
            ]
            (text
                ("Hello "
                    ++ model.name
                    ++ ". You seem to be "
                    ++ String.fromInt model.age
                    ++ " years old."
                )
            )
        )

Future Parts

In Part 2 we will cover basic concepts of elm-ui, custom types and how to embed your elm app in an existing app. Follow us on twitter to get updates.

Exercise

While waiting on part 2 we have a little exercise for you:

Add a second line which says ‘No, i’m not!’. When clicking on this the age should decrement.

Hint: you’ll need Element.column and you will need to extend the Msg-type.

A solution will be posted on Monday (June 3rd) on our twitter.


Interested in working with us?

CurrySoftware is now offering Elm consulting services and we would love to support you on your path using Elm! Please contact us if you are interested!