Write a post
Published Oct 24, 2016Last updated Jan 18, 2017

Building a Facebook-like Comment Box: A React Tutorial in Elm

Building a Facebook-like Comment Box: A React Tutorial in Elm

I. Introduction

Elm is a new programming language that compiles to JavaScript. It is a functional programming language that features static typing, ensuring type-safety in your web apps and catching common errors.

React is a library for creating user interfaces in JavaScript. What makes it cool is how fast and performant it is when rendering, thanks to its use of the concept of the Virtual DOM (Document Object Model). The virtual DOM lets changes be made in batches before they are applied to the DOM.

While the performance of React is awesome, at a certain point, you will notice that having React just isn't enough. You'll also want Redux, Flux, or some other data flow library installed. You will want Webpack and Babel installed so that you can use the latest features of ES6 (also known as EMCAScript 2016). Then you'll probably want to add type-checking and static types at that point, so you will also need to install Flow, a JavaScript type-checker.

Elm will give all the goodies that React, Redux/Flux, and Flow give you but as a collective programming language rather than as individual libraries.

II. What you will learn

In this tutorial, you will learn how to use Elm to accomplish the same thing as Facebook's React tutorial. That means we will be creating a CommentBox component where a user can enter a new comment and see a list of existing comments.

You will learn how to do the following in Elm:

  • How to use Elm Reactor to compile and run an Elm program
  • How to create a module
  • How to create a view
  • Update the HTML based on user input

III. Installing Elm

The quickest way to get started with Elm is to install it using NPM, the Node Package Manager.

You can do that with this command:

npm install -g elm

IV. Running Elm apps

A. Elm reactor

The Elm reactor will run a web server and host the Elm files in your directory. It will compile each Elm file when it is run.

To run it, do this:

elm reactor

The web server will be running on localhost:8000 and will display a list of Elm files when you go to that URL in a web browser. When you click a file, it will be compiled from Elm into JavaScript and run in the browser. This makes it fast to iterate through the development of a component or of a whole web application.

B. Elm REPL, interactive command-line interface

You can also run Elm in a REPL, a Read-Evaluate-Print-Loop, which is also known as an interactive command-line interface. You may be familiar with this in other languages like Python and Ruby's IRB. The REPL lets you experiment with the language on the command-line, typing in various expressions to see what happens.

V. Setting up the project

We will be creating a comment box component using Elm, it is similar to the comment box you see on blogs such as Wordpress and websites such as Facebook. It will show a list of comments about an article along with a form for a user to enter their own comment and submit it.

First, let's create a new directory named elm-tutorial. On the command-line, when you are in the directory, you will need to create the Elm project with the base Elm packages.

You can do this by typing in:

elm package install

You will be asked whether you want to install three elm-lang packages (core, html and virtual-dom), enter "Y" and press Enter to install those packages. This will create a new file named elm-package.json and a directory named elm-stuff. In elm-package.json you will see something that looks like a typical NPM-based package.json file. You can fill this file out later. The elm-stuff directory is similar to the node_modules directory when using NPM.

VI. Creating the CommentBox component

Open up the file CommentBox.elm. This is where we're doing to define our comment box component and display it to the user.

A. Importing what we need

We begin by importing the modules and libraries we will need.

A.1. Importing HTML elements for rendering

First up is the HTML library which is used for rendering HTML DOM elements such as H1 (heading level 1) and P (paragraphs):

import Html exposing (..)

A.2. Importing HTML attributes

Then we import the attributes for the HTML elements, things like href and class:

import Html.Attributes exposing (..)

A.3. Importing HTML events

Finally, we also import the events that will be handled like onClick and onInput:

import Html.Events exposing (..)

B. Representing a Comment as an object

In Elm, as in other functional languages like Haskell and Scheme, it has something called a record type. It is similar to a class but without any methods of its own, it is more similar to a struct in the C programming language.

To represent a comment on an article, we will define the Comment record type. It will contain a user's name and some text:

type alias Comment =
  { name : String
  , description : String
  }

C. Getting the name of the author of a comment

