You can consider all the previous article in this series as the warm up for this article. Here we’ll build a real Elm project the right way.

A close-up of a race track with an Elm logo overlaid

Note: Examples in this article use Elm 0.19.

Starting new Elm 0.19 project locally

Open your favorite editor - I’m using VS Code here.

Add new folder for your Elm project - I’ve called mine my-second-elm-app.

In VS Code, press the CTRL backtick keyboard combination to open the built-in console. Alternatively, on the main menu, click View, then Terminal.

Sometimes in VS Code, the console doesn’t show any content - just press the ENTER key and it will “wake up”.

Type the elm init command inside the console window and press ENTER. If things don’t work as expected, make sure your console is pointing to your new folder. Our VS Code before running the elm-init command

This will initialize your Elm project, and a new elm-package.json file will be added to it.

The elm init command initializes your Elm project.

Before it does, you’ll need to confirm you want to do it by pressing the y key in the console when prompted by Elm.

You'll need to type "y" to confirm

Now Elm will add the elm.json file to the root of our folder. This file holds the relative information about our project, including a list of dependencies.

It will also add an empty src folder.

After running the elm-init command

Here’s the full contents of the elm.json file:

{
    "type": "application",
    "source-directories": [
        "src"
    ],
    "elm-version": "0.19.0",
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.1",
            "elm/core": "1.0.2",
            "elm/html": "1.0.0"
        },
        "indirect": {
            "elm/json": "1.1.3",
            "elm/time": "1.0.0",
            "elm/url": "1.0.0",
            "elm/virtual-dom": "1.0.2"
        }
    },
    "test-dependencies": {
        "direct": {},
        "indirect": {}
    }
}

Now that we’ve started our Elm 0.19 project, let’s add the first file, Main.elm.

Adding Main.elm

Let’s make a new file. We’ll call it Main.elm.

We’ll begin working on our new Main.elm file by importing everything from the Html module:

import Html exposing (..)

In Elm, the (..) means “everything in the module”. The Html is obviously the module name. Pretty straightforward, right?

Alternatively, I could have just done import Html exposing (div), if the div function was the only thing that I needed.

Now let’s add a text function to main:

main =
    text "Elm is wonderful"

The text function comes from the Html module; it’s avaiable because we exposed everything.

Now let’s run the elm make command in our console. Here’s the output: After running the elm-init command

The error messages in Elm are known to be super-friendly.

Here it’s instructing us we need to add the file to run. It’s also saying that our Main.elm file should reside in the src folder.

So let’s fix this and move the Main.elm file inside the src folder, and then run elm make src/Main.elm. Running elm make with target file

We’ll see the success message, as well as the newly-compiled index.html in the file explorer sidebar in VS Code. Running elm make with target file

If you opened and inspected the contents of the index.html file, you might be surprised to see that it has over 4000 lines of code. That’s a lot of JavaScript!

The reason for this is that Elm compiled it’s entire VDOM and the Main module into JavaScript, so browsers can understand it and work with it.

Let’s now see how to compile Main.elm module into a separate JavaScript file.

First, let’s delete the index.html file.

Next, let’s run this command:

elm make src/Main.elm --output=js/app.js

This time the Main module is compiled into a separate JavaScript file, inside the js folder. Running elm make with target file

Of course, to serve it, we need an HTML file, so let’s add one ourselves, with the following contents:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My 2nd Elm App</title>
</head>
<body>
    <div id="app"></div>
    <script src="js/app.js"></script>
    <script>
      Elm.Main.init({
        node: document.getElementById("app")
      });
    </script>
  </body>
</html>

Now we can view our index.html file in the browser by right-clicking on it in the file explorer sidebar inside VS Code, then clicking the Open in Default Browser command.

We should see the “Elm is wonderful” text node in the viewport, and the “My 2nd Elm App” title in the browser tab - both these things are signs we’ve successfully compiled and served our Elm app in the browser.

Letting Elm 0.19 compile and serve our app for us

Alternatively, we can just let Elm 0.19 do everything for us; we just need to type this command inside the console:

elm reactor

We’ll get back the following message: A screenshot of elm reactor running in console of VS Code

So let’s now open localhost:8000 in our browser and we’ll see this: A screenshot of elm reactor running in the browser

Now we can simply click on index.html - it’s the file at the bottom of the File Navigation card. Clicking on it will open index.html in the browser, with the text Elm is Wonderful obligingly displaying.

Let’s now see what happens when we change the contents of our Main.elm, with elm reactor running.

Let’s just update the string we’re passing to the text function:

main = 
    text "I like Elm"

Let’s save all the changes, go back to the browser, and click on index.html again on localhost:8000.

Aaaaand…. nothing happens!

Actually, something does happen: We see the text node “Elm is wonderful” again.

The reason for this is that elm reactor was built just for very simple compilation, not for watch tasks and other shenanigans.

To overcome this minor issue, we can use webpack or bash, or maybe even a third approach. However, that’s an article in itself, so for now we’ll skip this setup.

TL; DR; if you want to see the change quickly, just recompile the app.

Alternatively, for simple apps and quick experiments, we can just use the Ellie app, like we did in the first 9 articles in this article series.

For now, it seems we’ll need to simply run the elm make command manually whenever needed:

elm make src/Main.elm --output=js/app.js

And this is perfectly fine.

Adding Bootstrap 4 to our Elm app

To add Bootstrap 4, let’s just add a link to Bootstrap 4 CDN, inside our index.html file:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My 2nd Elm App</title>
    <link 
        rel="stylesheet" 
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
    <div id="app"></div>
    <script src="js/app.js"></script>
    <script>
        Elm.Main.init({
            node: document.getElementById("app")
      });
    </script>
  </body>
</html>

Now we can run the elm make command again and open our index.html file in the browser to see the updated font styling on our one line of text.

Next, we’ll build a static HTML page, just like the one we built in the Bootstrap series of articles here.

We’ll use the online HTML to ELM converter to quickly convert the HTML, section by section.

Converting the pricing Bootstrap 4 layout from HTML to Elm

Using the online converter is pretty easy.

There’s just one thing to keep in mind: There needs to be a single wrapping element around all your HTML code for the code to be transpiled into HTML using the online HTML to Elm converter.

You can find the fully converted layout as it is on Ellie app.

Here’s the embedded preview:

Making our code more modular

Right now, the entire layout lives inside one large chunk of Elm code. Let’s split it so it is easier to work with and think about.

First, we’ll take out the navigation and put it into a separate function.

Adding the navigation function

We’ll cut the following code from main:

div [ class "d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm mb-0" ]
        [ h5 [ class "my-0 mr-md-auto font-weight-normal" ]
            [ text "Company name" ]
        , nav [ class "my-2 my-md-0 mr-md-3" ]
            [ a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Features" ]
            , a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Enterprise" ]
            , a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Support" ]
            , a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Pricing" ]
            ]
        , a [ class "btn btn-outline-secondary", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
            [ text "Sign up" ]
        ]

In its place, let’s just add a single word: navigation.

Now we’ll add the navigation function, as follows:

navigation = 
    div [ class "d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm mb-0" ]
        [ h5 [ class "my-0 mr-md-auto font-weight-normal" ]
            [ text "Company name" ]
        , nav [ class "my-2 my-md-0 mr-md-3" ]
            [ a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Features" ]
            , a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Enterprise" ]
            , a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Support" ]
            , a [ class "p-2 text-dark", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
                [ text "Pricing" ]
            ]
        , a [ class "btn btn-outline-secondary", href "https://getbootstrap.com/docs/4.3/examples/pricing/#" ]
            [ text "Sign up" ]
        ]

You can find this update in a new Ellie app.

Here’s the embedded app:

As we can see, the app looks exactly the same.

However, abstracting away sections of our layout will make it easier to reason about our app.

Next, we’ll update other sections similarly, starting with the pricing header.

Abstracting away the pricingHeader function

We’ll cut out another section of code and move it from the main function into its own, pricingHeader function:

pricingHeader = 
    div [ class "pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center bg-secondary text-light", attribute "style" "position: relative;    top: -20px;    min-height: 650px;    max-height: 350px;    background-image: url(https://images.unsplash.com/photo-1511988617509-a57c8a288659?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1951&q=80); background-position: -20px 1040px;   box-shadow: inset 0 0 0 1000px rgba(0, 121, 255, 0.4);" ]
        [ div [ class "container-narrow" ]
            [ h1 [ class "display-4" ]
                [ text "Pricing" ]
            , p [ class "lead" ]
                [ text "Quickly build an effective pricing table for your potential customers with this Bootstrap example. It’s built with default Bootstrap components and utilities with little customization." ]
            ]
        ]

And here’s the call to the pricingHeader function inside main:

main =
    div []
    [ navigation
    , pricingHeader
    , div [ class "container" ]

When abstracting away sections of code like this, the technique to follow is always the same: you just cut out the code string from one , to the next , character.

Once done, run elm make again:

elm make src/Main.elm --output=js/app.js

This way you can verify that everything runs smoothly.

Note that you don’t even have to look at the end result in the browser; most of the time it’s enough to see the successful compilation message in the console:

Success! Compiled 1 module.

Next, we’ll see how to easily abstract away the entire container section.

Abstracting away long sections of Elm code

After the navigation and pricing header, there is a section of code wrapped in a container div; this section is about 160 lines of code long. It goes almost to the end of the main function!

Let’s see how to cut it out from main and into its own function easily.

First, we’ll click next to the line number where the next , character starts: this is the delimiter that shows where our next section of code begins. A screenshot of Elm code twirled close in VS Code

What this means is that we need to copy all the code from lines 38 to 195; in other words: copy all the code from line 38 to the very end, except the closing square bracket on line 196.

Once copied, we’ll save it as a new function called wrappingContainer, and then we’ll just reference it in the main function like this:

main : Html msg
main =
    div []
    [ navigation
    , pricingHeader
    , wrappingContainer
    ]

To see this layout live, we’ve saved it in another Ellie app instance.

Like before, our layout is the same and the only thing actually changing is the code that is used to compile it.

At this point, we can choose to abstract away sections from the wrappingContainer function into its own separate building-block functions.

Just to demonstrate how that works, let’s add only one.

We’ll take the free card - the first of three pricing cards - and we’ll save it into the freeCard function:

freeCard = 
    , div [ class "card-body" ]
        [ h1 [ class "card-title pricing-card-title" ]
            [ text "$0 "
            , small [ class "text-muted" ]
                [ text "/ mo" ]
            ]
        , ul [ class "list-unstyled mt-3 mb-4" ]
            [ li []
                [ text "10 users included" ]
            , li []
                [ text "2 GB of storage" ]
            , li []
                [ text "Email support" ]
            , li []
                [ text "Help center access" ]
            ]
        , button [ class "btn btn-sm mt-5 btn-block btn-outline-secondary", type_ "button" ]
            [ text "Sign up for free" ]
        ]

This update is saved in Ellie too.

Here’s the embedded view of the Ellie app above:

In this article, we’ve worked with some static HTML in Elm 0.19 to separate our app into more manageable chunks.

This makes it a lot easier to make our app modular and easy to reuse.

In part 11 of this article series, we’ll see how to add a dynamic input in Elm 0.19.