Build an Instagram Clone in Elm: InstaElm Part 1

Published Nov 24, 2016Last updated Mar 14, 2017
Build an Instagram Clone in Elm: InstaElm Part 1

Introduction

In the last article on Elm, I showed you how to create a Facebook-like comment box component. That was just the starting step to using Elm for real world projects. This tutorial is the first part in a two-part series to show you how to use Elm to build a real project.

In this article, we're going to be building an Instagram clone using Elm. Instagram is a good example to work with because it's straightforward and is a great starting point for adding more features. We start with the photo grid, move onto the photo opener and then, in the second part of this two-part series, we work with an AJAX API in Elm.

You can check out the code for part 1 of the tutorial on gitlab.com

You can also click here to see the CodeMentor office hours webinar that this tutorial is based on.

What You Will Learn

We're going to be covering how the Elm architecture works by implementing two components of the Instagram website:

  • The photo grid: The photo grid displays a grid of photos and when you click on one of the photos, the photo viewer will pop up.
  • The photo viewer: In the photo viewer, you will see the photo and a sidebar, showing off some information about that photo, such as the name of the user who took the photo, the location that the photo was taken at when the photo was taken, etc. When you're done looking at the photo, you'll be able to click a close button and go back to the photo grid.

This kind of interaction between components is common in all sorts of applications but can be tricky in Elm at first, when you're used to React, Angular, Ember.js or other web frameworks.

Instagram clone

Setup

Installing Elm

You can refer to an earlier tutorial on Elm for instructions on installing it and for running the code.

Setting Up The Project

You're going to create a new directory and install Elm and then create two files: Main.elm and PhotoView.elm.

In PhotoView.elm you will want to define the module and also what it imports.

module PhotoView exposing (..)
    