When displaying comments, we want to let the user know if they've already commented on the article. We want to check this by comparing the author of the comment with the current user's name. When the name of the comment's author matches the name of the current user, then their name will appear as "[NAME] (You)" to show that they've already posted a comment on the article.

We can define a function, author, that does this, like so:

author : String -> Comment -> String
author currentUser { name } =
  if currentUser == name then
    name ++ " (You)"
  else
    name

The first part is the type signature of the function, the second part is the function definition itself. The type signature of a function doesn't always need to be defined but it is a very good habit and gives clarity to the writing of code.

D. Elm components display a model

Every Elm component that is displayed has a model definition. This model contains all the information needed to render the component. These are similar to the properties and state of a React component.

Our comment box is going to need three pieces of data; the text for a new comment, the name of the author of the new comment, and the list of existing comments that are being displayed:

type alias Model =
  { newCommentText : String
  , newCommentUser : String
  , comments : List Comment
  }

The model is defined as a type alias becauseā€¦

E. How to add new comments with Messages and Events

To get something to happen in Elm, you send messages. Messages are a type that you define for each Elm component; they are the events that your component will be able to handle and respond to.

In our case, the comment box component has these events:

  • The user has entered a username for a new comment;
  • the user has entered some text for a new comment;
  • or, the user has pressed "add comment".

Let's define the Message types for our component:

type Msg
  = AddComment
  | ChangeNewCommentUser String
  | ChangeNewCommentText String

Next, we define the update function. In this function, we will be checking which type of message has been sent and then updating the model in particular ways.

When the AddComment message is received, we will be creating a new comment from the username and text entered by the user and adding it as the first element of the comments list. We also clear the comment text so that the user can add a new comment.

When the ChangeNewCommentUser message is received, we update the new comment username based on the value received.

When the ChangeNewCommentText message is received, we update the new comment's text based on the value received.

update : Msg -> Model -> Model
update msg model =
  case msg of
    AddComment ->
      let
        comment = { name = ( "User " ++ model.newCommentUser)
        , description = model.newCommentText
        }
      in
        { model
        | comments = comment :: model.comments
        , newCommentText = ""
        }
    
    ChangeNewCommentUser username ->
      { model | newCommentUser = username }
    ChangeNewCommentText description ->
      { model | newCommentText = description }

F. Displaying the components and sending messages to trigger events

Now we define some view functions to display the pieces of the page.

F.1. Displaying a list of comments

The first view function will display the existing list of comments. And since we're displaying a list, we also need another helper function to display each individual comment:

commentView : Comment -> Html Msg
commentView comment =
  div [ class "comment" ]
    [ text (author "User A" comment)
    , text " - "
    , text comment.description
    ]
    
commentList : Model -> Html Msg
commentList model =
  div [ class "comment-list" ]
    (List.map commentView model.comments)

F.2. Displaying the input for a username

The second view function will display the input field for a user to enter their username. Whenever the user types into the box, the ChangeNewCommentUser message will be sent to the update function.

inputUsername : Model -> Html Msg
inputUsername model =
  div []
    [ label[] [ text "User:" ]
    , input [ class "comment-input-user"
      , type' "text"
      , value model.newCommentUser
      , onInput ChangeNewCommentUser
      ]
      []
    ]

F.3. Displaying the input for the comment text

The third view function will display the text area for a user to enter their new comment. Whenever the user types into the box, the ChangeNewCommentText message will be sent to the update function.

inputComment : Model -> Html Msg
inputComment model =
  div []
    [ label [] [ text "Comment:" ]
    , textarea [ class "comment-input-text"
      , onInput ChangeNewCommentText
      , value model.newCommentText
      ]
      []
    ]

F.4. Displaying the button to add a comment

The fourth view function will display a button. When the button is pressed it will send the AddComment message. It will trigger the adding of the new comment to the list of comments:

inputButton : Html Msg
inputButton =
  div []
    [ button [ class "comment-button-add"
      , onClick AddComment
      ]
      [ text "Add Comment" ]
    ]

F.5. Combining the view functions into one view

Now we define the view function which will use the view functions to put together the components for displaying to the user:

view : Model -> Html Msg
view model =
  div [ class "comment-box" ]
    [ commentList model
    , div [ class "comment-input" ]
      [ inputUsername model
      , inputComment model
      , inputButton
      ]
    ]

G. Putting It All Together

Finally, we need to display the comment list and comment input box on the screen. To do this, we're going to define the main entry point of this module so that Elm knows what to render in the browser, how to start up the web application, which function does the updating and what data flow subscriptions exist.

At the top of the file, we need to import the Html.App module which provides the entry point definition function:

import Html.App

Then at the bottom of the file, we define the initial state of the app. We're going to start with blank fields for new comment text and user name, and we're going to display a list of two comments:

model =
  { newCommentText = ""
  , newCommentUser = ""
  , comments = [ { name = "User A" , description = "Hi!" }
    , { name = "User B" , description = "Hello world!" } ]
  }

Then we define main entry point using the Html.App.beginnerProgram function:

main =
  Html.App.beginnerProgram
    { model = model
    , view = view
    , update = update
    }

The beginnerProgram function is a simplified way to define the entry point of an Elm program. It lets you specify the model of the component, the view that will display the model and the function that will be updating the model. In contrast, a typical Elm program will define the initial state of the model and any data flow subscriptions. The simplified beginnerProgram function lets us get started more quickly.

This is how it looks when we put it all together:

1  -- react-tutorial.elm
2  import Html exposing (..)
3  import Html.Attributes exposing (..)
4  import Html.Events exposing (..)
5  import Html.App
6  
7  type alias Comment =
8    { name : String
9    , description : String
10   }
11  
12  author : String -> Comment -> String
13  author currentUser { name } =
14    if currentUser == name then
15      name ++ " (You)"
16    else
17      name
18  
19  type alias Model =
20    { newCommentText : String
21    , newCommentUser : String
22    , comments : List Comment
23    }
24  
25  type Msg
26    = AddComment
27    | ChangeNewCommentUser String
28    | ChangeNewCommentText String
29  
30  update : Msg -> Model -> Model
31  update msg model =
32    case msg of
33      AddComment ->
34        let
35          comment = { name = ( "User " ++ model.newCommentUser)
36            , description = model.newCommentText
37            }
38        in
39          { model
40          | comments = comment :: model.comments
41          , newCommentText = ""
42          }
43  
44      ChangeNewCommentUser username ->
45        { model | newCommentUser = username }
46  
47      ChangeNewCommentText description ->
48        { model | newCommentText = description }
49  
50  commentView : Comment -> Html Msg
51  commentView comment =
52    div [ class "comment" ]
53      [ text (author "User A" comment)
54      , text " - "
55      , text comment.description
56      ]
57  
58  commentList : Model -> Html Msg
59  commentList model =
60    div [ class "comment-list" ]
61      (List.map commentView model.comments)
62  
63  inputUsername : Model -> Html Msg
64  inputUsername model =
65    div []
66      [ label [] [ text "User:" ]
67      , input [ class "comment-input-user"
68        , type' "text"
69        , value model.newCommentUser
70        , onInput ChangeNewCommentUser
71        ]
72        []
73      ]
74  
75  inputComment : Model -> Html Msg
76  inputComment model =
77    div []
78      [ label [] [ text "Comment:" ]
79      , textarea [ class "comment-input-text"
80      , onInput ChangeNewCommentText
81      , value model.newCommentText
82      ]
83      []
84   ]
85  
86  inputButton : Html Msg
87  inputButton =
88    div []
89      [ button [ class "comment-button-add"
90      , onClick AddComment
91      ]
92      [ text "Add Comment" ]
93    ]
94  
95  view : Model -> Html Msg
96  view model =
97    div [ class "comment-box" ]
98      [ commentList model
99      , div [ class "comment-input" ]
100     [ inputUsername model
101     , inputComment model
102     , inputButton
103     ]
104   ]
105  
106  model =
107    { newCommentText = ""
108    , newCommentUser = ""
109    , comments = [ { name = "User A" , description = "Hi!" }
110      , { name = "User B" , description = "Hello world!" } ]
111    }
112  
113  main =
114    Html.App.beginnerProgram
115      { model = model
116      , view = view
117      , update = update
118      }  

VII. Using an Elm Component on a regular web page

