5.4

Model View Update - Part 2

In the Model View Update - Part 1 section, we used a very simple example to learn the fundamental concepts behind the Elm Architecture. In this section, we’ll build a slightly more complex app to reinforce our understanding of the Model View Update pattern.

Building a Sign-Up Form

Most web applications today require users to create a new account, so let’s build a sign-up form in Elm. It will be a good exercise to see how the Elm Architecture holds up when our application isn’t just a simple counter. It’ll also give us an opportunity to learn how to style our app using different techniques including using an external CSS framework. Here’s how the sign-up form will look after we’re done implementing it.

When building a new app, we often don’t know where to start. One approach is to define the model first, write some view code to present that model to the user, and figure out what messages the user can send to our app. We can then use those messages to transform the current model into a new one. This is exactly the approach we took in the Model View Update - Part 1 section when we built a simple counter app. We’ll use the same approach here too.

Model

First, let’s think about what information does our app need to keep track of. To create a new account, we need the user’s name, email, and password. We also need to keep track of whether or not the user is logged in. Based on that, here is what the model looks like:

module Signup exposing (..)


type alias User =
    { name : String
    , email : String
    , password : String
    , loggedIn : Bool
    }

Create a new file called Signup.elm in the beginning-elm/elm-examples directory and add the above code to it.

Our model is a record with four properties. name, email, and password will store the information entered by the user. loggedIn will indicate whether or not the user has been authenticated to use our app. It’s quite common to use a record for defining a model in Elm as we’ve done here.

We called our model User. Elm doesn’t require us to use the word Model, so we can give it whatever name we want. It makes sense to call our model User since we’re using it to track information about the user. The model for the sign-up app is more complex compared to the counter app. Here’s the counter app model again for comparison:

type alias Model
    = Int

Now that we’ve defined the structure of our model, we need to create an initial version of it. The initial model will be given to the app when it gets launched. Add the following code to the bottom of Signup.elm.

initialModel : User
initialModel =
    { name = ""
    , email = ""
    , password = ""
    , loggedIn = False
    }

View

The next step is to present the initial model to the user. We do that by passing the model to the view function. Add the following code to the bottom of Signup.elm.

view : User -> Html msg
view user =
    div []
        [ h1 [] [ text "Sign up" ]
        , Html.form []
            [ div []
                [ text "Name"
                , input
                    [ id "name"
                    , type_ "text"
                    ]
                    []
                ]
            , div []
                [ text "Email"
                , input
                    [ id "email"
                    , type_ "email"
                    ]
                    []
                ]
            , div []
                [ text "Password"
                , input
                    [ id "password"
                    , type_ "password"
                    ]
                    []
                ]
            , div []
                [ button
                    [ type_ "submit" ]
                    [ text "Create my account" ]
                ]
            ]
        ]

We already know what the div, text, and button functions do. The new functions — h1, form, and input — represent the <h1>, <form>, and <input> HTML tags respectively. The id function represents the id attribute of an HTML tag. Finally, the type_ function defines which type of button we want to use. We can also use type_ to specify the type for other tags such as <input>, <script>, and <style>.

Notice the underscore in the type_ function’s name. That’s because the type keyword is already taken in Elm. Earlier in the Naming Constants section, it was suggested that we should try to replace an underscore with a meaningful word like this:

> name = "Sansa"
"Sansa"

> name_ = "Stark"    -- Valid but not recommended
"Stark"
> firstName = "Sansa"
"Sansa"

> lastName = "Stark"    -- Much better
"Stark"

Following that suggestion, the creators of the Html module could have renamed the type_ function to buttonType, but then they’d have to create separate functions for specifying the type attribute of other tags too such as <input>, <script>, and <style>. That seems a bit superfluous, so the use of an underscore is justified in this case.

All of the functions used in view are defined in the Html and Html.Attributes modules, so go ahead and import them in Signup.elm.

module Signup exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
.
.

Notice how we had to use the Html prefix before form in the view function — Html.form. That’s because we exposed everything from the Html and Html.Attributes modules and both of them define a function called form. If we hadn’t added the prefix, the Elm would get confused and show us the following error.

Generally speaking, we should avoid exposing everything from a module due to reasons explained in the Qualified vs Unqualified Import section. However, if we have a family of modules that often get used together such as Html and Html.Attributes where the chances of name clashes are minimal and the function names are self-documenting then it’s safe to expose everything. If you don’t feel comfortable exposing everything at all, an alternative is to expose each function individually like this:

module Signup exposing (..)

import Html exposing (Html, div, h1, form, text, input, button)
import Html.Attributes exposing (id, type_)
.
.

Application Entry Point

To display the view, we need to define an entry point to our app. Add the following code to the bottom of Signup.elm.

main : Program Never User msg
main =
    beginnerProgram
        { model = initialModel
        , view = view
        , update = update
        }

We’ve already defined the initialModel and view functions. Let’s define the update function next. Add the following code right above the main function in Signup.elm.

update : msg -> User -> User
update msg model =
    initialModel


main =
    ...

The update function simply returns the initial model. We’ll expand it to respond to various messages generated by the UI elements a little bit later. Now, we’re ready to render the view on a screen. Run elm-reactor from the beginning-elm directory in terminal if it’s not running already, and go to this URL in your browser: http://localhost:8000/elm-examples/Signup.elm. You should see the sign-up form.

Styling Our View

Our sign-up form looks a bit ugly. Let’s add some style. There are multiple ways to style a page in Elm:

  • Using inline styles
  • Using the elm-css package
  • Using an external CSS file
  • Using a CSS framework

We’ll try each approach from the above list so that you can compare them. That way, you’ll have a better sense of which one feels more comfortable and maintainable to you.

1. Using Inline Styles

We can use the style function from the Html.Attributes module to add an inline style. Let’s add some padding to the header. In the view function, modify the line that applies the h1 function like this:

view : User -> Html msg
view user =
    div []
        [ h1 [ style [ ( "padding-left", "3cm" ) ] ] [ text "Sign up" ]
        , Html.form []
        .
        .

The style function takes a list of tuples that contain the CSS property names and values. Here’s how its type signature looks:

style : List (String, String) -> Attribute msg

The line that contains the h1 in the view function is a bit difficult to read with all the parentheses and square brackets. Let’s extract the style info into a separate function. Add the following function definition right below the view function.

view =
    ...


headerStyle : Attribute msg
headerStyle =
    style
        [ ( "padding-left", "3cm" ) ]

Now we can simply apply the headerStyle function like this:

view : User -> Html msg
view user =
    div []
        [ h1 [ headerStyle ] [ text "Sign up" ]
        , Html.form []
        .
        .

Next, we’ll style the form, input, and button tags. Add the following function definitions right below the headerStyle function.

headerStyle =
    ...


formStyle : Attribute msg
formStyle =
    style
        [ ( "border-radius", "5px" )
        , ( "background-color", "#f2f2f2" )
        , ( "padding", "20px" )
        , ( "width", "300px" )
        ]


inputTextStyle : Attribute msg
inputTextStyle =
    style
        [ ( "display", "block" )
        , ( "width", "260px" )
        , ( "padding", "12px 20px" )
        , ( "margin", "8px 0" )
        , ( "border", "none" )
        , ( "border-radius", "4px" )
        ]


buttonStyle : Attribute msg
buttonStyle =
    style
        [ ( "width", "300px" )
        , ( "background-color", "#397cd5" )
        , ( "color", "white" )
        , ( "padding", "14px 20px" )
        , ( "margin-top", "10px" )
        , ( "border", "none" )
        , ( "border-radius", "4px" )
        , ( "font-size", "16px" )
        ]

Apply these styles to the respective tags in the view function.

view : User -> Html msg
view user =
    div []
        [ h1 [ headerStyle ] [ text "Sign up" ]
        , Html.form [ formStyle ]
            [ div []
                [ text "Name"
                , input
                    [ id "name"
                    , type_ "text"
                    , inputTextStyle
                    ]
                    []
                ]
            , div []
                [ text "Email"
                , input
                    [ id "email"
                    , type_ "email"
                    , inputTextStyle
                    ]
                    []
                ]
            , div []
                [ text "Password"
                , input
                    [ id "password"
                    , type_ "password"
                    , inputTextStyle
                    ]
                    []
                ]
            , div []
                [ button
                    [ type_ "submit"
                    , buttonStyle
                    ]
                    [ text "Create my account" ]
                ]
            ]
        ]

If you refresh the page at http://localhost:8000/elm-examples/Signup.elm, you should see a much better looking sign-up form.

2. Using the elm-css Package

Although being able to style elements inline is convenient, it’s a bit cumbersome to use a list of tuples for specifying the CSS properties and values. More importantly, if we make a mistake typing one of the properties, the Elm compiler won’t catch it because they’re just plain old strings. Wouldn’t it be great if we could use Elm’s type system to detect errors in our CSS? That’s exactly what the elm-css package offers.

Let’s re-implement the styles we applied above using the functions defined in the Css module included in the elm-css package. Before we can do that though, we need to install the package. From the beginning-elm directory in terminal, run the following command.

elm-package install rtfeldman/elm-css

Answer y when asked to add rtfeldman/elm-css as a dependency to the elm-package.json file and approve the installation plan. After that, import the Css module in Signup.elm.

module Signup exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Css
.
.

Next, we’ll rewrite the headerStyle, formStyle, inputTextStyle, and buttonStyle functions using the functions defined in the Css module. Replace the contents of those functions in Signup.elm like this:

view =
    ...


styles cssPairs =
    Css.asPairs cssPairs
        |> Html.Attributes.style


headerStyle : Attribute msg
headerStyle =
    styles
        [ Css.paddingLeft (Css.cm 3) ]


formStyle : Attribute msg
formStyle =
    styles
        [ Css.borderRadius (Css.px 5)
        , Css.backgroundColor (Css.hex "#f2f2f2")
        , Css.padding (Css.px 20)
        , Css.width (Css.px 300)
        ]


inputTextStyle : Attribute msg
inputTextStyle =
    styles
        [ Css.display Css.block
        , Css.width (Css.px 260)
        , Css.padding2 (Css.px 12) (Css.px 20)
        , Css.margin2 (Css.px 8) (Css.px 0)
        , Css.border (Css.px 0)
        , Css.borderRadius (Css.px 4)
        ]


buttonStyle : Attribute msg
buttonStyle =
    styles
        [ Css.width (Css.px 300)
        , Css.backgroundColor (Css.hex "#397cd5")
        , Css.color (Css.hex "#fff")
        , Css.padding2 (Css.px 14) (Css.px 20)
        , Css.marginTop (Css.px 10)
        , Css.border (Css.px 0)
        , Css.borderRadius (Css.px 4)
        , Css.fontSize (Css.px 16)
        ]

As you can see, we’re now using plain old Elm functions to declare CSS properties. Instead of using the style function defined in the Html.Attributes module, we used our own function called styles which takes the CSS properties and converts them into key-value pairs that can be passed to the Html.Attributes.style function.

Since we didn’t expose the functions when importing the Css module, we had to use a prefix which made the code hard to read. The reason we didn’t expose them all was due to the fact that the Css module defines some functions that are also found in the Html, and Html.Attributes modules, for example text, id, and width. A better solution is to extract all CSS related code to a separate module. Let’s do that. Create a new file called SignupStyle.elm in the beginning-elm/elm-examples directory.

Add the following code to the SignupStyle.elm file.

module SignupStyle exposing (..)

import Html exposing (Attribute)
import Html.Attributes
import Css exposing (..)


styles cssPairs =
    Css.asPairs cssPairs
        |> Html.Attributes.style


headerStyle : Attribute msg
headerStyle =
    styles
        [ paddingLeft (cm 3) ]


formStyle : Attribute msg
formStyle =
    styles
        [ borderRadius (px 5)
        , backgroundColor (hex "#f2f2f2")
        , padding (px 20)
        , width (px 300)
        ]


inputTextStyle : Attribute msg
inputTextStyle =
    styles
        [ display block
        , width (px 260)
        , padding2 (px 12) (px 20)
        , margin2 (px 8) (px 0)
        , border (px 0)
        , borderRadius (px 4)
        ]


buttonStyle : Attribute msg
buttonStyle =
    styles
        [ width (px 300)
        , backgroundColor (hex "#397cd5")
        , color (hex "#fff")
        , padding2 (px 14) (px 20)
        , marginTop (px 10)
        , border (px 0)
        , borderRadius (px 4)
        , fontSize (px 16)
        ]

The CSS code looks much cleaner now. We need to import the SignupStyle module from the Signup.elm file.

module Signup exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import SignupStyle exposing (..)
.
.

Don’t forget to remove the import Css line from Signup.elm. Also, remove the styles, headerStyle, formStyle, inputTextStyle, and buttonStyle functions from that file. If you refresh the page at http://localhost:8000/elm-examples/Signup.elm, you should see the exact same form as before.

3. Using an External CSS File

The third option is to create a regular CSS file and use it in our Elm app. Unfortunately, elm-reactor doesn’t provide an easy way to load external CSS files. So we’ll have to compile the Signup.elm file ourselves. Create a new file called signup-style.css in the beginning-elm directory.

Add the following CSS code to the signup-style.css file.

h1 {
    padding-left: 3cm;
}

form {
    border-radius: 5px;
    background-color: #f2f2f2;
    padding: 20px;
    width: 300px;
}

input {
    display: block;
    width: 260px;
    padding: 12px 20px;
    margin: 8px 0;
    border: none;
    border-radius: 4px;
}

button {
    width: 300px;
    background-color: #397cd5;
    color: white;
    padding: 14px 20px;
    margin-top: 10px;
    border: none;
    border-radius: 4px;
    font-size: 16px;
}

Next, we’ll use elm-make to compile the Signup.elm file. Before that, we need to remove all traces of the SignupStyle module from that file. Remove the line that imports the SignupStyle module.

module Signup exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
.
.

We also need to stop applying the headerStyle, formStyle, inputTextStyle, and buttonStyle functions in view.

view : User -> Html msg
view user =
    div []
        [ h1 [] [ text "Sign up" ]
        , Html.form []
            [ div []
                [ text "Name"
                , input
                    [ id "name"
                    , type_ "text"
                    ]
                    []
                ]
            , div []
                [ text "Email"
                , input
                    [ id "email"
                    , type_ "email"
                    ]
                    []
                ]
            , div []
                [ text "Password"
                , input
                    [ id "password"
                    , type_ "password"
                    ]
                    []
                ]
            , div []
                [ button
                    [ type_ "submit" ]
                    [ text "Create my account" ]
                ]
            ]
        ]

Now we’re back to where our sign-up form had no style at all. Run the following command from the beginning-elm directory to compile Signup.elm to JavaScript.

elm-make elm-examples/Signup.elm --output signup.js

elm-make will create a new file called signup.js in the beginning-elm directory. If it already exists, its content will be overwritten. The final step is to create an HTML file that loads the signup-style.css and signup.js files. Create a file called signup.html in the beginning-elm directory.

Add the following code to the signup.html file.

<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet" href="signup-style.css">
</head>

<body>
    <div id="elm-code-is-loaded-here"></div>
    <script src="signup.js"></script>
    <script>
        Elm.Signup.embed(document.getElementById("elm-code-is-loaded-here"));
    </script>
</body>

</html>

The code for loading the signup-style.css file is straightforward, but the one that loads the Elm app is slightly more complex. First, we need to load the signup.js file, which contains the compiled JavaScript code. Remember, the signup.js file includes not only the code we wrote, but also the entire Elm runtime and all the other Elm packages we installed.

We created a div and gave it an id. We then asked the Elm runtime to load our app inside the elm-code-is-loaded-here div. When we call the Elm.Signup.embed function, the Elm runtime looks for the main function inside the Signup module and uses it as an entry point. If the Signup module is missing, or if the Signup module exists, but doesn’t have the main function, we’ll get an error. If you open the beginning-elm/signup.html file in a browser, you should see the exact same form as before.

Loading CSS from an external file forced us to take a few more steps, but it also enabled us to use any CSS file in our Elm app. This approach will come in handy if you are planning to introduce Elm into your existing app that already contains numerous CSS files.

4. Using a CSS Framework

It’s also possible to use a CSS framework with Elm apps. It’s actually very similar to using an external CSS file. Back in the Building a Simple Page with Elm section, we used the popular Bootstrap framework to style the home page of Dunder Mifflin Inc. Let’s use it here again to style our sign-up form. Modify the signup.html file to include the Bootstrap framework instead of the signup-style.css file.

<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>

<body>
    <div id="elm-code-is-loaded-here"></div>
    <script src="signup.js"></script>
    <script>
        Elm.Signup.embed(document.getElementById("elm-code-is-loaded-here"));
    </script>
</body>

</html>

Next, we need to specify which CSS classes we want to use in our Elm code. Modify the view function in Signup.elm like this:

view : User -> Html msg
view user =
    div [ class "container" ]
        [ div [ class "row" ]
            [ div [ class "col-md-6 col-md-offset-3" ]
                [ h1 [ class "text-center" ] [ text "Sign up" ]
                , Html.form []
                    [ div [ class "form-group" ]
                        [ label
                            [ class "control-label"
                            , for "name"
                            ]
                            [ text "Name" ]
                        , input
                            [ class "form-control"
                            , id "name"
                            , type_ "text"
                            ]
                            []
                        ]
                    , div [ class "form-group" ]
                        [ label
                            [ class "control-label"
                            , for "email"
                            ]
                            [ text "Email" ]
                        , input
                            [ class "form-control"
                            , id "email"
                            , type_ "email"
                            ]
                            []
                        ]
                    , div [ class "form-group" ]
                        [ label
                            [ class "control-label"
                            , for "password"
                            ]
                            [ text "Password" ]
                        , input
                            [ class "form-control"
                            , id "password"
                            , type_ "password"
                            ]
                            []
                        ]
                    , div [ class "text-center" ]
                        [ button
                            [ class "btn btn-lg btn-primary"
                            , type_ "submit"
                            ]
                            [ text "Create my account" ]
                        ]
                    ]
                ]
            ]
        ]

Bootstrap defines many CSS classes such as container, row, col-md-6, and form-group that have special meaning. We won’t try to understand what each of these classes mean in this book, but you can learn all about them from the official site. Once you know how the Bootstrap framework works, it’s pretty easy to understand the above code.

The next step is to re-compile the Signup.elm file using elm-make. Run the following command from the beginning-elm directory in terminal.

elm-make elm-examples/Signup.elm --output signup.js

Now, if you open the signup.html file in a browser, you should see this form:

One big disadvantage of using an external CSS file or framework in this way is that we lose the ability to detect errors in our CSS code during compile time. The good news is, the Elm community members are working hard to provide Elm packages for making the use of external frameworks such as Bootstrap and Material Design more reliable through Elm’s type system and other features. The elm-bootstrap and elm-mdl packages give us the ability to design our pages with Bootstrap and Material Design respectively without sacrificing Elm’s type safety. You should definitely check them out.

Let’s revert the view function back to where it used the styles defined in the SignupStyle.elm file so that we can keep using elm-reactor to test our changes instead of recompiling our code with elm-make every time we make a small change.

There is actually a library called elm-live which reloads pages automatically whenever a change is made to them. We won’t cover the instructions for setting it up in this book, but you should definitely give it a try.

view : User -> Html msg
view user =
    div []
        [ h1 [ headerStyle ] [ text "Sign up" ]
        , Html.form [ formStyle ]
            [ div []
                [ text "Name"
                , input
                    [ id "name"
                    , type_ "text"
                    , inputTextStyle
                    ]
                    []
                ]
            , div []
                [ text "Email"
                , input
                    [ id "email"
                    , type_ "email"
                    , inputTextStyle
                    ]
                    []
                ]
            , div []
                [ text "Password"
                , input
                    [ id "password"
                    , type_ "password"
                    , inputTextStyle
                    ]
                    []
                ]
            , div []
                [ button
                    [ type_ "submit"
                    , buttonStyle
                    ]
                    [ text "Create my account" ]
                ]
            ]
        ]

We also need to import the SignupStyle module again in the Signup.elm file.

module Signup exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import SignupStyle exposing (..)
.
.

Update

Now that we have presented a view to the users, what can they do with it? They can enter their name, email, and password into the form and click the Create my account button to initiate the creation of a new account. The first thing we need to figure out is how to store the name, email, and password into the User model. The Html.Events module defines a function called onInput that allows us to specify which message should be triggered when the user starts typing into an input text box. Here’s an example:

input [ id "name"
      , type_ "text"
      , inputTextStyle
      , onInput SaveName
      ]

Don’t type the above code yet; we’ll need to add the onInput attribute to all of our input fields. We’ll do that after we’ve defined the SaveName and all other messages needed in our app. Add the following type definition right above the update function.

type Msg
    = SaveName String
    | SaveEmail String
    | SavePassword String
    | Signup


update =
    ...

SaveName, SaveEmail, and SavePassword messages will be triggered when the user types in the name, email, and password respectively. The Signup message will be triggered when the Create my account button is clicked.

We need to tell Elm what to do with these messages. This is where the update function comes in. Whenever Elm receives a message, it checks to see if there is a pattern in the update function that matches the incoming message. If a pattern does exist, it executes the code for that pattern. Otherwise, the application crashes. Therefore, it’s important to handle all possible incoming messages. Modify the update function like this:

update : Msg -> User -> User
update message user =
    case message of
        SaveName name ->
            { user | name = name }

        SaveEmail email ->
            { user | email = email }

        SavePassword password ->
            { user | password = password }

        Signup ->
            { user | loggedIn = True }

The update function takes a message and a model (User in our case) and performs some operation on that model. It then returns the resulting model. So when the user types his or her name in a text field, Elm receives the SaveName message. It finds a pattern that matches this message:

SaveName name ->
    { user | name = name }

All we are doing here is saving the name that came in as a payload with the SaveName message into the User model using the update record syntax. The code for handling messages originated from the SaveEmail and SavePassword text fields looks very similar. The branch for the Signup message simply sets the loggedIn flag to True. The actual code for handling this message requires us to know how to send HTTP requests to a server in Elm. We haven’t learned that yet, so we’ll have to wait until the HTTP Requests section to fully implement it.

Not sure if you noticed, but the type annotation of the update function now uses the Msg type instead of the msg type variable.

-- Before the Msg type was defined
update : msg -> User -> User


-- After the Msg type was defined
update : Msg -> User -> User

We also need to replace the msg type variable with the Msg type in the view and main functions’ type annotations.

view : User -> Html Msg
view user =
    ...
main : Program Never User Msg
main =
    ...

Next, we need to add the onInput and onClick attributes to all input fields and Create my account button respectively in the view function.

view : User -> Html Msg
view user =
    div []
        [ h1 [ headerStyle ] [ text "Sign up" ]
        , Html.form [ formStyle ]
            [ div []
                [ text "Name"
                , input
                    [ id "name"
                    , type_ "text"
                    , inputTextStyle
                    , onInput SaveName
                    ]
                    []
                ]
            , div []
                [ text "Email"
                , input
                    [ id "email"
                    , type_ "email"
                    , inputTextStyle
                    , onInput SaveEmail
                    ]
                    []
                ]
            , div []
                [ text "Password"
                , input
                    [ id "password"
                    , type_ "password"
                    , inputTextStyle
                    , onInput SavePassword
                    ]
                    []
                ]
            , div []
                [ button
                    [ type_ "submit"
                    , buttonStyle
                    , onClick Signup
                    ]
                    [ text "Create my account" ]
                ]
            ]
        ]

Since the onInput and onClick functions are defined in the Html.Events modules, we need to import that in Signup.elm.

module Signup exposing (..)
.
.
import SignupStyle exposing (..)
import Html.Events exposing (onInput, onClick)
.
.

If you refresh the page at http://localhost:8000/elm-examples/Signup.elm, you should see the sign-up form once again.

After the user has typed all three pieces of information and clicked the Create my account button, the resulting model will look like this:

{ name = "Art Vandelay"
, email = "art@vandelayindustries.com"
, password = "opposite"
, loggedIn = True
}

We don’t get any visual feedback when the Create my account button is clicked, but we’ll fix that later in the Http Requests section.

Summary

In this section, we reinforced our understanding of how the Model View Update pattern works by building a slightly more complex app that allowed users to enter the name, email, and password information. The interaction between various parts of the sign-up form app is very similar to how the components in the counter app from the Model View Update - Part 1 section interacted with each other.

We also learned how to style our views in different ways using inline CSS, the elm-css package, an external CSS file, and an external framework such as Bootstrap.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close