import Html exposing (Html, div, text, input, img)
import Html.Attributes exposing (class, type', src)
import Html.Events exposing (onClick)
import Html.App exposing (beginnerProgram)
import List exposing (repeat, map, append, reverse)

In Main.elm, you will want to define the module and its imports:

module Main exposing (..)
    
import PhotoView exposing (Photo, Comment)
import Html.App exposing (beginnerProgram)
import Html exposing (Html, div, text, img, span)
import Html.Attributes exposing (class, src, width, height)
import Html.Events exposing (onClick)
import List exposing (map, take, length, drop, repeat, append)

The imports that we define will be used later on in the code snippets.

The Data Model: Defining Types

In Elm, just like in other languages, you're encouraged to start thinking about the data types you're going to manipulate and work with. In JavaScript, for Instagram, we would start with defining an object that represents a single photo. Then we would define a comment object for all the user comments on a photo.

We're going to use records in Elm to define these two data types.

First let's go over what records are and how we're going to use type aliases to define the Photo and Comment data types.

What are records?

Records are objects that have fields, similar to key/value dictionaries and hash tables. For each field in a record, you can define its type and when compiling, Elm will make sure you're creating new records that have the correct types. This is something you can do with Flow and TypeScript as well.

You can define anonymous records anywhere you like. When we define our records, we're actually going to be defining type aliases, rather than new types.

What are type aliases?

A type alias is an alias for the fields that need to be defined in a record for that record to be recognized as that particular type.

According to the Elm documentation,

The whole point of type aliases is to make your type annotation easier to read.
...
Type aliases are not just about cosmetics though. They can help you think more clearly. When writing Elm programs, it is often best to start with the type alias before writing a bunch of functions. I find it helps direct my progress in a way that ends up being more efficient overall. Suddenly you know exactly what kind of data you are working with.

To give you an example: if you have a record with two fields, name and age, and you define the type alias Person, then any record with any number of fields will be recognized as a Person type if it has both of those fields. So if your object has three fields, and among those fields is name and age, it will be a valid Person type.

Defining the photo and comment types

Now that we've got that out of the way, let's see how that looks and define the photo and comment types.

Photo type and example instance

What attributes does a photo have? It has the name of the photographer, the location it was taken in, and there are some statistics about the photo. The stats are the number of likes the photo has received, and the total number of comments there are. There's also the current list of comments that have been loaded. Finally, the photo has a URL for the image to display.

type alias Photo =
    { user : String
    , location : String
    , likesCount : Int
    , commentsCount : Int
    , comments : List Comment
    , url : String
    }

Let's also define an example instance of a Photo record:

examplePhoto : Photo
examplePhoto =
    { user = "Rudolf"
    , location = "Toronto, Canada"
    , likesCount = 1000000
    , commentsCount = 99
    , comments = repeat 10 exampleComment
    , url = "webinar-ad.png"
    }

You'll note that we created 10 example comments using the List.repeat function, you'll need to import the List module for that function.

Comment type and example instance

Comments are simple, they have the text of a message and the name of the user who wrote the message. Let's define the Comment type alias:

type alias Comment =
    { user : String
    , message : String
    }

Then let's define some example comments:

exampleComment : Comment
exampleComment =
    { user = "Person"
    , message = "Amazing photo!"
    }

PhotoView.elm: Viewing A Photo

Now that we have the Photo and Comment type, let's work on making a component that will display the photo and the information about it, including the comments.

Data Model Of The PhotoView

When we open up the photo view, the data model contains the photo we are displaying, the new comment that a user is going to add (if they would like to add one), and whether or not to show the close button.

type alias Model =
    { photo : Photo
    , newComment : String
    , showCloseButton : Bool
    }

PhotoView Actions And Update Messages

For the PhotoView component, we also want to define some of the actions that can be taken. Actions are messages that are sent from events like clicking on a button or changing the text in an input box. The update function, which we will define later, will update the data model based on which action message was sent to it.

When we open up a photo in the PhotoView, we will have a few actions available: liking the photo; submitting a comment about the photo; and if the photo view is a modal, we can also close the modal.

We define the Msg type with each of these action types:

type Msg
    = LikePhoto
    | SubmitComment String
    | CloseModal

Notice that for the SubmitComment we define the type of argument as well. When a comment is submitted, the update function will also give us access to the comment string that the user entered. If you want to handle more actions, you can add more types to the Msg type and add action handlers for each new message in the update function.

Displaying The Photo In The PhotoView

The purpose of the PhotoView component is showing a photo, so let's use the img HTML tag to do that:

photo : String -> Html Msg
photo photoUrl =
    img [ class "photo", src photoUrl ] []

It's a very simple function, we're accepting the URL of the photo and then displaying it as the source of an image. We're also setting the CSS class name to "photo".

This is something I enjoy about Elm, most of the code you're writing ends up being small composable functions. This allows you to iterate faster and it feels more natural. You can write some functions, try them out, see what happens and then rewrite them quickly. If you need a new component, just write a new function.

We also need to define the sidebar function which returns all the HTML necessary to display the sidebar with photo information, like the name of the person who took the photo and the comments on the photo.

We start with the sidebar function and separate it into three parts: the top of the sidebar, the row displaying the number of likes and comments, and the various user comments about the photo.

sidebar : Photo -> Html Msg
sidebar photo  =
    div [ class "sidebar" ]
        [ sidebarTop photo.user photo.location
        , sidebarCount photo.likesCount photo.commentsCount
        , sidebarComments photo.commentsCount photo.comments
        ]

Basic photo info

The top of the sidebar is going to display the name of the person who took the photo and the location it was taken in. It will also include two actions, the "like" button and the "follow" button:

sidebarTop : String -> String -> Html Msg
sidebarTop user location =
    div [ class "sidebar-top" ]
        [ div [ class "photo-info" ]
              [ div [ class "user" ] [ text user ]
              , div [ class "location" ] [ text location ]
              ]
        , div [ class "photo-actions" ]
              [ followButton , likeButton ]
        ]

A. The Follow Button

The "follow" button is going to render some HTML but won't do anything when clicked:

    followButton : Html Msg
    followButton =
        div [ class "follow-button" ]
            [ text "Follow" ]

B. The Like Button

The "like" button is different in that it renders HTML and it has an onClick event (imported from Html.Events) that will send the LikePhoto message. The LikePhoto message can be handled by the update method of the PhotoView component, which we'll be defining soon enough.

Let's see how the "like" button's code looks like:

    likeButton : Html Msg
    likeButton =
        div [ class "like-button"
            , onClick LikePhoto
            ]
            [ text "Like This Photo" ]

The number of likes and comments on a photo

The next row of the sidebar will display the number of likes and the number of comments for the photo.

sidebarCount : Int -> Int -> Html Msg
sidebarCount likesCount commentsCount =
    let
        likesCountStr = toString likesCount
        commentsCountStr = toString commentsCount
    in
        div [] [ text likesCountStr
            , text " likes, "
            , text commentsCountStr
            , text " comments"
            ]

We use the let syntax to define some local variables that convert the numbers to strings so that they can be included in the text DOM nodes.

As you can see from the type signature, we are accepting two number arguments. The text HTML function accepts a string argument. You'll find that much of the time in Elm, you're simply mapping one type into another and it gives a sense of clarity when programming. By making the type definitions explicit and by having a good type-checking system, type inference, and a compiler with easy-to-understand error messages; Elm makes programming much easier.

Photo comments

The third and final row of the sidebar displays the comments about the photo. Check out the code, the explanation for it is below:

sidebarComments : Int -> List Comment -> Html Msg
sidebarComments commentsCount comments =
    let
        pageSize = 10
        pages = commentsCount // pageSize
        commentsList = comments
                     |> map displayComment
                     |> div [ class "comments-list" ]
        loadMoreItems = div [ class "load-more-comments" ]
                            [ text "Load More Comments" ]
        container = if pages == 1 then
                        [ commentsList ]
                    else
                        [ commentsList , loadMoreItems ]
        in
        div [ class "comments-container" ] container

We are again using the let syntax to define some local variables. The loadMoreItems div is defined just for convenience, you'll notice that it will do nothing because there are no Html.Events defined for it.

The page size determines whether or not to show the "load more comments" button depending on how many more pages can be loaded. In the container variable we also use the if/then/else syntax to check how many pages there are.

The commentsList maps the displayComment function over the list of comments and puts them into an HTML div container.

The definition for displayComment looks like this:

displayComment : Comment -> Html Msg
displayComment {user,message} =
    div [ class "comment" ]
        [ div [ class "user" ] [ text user ]
        , div [ class "message" ] [ text message ]
        ]

One thing you'll note here is that we're using destructuring in the parameters to avoid the long-form way of accessing a field of a record, we can use user instead of comment.user. This little component is quite simple and the heavy lifting is done through CSS for displaying the comments in an eye-pleasing manner.

The View Function: Combining the Photo and Sidebar

We have the photo view and the sidebar view functions defined. Let's combine them into the PhotoView component's view function:

view : Model -> Html Msg
view model =
    let
        photoHtml = photo model.photo.url
        sidebarHtml = sidebar model.photo
        html = case model.showCloseButton of
               True -> [ photoHtml , sidebarHtml , closeButton ]
               False -> [ photoHtml , sidebarHtml ]
    in        
        div [ class "photo-view" ] html

We define three local variables: photoHtml, sideBarHtml, and html. The first two store the result of rendering the photo and sidebar. The last variable will store a list of HTML elements depending on whether the close modal button should be shown. As I said earlier, Elm is nice in that you're defining small and manageable functions that are easy to reason about. If you've seen really elegant code and code that could be called beautiful, then you'll recognize that Elm's syntax gets us to a cleaner and more elegant result than the typical JavaScript code you will encounter.

We also need the closeButton view function defined so that it can be rendered:

closeButton : Html Msg
closeButton =
    div [ class "close-button" , onClick CloseModal ]
        [ text "X" ]

The close button will appear when the data model's showCloseButton field is set to True. When you click on the close button, the CloseModal message is sent to the update function.

Testing the PhotoView component in Elm-reactor

For easy testing in the Elm-reactor, you can define a main function in your PhotoView module with a beginnerProgram. By supplying a model function that returns example/fake data and an update function that handles the messages, you can test your component in isolation.

Here's the code to define the data model for testing, we're going to display a photo and have a comment box that is empty at first and we need to choose whether or not to show a close button:

type alias Model =
    { photo : Photo
    , newComment : String
    , showCloseButton : Bool
    }

And the model with fake data:

model =
    Model examplePhoto "" True

Depending on what you're testing, you can adjust the fake data as you see fit. For example, you can change the "True" to "False" so that the close button does not render.

The update function that updates the model looks like this:

update : Msg -> Model -> Model
update msg model =
    case msg of
        LikePhoto ->
            model
        SubmitComment newComment ->
            { model | newComment = "" }
        CloseModal ->
            model

When the LikePhoto and CloseModal messages are received, the fake data in the model will not be updated. Again, this is just for testing in the Elm-reactor. However, the SubmitComment message will update the model by clearing the text of newComment. Not the coolest functionality, but at least all messages are handled and we can make sure things are being rendered correctly. You can change the update function to update the model in different ways so that you can do a test of the component that is closer to a real-world test.

Now, here is the the main function that puts all these testing functions together:

main =
    beginnerProgram
        { model = model
        , view = view
        , update = update
        }

Now you can run the Elm-reactor in your directory, click the PhotoView.elm file and see the component rendered with fake data.

Main.elm: A Grid of Photos

For the main file that shows the grid of photos, we're going to define the main entry point in the main function first:

main =
    beginnerProgram
        { model = model
        , view = view
        , update = update
        }

Then we're going to define the model, view, and update functions; and break those down.

The Model

In our model, we need a list of photos, photos, since we're displaying a grid of photos. The photo grid is for a particular hashtag, which we'll call name, and we want to show the number of photos in total that this hashtag has, photoCount. We also need to keep track of which photo is being shown in the photo view modal, photoOpened, this is a Maybe type because sometimes we aren't opening a photo to view at all. We also want a space for users to enter a comment on a photo, newComment.

Based on that, our model will look like this:

type alias Model =
    { name : String
    , photoCount : Int
    , photos : List Photo
    , photoOpened : Maybe Photo
    , newComment : String
    }

And here is an example model which we can use for testing:

model =
    { name = "#elm"
    , photoCount = 123456789
    , photos = repeat 8 PhotoView.examplePhoto
    , photoOpened = Nothing
    , newComment = ""
    }

We're going to use "#elm" as the hash-tag, with a made up number of total photos, and we're going to use List.repeat to create eight copies of the example photo that we used when testing the PhotoView component. There will initially be no photo opened. To test that the modal shows, up you can change photoOpened to be equal to PhotoView.examplePhoto and the photo view modal should be open after the web page has loaded. Finally, the newComment is a blank string so that the user can enter whatever comment they want on a photo.

Starting With A Grid

Instead of starting with the data model, we're going to start with the view for the photo grid:

photoGrid : List Photo -> Html Msg
photoGrid photos =
    photos
    |> map photoItem
    |> div [ class "photo-grid" ]

In the first line we declare the type signature of the view, this function accepts a list of photos and returns an HTML node. In the second line, we begin defining the function and name its parameters.

The Pipe Function, also known as Forward Function Application

Now here's where it gets interesting. We're using the |> pipe function to pipe the photos variable into the map function and then into the div HTML function. What's interesting is that the variable we're piping in is always the last variable supplied to the function.

Here's how that bit of code would look without using the pipe function:

div [ class "photo-grid" ] (map photoItem photos)

As you can see, the variable we really care about, photos, is all the way at the end and it's a bit easier to visualize the flow of the data when we're using the pipe function. Click here to read more about the pipe function (also known as forward function application).

Rendering a Single Photo in the Grid

Let's get back to rendering the photo grid. For each photo in the photo grid, we're using the photoItem function to render it.

Each photo will be in a DIV with the CSS class "photo" and will contain an image whose source is the URL of the photo. Each photo will be capped at a width and height of 300 pixels:

photoItem : Photo -> Html Msg
photoItem photo =
    div [ class "photo" ]
        [ img [ src photo.url
            , width 300
            , height 300
            , onClick (OpenPhoto photo)
            ]
            []
        ]

We add the onClick to the image so that whenever a user clicks on the photo, the OpenPhoto message will be sent with the photo parameter.

That takes care of the photo grid.

Profile Information for a Hashtag

Let's display some information about the hashtag with the profileInfo function. It accepts a string and an integer, the name of the hashtag and the total number of photos:

profileInfo : String -> Int -> Html Msg
profileInfo name photoCount =
    div [ class "profile-info" ]
        [ div [ class "name" ]
            [ text name ]
        , div [ class "count" ]
            [ span [ class "value" ]
                [ photoCount |> toString |> text ]
            , text " posts"
            ]
        ]

There are a few DIV elements, the only tricky thing to note here is that the photoCount integer has to be converted to a string before being displayed as text node in the DOM. The toString function will convert any type into a string. The text function is imported from the Html package and will create a DOM Text Node. You can create custom DOM nodes using the Html.node function.

Changing the Dom to Open the PhotoView Modal

Now we have the two parts we need to display the view; the profileInfo function and the photoGrid function (which relies on the photoItem function).

Let's combine them into the view function. The view will depend on whether or not we're showing the photo view modal:

view : Model -> Html Msg
view model =
    let
        body = [ profileInfo model.name model.photoCount
            , photoGrid model.photos
            ]
    in
        case model.photoOpened of
            Nothing ->
                div [] body
            Just photoOpened ->
                photoOpened
                    |> photoView model.newComment
                    |> repeat 1
                    |> append body
                    |> div []

When the photo view modal is open, it is Nothing, we will display the results of running the profileInfo, photoGrid and the photoView functions.

When the photo view modal is closed, it is Just some value, we only display the profileInfo and the photoGrid. Since the results for these will be the same, we store the results in a variable named body.

Because Elm has a virtual (also known as a "shadow") DOM implementation, it knows that the profileInfo and photoGrid do not need to be re-rendered because their values haven't changed. When a photo is opened or closed, only the last DOM node, the one containing the photo view modal will be modified. You can check this for yourself by viewing it in a browser and watching the paint events in the developer tools of Chrome or Firefox. You will be able to see the way that Elm updates the nodes in the DOM tree. It's fairly quick and in rendering performance benchmarks it has been shown to out-perform React and AngularJS (including AngularJS 2).

Using PhotoView as a Nested Component

Now we get to the fun part, re-using the PhotoView.elm code that we wrote earlier!

Here's how it looks:

photoView : String -> Photo -> Html Msg
photoView newComment photoOpened =
    let
        model = { photo = photoOpened
                , newComment = newComment
                , showCloseButton = True
                }
    in
        model
            |> PhotoView.view
            |> Html.App.map (\_ -> ClosePhoto)

The whole function consists of extracting fields from the photo grid's model and rendering those properties through the PhotoView.view function.

We define the model of the photo view to contain the photo that has been opened, the new comment that the user is entering (starting with a blank string), and whether or not the close button should be shown (this will come up in the 2nd part of the tutorial where we use the PhotoView as a fullscreen component, which has no need for a close button).

The important part is the Html.App.map function which will map whatever messages that the PhotoView receives and handles into a message that can be received and handled by the photo grid component. In our case, when the PhotoView receives any message, it will map it into the ClosePhoto message. From this starting point, we can map messages received from clicking the like button or the follow button into a message that can be handled by our Main.elm's update function.

Messages and Events

When an event occurs in Elm, a message can be sent that will be picked up by the update function. As we've seen, when a user clicks on a photo, the Html.Events.onClick function will send the message OpenPhoto along with the photo that should be opened. When the close button in the photo view modal is clicked, it will send the ClosePhoto message.

We need to define these two message types like this:

type Msg
    = OpenPhoto Photo
    | ClosePhoto

And then we must define the update function that will receive these messages and update our modal accordingly:

update : Msg -> Model -> Model
update msg model =
    case msg of
        OpenPhoto photo ->
            { model | photoOpened = Just photo }
        ClosePhoto ->
            { model | photoOpened = Nothing }

We use the case syntax to pattern-match against the msg parameter and check if the OpenPhoto or ClosePhoto message has been received. If it's the OpenPhoto, we will update the photoOpened field of the model to display the given photo object. This will alter the DOM in the view function (which we defined earlier). When the message received is ClosePhoto, we update the photoOpened field to Nothing which means the modal will be closed based on the DOM being updated in the view function.

Running it in Elm Reactor

That's it! We've put together the photo grid in Main.elm and imported the photo view modal from PhotoView.elm. We're ready to test this with Elm-reactor.

Run Elm-reactor with elm reactor and then open http://localhost:8000/Main.elm/ and you should see the photo grid appear.

The problem with testing Elm-reactor is that it doesn't match our real-world experience of loading the InstaElm website and interacting with it.

Styling and Compiling

To get around this, we have to compile the Main.elm file into a JavaScript file and then create a bootstrap index.html file which will load the photo grid module into the page. You'll also notice that Elm-reactor didn't display any CSS styles for our components. By compiling the code and loading the bootstrap file, we will also be able to load CSS stylesheets and see how the components look when fully styled.

Styling it

There's not much to explain here, it's just plain CSS which you can view in the code repository:

Compiling Main.elm into instaelm.js

To compile our file, we just run the elm make command:

elm make Main.elm --output instaelm.js

When compiled, the PhotoView.elm will also be compiled because the module is imported into Main.elm.

In the next tutorial, we're going to be bundling all of our components into one file as separate modules so that we use them in different ways.

Loading instaelm.js with a bootstrap index.html file

Now let's load up the instaelm.js along with our stylesheets. I also wanted to make the text look better so let's load up a Google Fonts stylesheet too.

Here's how our index.html bootstrap file looks:

<html>
  <head>
    <title>InstaElm</title>
    <link href="https://fonts.googleapis.com/css?family=Prociono" rel="stylesheet">
    <link rel="stylesheet" href="style.css" />
    <link rel="stylesheet" media="screen and (max-width: 1024px)" href="style-laptop.css" />
    <link rel="stylesheet" media="screen and (min-width: 1025px)" href="style-desktop.css" />
  </head>
  <body>
    <div id="main"></div>
    <script src="instaelm.js"></script>
    <script>
      var node = document.getElementById("main");
      Elm.Main.embed(node);
    </script>
  </body>
</html>

As you can see, we create an empty DIV with the ID set to "main" which will be where we embed the model, view, and update bundle from the Main.elm module.

Awesome, Right? Now Check Out the Next Part!

That was awesome, right? We have two components that handle action messages in different ways and one component that renders differently based on the data model it is supplied with. Most importantly, you have the beginnings of the frontend for an Instagram clone!

In the next part of this series, we cover advanced topics like using HTTP and AJAX calls to get data from an API.

You can check out the code for part 1 of the tutorial on gitlab.com

You can also click here to see the CodeMentor office hours webinar that this tutorial is based on.

Discover and read more posts from Rudolf Olah
get started