By far, the coolest thing about Elm is how easy it is to insert Elm components into existing web pages and web applications. It doesn't matter if you're using Ember.js, AngularJS, or some other front-end web framework, you can still use Elm components. What makes this cool is that when you're working with existing legacy code, you can create new components in Elm and take advantage of the performance and type-checking safety of Elm.

A. Compiling the Elm file

Take the Elm file that we wrote, react-tutorial.elm and compile it using elm:

elm make react-tutorial.elm

The file generated will be index.html and it will contain the component and all the JavaScript code needed for the component. The file generated for me was around 8000 lines of code. While this is great for playing around with Elm and sharing the HTML file will let you share the component, it isn't the same as real-world production settings.

Let's try that again, this time by compiling the Elm component into a JavaScript file:

elm make react-tutorial.elm --output comment-box.js

When you open up comment-box.js it will also be around 8000 lines of code. It includes all of Elm, all the Elm modules we used and the main component code.

B. Inserting the component

First we're going to set up our basic HTML page to have a div whose id is "comment-box". This is the element that will be taken over by Elm to display the comment box component:

<h2>Hello Elm!</h2>
<p>This is an article, below is the comment box.</p>
<div id="comment-box"></div>

To insert the Elm component onto the page, we're going to use the script tag to import the JavaScript file that we just generated.

<script src="comment-box.js"></script>

Then we're going to insert the comment box into the element whose id is "comment-box". The steps will be similar when integrating Elm into frameworks like AngularJS and Ember.js:

<script>
var node = document.getElementById("comment-box");
var app = Elm.Main.embed(node);
</script>

C. Just for fun: Styling the comment box the Facebook way

Just to match the style of Facebook, let's add some CSS for the Elm component.

Here's how it looks:

.comment-box {
  border-radius: 3px;
  border: 1px solid #ccccff;
  padding: 10px 3px;
  }
    
.comment-input {
  margin-top: 10px;
    }
    
.comment-input label {
  color: #5555cc;
    }
    
.comment {
  padding: 10px;
  margin-bottom: 5px;
  background-color: #eeeeff;

.comment-button-add {
  border-radius: 2px;
  border: 1px solid #555555;
  color: #333333;
  background-color: #ffffff;
  box-shadow: 0px 1px 5px 0px #aaa;
    }

D. Putting together the final HTML file

And finally, let's see how it looks when it's all put together. We're setting the style of the component, then including the JavaScript file that was compiled by Elm.

1  <html>
2    <style>
3    .comment-box {
4      border-radius: 3px;
5      border: 1px solid #ccccff;
6      padding: 10px 3px;
7    }
8  
9    .comment-input {
10      margin-top: 10px;
11    }
12  
13    .comment-input label {
14      color: #5555cc;
15    }
16  
17    .comment {
18      padding: 10px;
19      margin-bottom: 5px;
20      background-color: #eeeeff;
21    }
22  
23    .comment-button-add {
24      border-radius: 2px;
25      border: 1px solid #555555;
26      color: #333333;
27      background-color: #ffffff;
28      box-shadow: 0px 1px 5px 0px #aaa;
29    }
30    </style>
31    <body>
32      <h2>Hello Elm!</h2>
33      <p>This is an article, below is the comment box.</p>
34      <div id="comment-box"></div>
35      <script src="comment-box.js"></script>
36      <script>
37      var node = document.getElementById("comment-box");
38      var app = Elm.Main.embed(node);
39      </script>
40    </body>
41  </html>

VIII. Let's see how it turned out

Here's how it looks in the browser:

React tutorial

Thanks for reading this tutorial on using the Elm functional programming language for front-end web development. It's an excellent alternative to other languages that compile to JavaScript, like TypeScript or Babel/ES6. Elm has the ideas of immutability and data flow subscriptions built into, along with type-checking. It's a strong alternative to JavaScript and as shown in this tutorial, it's also an easy-to-learn alternative to libraries and frameworks like React.js.

Try out Elm for a small component like a comment box or a survey form in your next website. Happy hacking!


Other tutorials you might find interesting

Discover and read more posts from Rudolf Olah
get started
Enjoy this post?

Leave a like and comment for Rudolf

2