In Model View Update - Part 1, we used a very simple example to learn the fundamental concepts behind 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 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:
Create a new file called Signup.elm
in the beginning-elm/src
directory and add the above code to it.
Our model is a record with four fields. The user entered information will be stored in name
, email
, and password
. loggedIn
will indicate whether or not the user has been authenticated to use the 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 our sign-up app is more complex compared to the counter app. Here’s the counter app model again for comparison:
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
.
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
.
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:
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
.
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 value individually like this:
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
.
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/src/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 use all of those approaches to style the sign-up form. 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:
The style
function takes two arguments: CSS property name and value. Here’s how its type signature looks:
Next, we’ll style the form
, input
, and button
tags. Add the following code below the view
function.
Apply these styles to the respective tags in the view
function.
Refresh the page at http://localhost:8000/src/Signup.elm
and you should see a much better looking sign-up form.
Note: The best practice for working with HTML suggests that the styles should primarily be specified in CSS files. Therefore, we shouldn’t use too many inline styles.
2. Using the elm-css
Package
It’s convenient to style elements inline. However, 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 too? That’s exactly what the elm-css
package offers.
Let’s re-implement the styles we applied above using functions defined in the Css
module included in the elm-css
package. Before we can do that though, we need to install the package. Run the following command from the beginning-elm
directory in terminal.
Answer y
when asked to add rtfeldman/elm-css
and other packages to elm.json
file. elm-css
is a complex package with more than a dozen modules in it, but we’re interested in only three of them:
Css
Html.Styled
Html.Styled.Attributes
Css Module
The Css
module enables us to apply CSS styles using plain old Elm functions. For example, if we want to give a blue background to a button, we can use the backgroundColor
and hex
functions defined in the Css
module like this:
For comparison, here is how we used the style
function earlier to do exactly that:
If we make a mistake while typing a function name from the CSS
module, the Elm compiler will throw an error. However, if we introduce a typo in the CSS property name passed to the style
function, the compiler can’t help us. By taking advantange of Elm’s powerful type system, we can get rid of so many CSS related bugs during compile time. Isn’t that nice? Let’s import the Css
module in Signup.elm
.
Html.Styled Module
Html.Styled
is a drop-in replacement for the Html
module from the elm/html
package. All it does is return a styled version of elements defined in the Html
module. For example, here is how the Html.text
function is implemented.
Remember all HTML elements in Elm are virtual DOM nodes behind the scenes. Let’s compare the implementation above with that of Html.Styled.text
.
As you can see, the only difference is that Html.Styled.text
returns a styled node whereas Html.text
returns a plain old node. Let’s import Html.Styled
in Signup.elm
and remove the line that imports the Html
module.
We can now create styled elements in our app like this:
Add that code below view
in Signup.elm
and also remove the formStyle
, inputStyle
, and buttonStyle
functions while we’re at it. To style an element using elm-css
, we need to use the styled
function defined in the Html.Styled
module. Here is how its type signature looks:
The first argument is an HTML element from the Html.Styled
module. We can’t use the one defined in Html
. The second argument is a list of CSS styles. Each style is represented by the Style
type defined in the Css
module we covered earlier. Now that we have styled the form
, input
, and button
elements separately, let’s use them in our view
function.
Html.Styled.Attributes Module
To style the header, we’ll use the css
function from the Html.Styled.Attributes
module like this:
We’re still inlining our style here, but we’re using an Elm function to specify the padding-left
CSS property. Here’s how the css
function’s type signature looks:
Html.Styled.Attributes
is a drop-in replacement for the Html.Attributes
module from the elm/html
package. Let’s import it in Signup.elm
and remove the line that imports Html.Attributes
.
The id
and type_
attributes used inside the view
function are now from Html.Styled.Attributes
instead of Html.Attributes
.
Rendering Styled Elements
All of our HTML elements and their attributes now come from the Html.Styled
and Html.Styled.Attributes
modules respectively. Elm doesn’t know how to render them. Luckily, the Html.Styled
module provides a function called toUnstyled
for converting the styled elements to something Elm can render. Here is how its type signature looks:
Modify main
in Signup.elm
to this:
main
’s type is now VirtualDom.Node msg
. Before using elm-css
it was Html msg
. The definition of the Html
type reveals that it’s just a type alias for VirtualDom.Node
.
Because of this, we are allowed to return a virtual DOM node directly from main
. Let’s install the elm/virtual-dom
package by running the following command from the beginning-elm
directory.
Actually, that package is already installed as an indirect
dependency. Answer y
to make it a direct
dependency. Now import the VirtualDom
module in Signup.elm
.
Finally, we’re ready to run our app. Refresh the page at http://localhost:8000/src/Signup.elm
and 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 manually. Create a new file called signup-style.css
in the beginning-elm
directory and add the code below to it.
Next, we’ll use elm make
to compile the Signup.elm
file. Before we do that, we need to remove all traces of elm-css
from that file. Remove the styledForm
, styledInput
, and styledButton
functions and modify view
to this:
We’re back to where our sign-up form had no style at all. Remove toUnstyled
from main
and also change its type signature.
And replace the import list in Signup.elm
with the following.
We’re now ready to compile Signup.elm
to JavaScript. Run the following command from the beginning-elm
directory in terminal.
elm make
will create a new file called signup.js
in the beginning-elm
directory. If it already exists, its contents will be overwritten. The final step is to create an HTML file that loads the signup-style.css
and signup.js
files. Create a new file called signup.html
in the beginning-elm
directory and add the code below to it.
The code for loading the signup-style.css
file is straightforward, but the one that loads our 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.init
function, the 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. 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.
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:
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 Bootstrap works, it’s pretty easy to understand the code above.
The next step is to re-compile Signup.elm
. Run the following command from the beginning-elm
directory in terminal.
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.
For the rest of this section, we’ll use elm-css
, so let’s revert Signup.elm
back to the following code. This will allow us to keep using elm reactor
instead of recompiling our code with elm make
every time we make a small change.
Note: There is actually a tool called elm-live
which reloads pages automatically whenever a change is made to them. We’ll cover it in the Using elm-live section in chapter 6.
Update
Now that we have presented the initial model to users, what can they do with it? They can enter their name, email, and password into the form and click Create my account to sign up. The first thing we need to figure out is how to store name, email, and password entered by the user in User
model. The Html.Events
module from the elm/html
package 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:
Note: The Html.Styled.Events
module from the elm-css
package also defines the onInput
function. Just like Html.Styled
and Html.Styled.Attributes
, the Html.Styled.Events
module is a drop-in replacement for Html.Events
.
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.
Handling Messages
Add the following type definition right above main
in Signup.elm
.
SaveName
, SaveEmail
, and SavePassword
messages will be triggered when the user enters 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. Add the update
function above main
in Signup.elm
.
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 users type their name in a text field, Elm receives the SaveName
message. It finds a pattern that matches this message:
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 chapter 6 to fully implement it.
Creating Messages
We’ve created a mechanism for handling messages, but we still don’t have a way to create them. Let’s take care of that. Add onInput
and onClick
to styledInput
and styledButton
respectively so that they can fire messages to the runtime.
Previously, the HTML returned by view
wasn’t creating any messages, so we just used msg
as a placeholder in its type annotation.
Now, view
uses the onInput
and onClick
functions from the Html.Styled.Events
module to generate messages of type Msg
. That’s why we needed to change msg
to Msg
in view
’s type annotation. Let’s import Html.Styled.Events
in Signup.elm
.
Wiring Everything Up
Here is how our main
looks right now in Signup.elm
:
Passing initialModel
to view
was enough when we were just displaying a static view. To allow interactivity, we need to add update
in the mix. Modify main
to this:
And import the Browser
module in Signup.elm
.
When we compare the main
function in Signup.elm
to the one in Counter.elm
from the Model View Update - Part 1 section, we notice two differences.
Difference #1: The type annotation for main
in Signup.elm
includes User
instead of Model
. That’s because we decided to name our model User
to make it more descriptive.
Difference #2: toUnstyled
is applied before rendering the view in Signup.elm
.
As mentioned earlier in the Rendering Styled Elements section, Elm doesn’t know how to render elements and attributes defined in the Html.Styled
and Html.Styled.Attributes
modules from the elm-css
package. Therefore, we need to use the toUnstyled
function to convert them to something Elm can render.
Using >>
Operator
We assigned an anonymous function to the view
field in Browser.sandbox
. Anonymous functions are a little hard to read by nature. We can simplify main
in Signup.elm
by replacing the anonymous function with >>
like this:
>>
is a built-in operator for composing multiple functions. You can think of func1 >> func2
as equivalent to this:
We’re now ready to run our app once again. Refresh the page at http://localhost:8000/src/Signup.elm
and you should see the sign-up form.
When the user enters all information and clicks Create my account, the resulting model will look like this:
We don’t get any visual feedback when the Create my account button is clicked because we aren’t sending the user data to a server. We’ll learn how to do that in chapter 6.
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 their name, email, and password.
The following diagram shows the interaction between various parts of the sign-up form app which is very similar to the interaction between different components in the counter app we built in Model View Update - Part 1.
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. Here is the entire code for building a sign-up form using elm-css
: