10
Published Dec 09, 2016Last updated Jan 18, 2017

Build an Instagram Clone in Elm: InstaElm Part 2

Build an Instagram Clone in Elm: InstaElm Part 2

Introduction

This is the second part of the InstaElm tutorial where we create an Instagram clone in the Elm programming language to demonstrate how a real world project is implemented in Elm. We're focusing on inter-operation with other JavaScript code and being able to interact with an API server written in Node.js and using the Hapi.js library.

In the previous tutorial we covered the basics of the Elm Architecture and how to create nested components.

What You Will Learn

You will learn:

  • How to set up a Hapi.js server that serves fake API data
  • How to get data from JavaScript into an Elm module with ports
  • How to send a message from an Elm module to JavaScript to fetch more API data

Setup

In the previous part of the tutorial, we detailed how to set up Elm, but for this part of the tutorial we only have to install Hapi.js and the Inert plugin which is used for serving static files and directories.

To do that, we run this command:

    npm install --save hapi inert

Creating an API server with Hapi: server.js

Before we do anything in Elm, let's create our API server. The API server will serve data about photos that will be displayed in the photo grid.

Importing Hapi.js

In the file server.js we start by importing the hapi module:

    var Hapi = require('hapi');

Creating the server

Then we create a server that loads on port 3000:

    var server = new Hapi.Server();
    server.connection({ port: 3000 });

The GET photos data route

The first route will be serving data about the photos to a GET request that accesses the /photos/ URL. The data will have to match the record type that we defined in the first part of the tutorial (in the PhotoView.elm file). We're going to return the data for two photos, the first photo will have no comments while the second photo will have one photo.

    server.route({
      method: 'GET',
      path: '/photos/',
      handler: function(request, reply) {
        var photos = {
          'photos': [
            {
              user: 'User',
              location: 'Some City',
              likesCount: 123,
              commentsCount: 0,
              comments: [],
              url: 'webinar-ad.png'
            },
            {
              user: 'Another User',
              location: 'Another City',
              likesCount: 987,
              commentsCount: 11,
              comments: [{ user: 'User', message: 'Awesome photo!' }],
              url: 'webinar-ad.png'
            }
          ]
        };
        reply(photos);
      }
    });

Liking a photo with a POST request

The second route will be accepting a POST request to the URL /like/ that will increase the number of likes on a photo. It has one parameter: the URL of the photo.

    server.route({
      method: 'POST',
      path: '/like/',
      handler: function(request, reply) {
        reply({
          result: 'added like to photo with url: ' +
            request.payload.photo
        });
      }
    });

The parameters passed to the request are in the request.payload object. We're going to reply with a message that says everything ok.

Serving the index.html and other static files

To simplify things, we're going to use the Hapi server to also serve the static files in our project. Those static files are:

  • index.html: The bootstrap file that loads up our JavaScript, CSS and embeds the photo grid into the page.
  • instaelm.js: The compiled InstaElm project (currently compiled with only Main.elm, but later in this tutorial we will compile other modules into it).
  • style.css: Common CSS styles for all devices.
  • style-desktop.css: CSS style for desktop screens.
  • style-laptop.css: CSS style for laptop screens.
  • webinar-ad.png: An example photo.

Here's how we serve those files with Hapi:

    server.register(require('inert'), function(err) {
      if (err) {
        throw err;
      }
    
      server.route({
        method: 'GET',
        path: '/{param*}',
        handler: {
          directory: {
            path: '.'
          }
        }
      });
    });

We're simply serving all files from the project's directory using Hapi's directory handler. The route is created when the inert plugin is registered and loaded into Hapi. This is not recommended for a production setup but in our case, we can live with it to ensure everything is working properly.

Starting the server

We start the server like this:

    server.start(function(err) {
      if (err) {
        throw err;
      }
      console.log('Server running at: ' + server.info.uri);
    });

It will output the URL and port number of the API server when it starts running, otherwise, it throws an error.

Running the API server

We can run the server.js file by running:

    node server.js

Now we're ready to make API calls from our web app.

Visit http://localhost:3000/ and you will see that the index.html has loaded up along with the JavaScript and stylesheets.

Using ports for data transfer between Elm and JavaScript

Now let's set up the methods that will make API requests. After that, we can set up the Elm module for the photo grid with a port so that we can load data into it.

Loading jQuery

First, we need to load jQuery. We're going to add the following line right before the "instaelm.js" script tag:

    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>

It's jQuery loaded from the official jQuery CDN website.

Making requests with jQuery AJAX: index.html

We're going to replace the current JavaScript code in index.html with some new code that will make a request for photos from the API server and that will allow us to "like" a photo.

Making a POST request to like a photo

Here's the code for liking a photo:

    function likePhoto(photoUrl) {
      var payload = { photo: photoUrl };
      $.post('/like/', payload, function(data) {
        alert(data.result);
      });
    }

We'll have to implement this in Elm on the photo view component so that when we like a photo, it will trigger this API request.

We can test that this works by calling the likePhoto function with some sample data:

    likePhoto('test url', 'test user');

It will display an alert dialog box in the browser that shows the response from the API server.

Making a GET request to fetch photos

Now here's the code for fetching a list of photos from the API server to display in the photo grid:

    function getPhotos(onSuccessCallback) {
      $.getJSON('/photos/', {}, function(data) {
        onSuccessCallback(data.photos);
      });
    }

We pass in a callback function that will be called when the GET request was successful. We are passing the result to the callback function. This makes it easy for us to test this API request and to hook it into Elm.

Here's how we would test this API function:

    getPhotos(function(photos) {
      alert('There are ' + photos.length + ' photos to render');
    });

Loading data into Elm with ports: Main.elm

To load data into Elm, we're going to create a port. Ports can be used to access Elm data from JavaScript and can be used to send data into Elm.

As the documentation says,

Sending values out to JS is a command. Listening for values coming in from JS is a subscription.

Declaring a port module

In Main.elm we're going to change the module declaration to declare that this module has ports:

    port module Main exposing (..)

Replacing the beginnerProgram with program

Then we replace the main function with the following code:

    main =
        program
            { init = init
            , view = view
            , update = update
            , subscriptions = subscriptions
            }

We remove the main function definition because we are no longer going to be using the beginnerProgram function. We change the beginnerProgram to program when importing Html.App.

Defining an init function

We now have to define an init function. It will be returning the model that we already defined for use as the initial model to display.

    init : (Model, Cmd Msg)
    init =
        (model, Cmd.none)

The init function is a tuple of two items; the model, and a message. This lets us initialize a component in a way that triggers an event. Very useful when we're loading up data as soon as the component is loaded into the DOM.

In this case, we don't want to send any message and just want to use the example data in the model that we already have defined.

Defining the subscriptions

The subscriptions function also needs to be defined. It sets up the port photos with the message that it will send whenever it is updated:

    subscriptions : Model -> Sub Msg
    subscriptions model =
        photos UpdatePhotos

We're going to call our message UpdatePhotos and we'll have to add it to the Msg type alongside the OpenPhoto and ClosePhoto messages.

Updating the Msg type

Let's add the UpdatePhotos message to the Msg type:

    type Msg
        = OpenPhoto Photo
        | ClosePhoto
        | UpdatePhotos (List Photo)

Updating the update function

Now that we've added a new Msg type, we're going to have to update our update function to handle the UpdatePhotos message.

We also need to need to update the function because the type signature changed when we switched from Html.App.beginnerProgram to Html.App.program. The current type signature of update accepts a Model argument and then returns a Model object. We need to change the type signature to accept a Model argument and then return a tuple containing the updated model and the message to pass along (this is useful when we want to update the model and trigger other messages). We also need to change the return values to include Cmd.none:

    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model =
        case msg of
            OpenPhoto photo ->
                ({ model | photoOpened = Just photo }, Cmd.none)
            ClosePhoto ->
                ({ model | photoOpened = Nothing }, Cmd.none)
            UpdatePhotos newPhotos ->
                ({ model | photos = newPhotos }, Cmd.none)

When the update function receives the message UpdatePhotos, it will modify the field photos of the existing model to show the new set of photos.

Re-compile and call getPhotos in index.html

Now we can recompile the code: elm make Main.elm --output instaelm.js

And then, in index.html, we have to store the result of embedding Elm.Main and then initiate a call to getPhotos with a callback that will update the photo grid component in Elm:

    // index.html
    var node = document.getElementById("main");
    var main = Elm.Main.embed(node);
    
    setTimeout(function() {
      getPhotos(function(photos) {
        main.ports.photos.send(photos);
      });
    , 300);

To access the port, we store a reference to the embedded Main component. Then, after the API request is finished, we use the send function of the photos port to send a message from JavaScript into Elm with the data.

We're using setTimeout to load the photos after a few seconds.

Using Flags to initialize the data model

An alternative to this is using Html.App.programWithFlags which lets us pass in "flags" that are used to build up the initial data model. This would allow us to load the photos from the server and then, as soon as it is finished loading, we could insert the Main photo grid component into the DOM with the photo data.

Let's just go ahead and replace the previous code in index.html with the following:

    var node = document.getElementById("main");
    var main;
    getPhotos(function(photos) {
      main = Elm.Main.embed(node, {
        name:'hello',
        photos: photos
      });
    });

In the above code snippet, in the callback function passed to getPhotos, we will be inserting the photo grid (Elm.Main) in the DOM and supplying it with flags (the initial parameters of the component).

Since we're using programWithFlags we have to change our main function in Main.elm to use programWithFlags instead of program and we have to change the init function to accept the flags parameter:

    import Html.App exposing (programWithFlags)
    
    main =
        programWithFlags
            { init = init
            , view = view
            , update = update
            , subscriptions = subscriptions
            }
    
    type alias Flags =
        { photos : List Photo
        , name : String
        }
    
    init : Flags -> (Model, Cmd Msg)
    init flags =
        ({ model | photos = flags.photos , name = flags.name }, Cmd.none)

To make our code clearer, we also defined a type alias called Flags which makes it easy to see which flags can be used when initializing the component. In this case, we want the initial list of photos to load and the name of the profile that we're loading (this is to keep things simple and you can add more program flags if you like).

Now, let's re-compile the code. What we should see happen is that the page loads, the API request is made and as soon as it is finished, the photo grid component is inserted into the DOM.

Using Elm to send messages to other JavaScript code: Main.elm, index.html

Now that we can pass data into an Elm component, we also want to send data from Elm back to other JavaScript code. We want to be able to click the "Like" button after opening a photo in the PhotoView and have that trigger a jQuery AJAX request to our API server.

First we update LikePhoto message to accept an argument and update references to that message in the nested PhotoView component. Then we set up the message handling from the PhotoView into the Main component's update function. Then we create and use an additional port to send messages back to JavaScript and trigger an event. Finally, we respond to the event with some jQuery code to send the liked photo's data to the API server.

Updating the LikePhoto message to accept an argument

In the PhotoView component, we need to update the Msg types that we have defined, in particular we have to re-define the LikePhoto type to accept a Photo record type as an argument:

    type Msg
        = LikePhoto Photo
        | SubmitComment String
        | CloseModal

Then we update the function that renders the "like" button:

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

We must also update the sidebarTop and sidebar rendering functions.

The sidebarTop function needs to accept a Photo record rather than just the user's name and the photo's location:

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

The function call to sidebarTop will be updated to support that:

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

In hindsight, it might have been better to pass in the whole Photo record to the sidebarCount and sidebarComment functions rather than the specific fields from the record. As you learn Elm, you will find different ways to define functions that suit your purposes and refactoring is simple and safer than usual thanks to the Elm compiler's type checking.

Mapping PhotoView messages to be handled in the photo grid: Main.elm

The messages that the PhotoView sends are exclusive to the PhotoView module, they are of the type PhotoView.Msg. The same is true in the photo grid in Main.elm, the Msg type in that file is exclusive to that module.

The types are two different types and so we have to map the PhotoView.Msg types into the Main.Msg to be able to handle them with our update function.

We have to add the PhotoViewMsg as a new type that accepts a PhotoView.Msg message:

    -- Main.elm
    type Msg
        = OpenPhoto Photo
        | ClosePhoto
        | UpdatePhotos (List Photo)
        | PhotoViewMsg PhotoView.Msg

To render the PhotoView, we now have to update the photoView function so that the PhotoView messages are mapped to the PhotoViewMsg type:

    -- Main.elm
    photoView : String -> Photo -> Html Msg
    photoView newComment photoOpened =
        let
            model = { photo = photoOpened
                    , newComment = newComment
                    , showCloseButton = True
                    }
        in
            model
                |> PhotoView.view
                |> Html.App.map PhotoViewMsg

This is done using the Html.App.map function. Without this, Elm will not compile our file because the types will not match (PhotoView.Msg is not the same as Msg).

Sending the LikePhoto message from Elm through a port

In our update function we have to handle case of PhotoView messages like CloseModal and LikePhoto being received:

    -- Main.elm
    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model =
        case msg of
            OpenPhoto photo ->
                ({ model | photoOpened = Just photo }, Cmd.none)
            ClosePhoto ->
                ({ model | photoOpened = Nothing }, Cmd.none)
            UpdatePhotos newPhotos ->
                ({ model | photos = newPhotos }, Cmd.none)
            PhotoViewMsg msg ->
                case msg of
                    PhotoView.SubmitComment comment ->
                        (model, Cmd.none)
                    PhotoView.CloseModal ->
                        update ClosePhoto model
                    PhotoView.LikePhoto { url } ->
                        (model, likePhoto url)

All messages that will be sent and received through the photo grid component and the nested photo view component need to be handled by the update function, whether or not they make any changes to the model.

Let's take a closer look at what happens when the main photo grid component receives the LikePhoto message from the PhotoView component:

    update msg model =
        case msg of
            PhotoViewMsg msg ->
                case msg of
                    PhotoView.LikePhoto { url } ->
                        (model, likePhoto url)

The message received is of the type PhotoViewMsg and has one parameter, the msg which is of the type PhotoView.Msg. The case that matches when we send LikePhoto is the PhotoView.LikePhoto case which has one argument, the Photo record. We're using destructuring in the argument because we are only concerned about one field in the record, the url field.

The result is that the model does not change, however we will be sending the url to the likePhoto port.

Adding a new port to send messages through

Let's add the likePhoto port:

    -- Main.elm
    port likePhoto : String -> Cmd msg

This was straight-forward because we only have to declare the type of the port. Now we can receive the photo's url into JavaScript from Elm.

Responding to the message with jQuery

The Elm port, likePhoto, is set up and when clicking the "Like" button after opening the PhotoView modal, the message will be passed along through Elm until it's sent through the port.

Back to our index.html file. In JavaScript, we need to subscribe to the likePhoto port and whenever it receives a photo url string, we will trigger an AJAX request to the API server.

We're going to define a subscribe function that will be called as soon as the main photo grid component is loaded up, and will subscribe the likePhoto callback function that we defined earlier to the likePhoto port:

    // index.html
    function subscribe(mainApp) {
      mainApp.ports.likePhoto.subscribe(function(photoUrl) {
        likePhoto(photoUrl);
      });
    }

To call this function, we have to first call to getPhotos:

    var node = document.getElementById("main");
    var main;
    getPhotos(function(photos) {
      main = Elm.Main.embed(node, {
        name:'hello',
        photos: photos
      });
      subscribe(main);
    });

Compile it (elm make Main.elm --otput instaelm.js) and then run the API server (node server.js) and check out http://localhost:3000. You should now be receiving an initial batch of photos for display in the photo grid and be able to like photos through the photo view modal.

Conclusion

Here is a call graph of our Instagram code. You can see that there are a lot of view functions and that the way to nest components and handle messages/events is very clear. The interoperation between JavaScript and Elm is compact and easy to see from the small collection of functions.

instagram clone Click here to view the image in full

Overall, you will find that development on the front-end in Elm will be slightly faster than in other languages but what's more important is that whenever you have to debug some code, you will be much faster at finding the source of any problems.

For more information on JavaScript interoperation with Elm, be sure to read through An Introduction to Elm, written by the language's creator.

Thanks for reading this second part of the tutorial. I'm doing more work here and there on the InstaElm, Instagram-clone in Elm, and you check out the code here: https://gitlab.com/rudolfo/instaelm/tree/master The part two code is under the tag part-two. I definitely encourage everyone who is using React or Angular to give Elm a try.

How to Effectively Develop Vanilla Javascript Application
Localizing time in a Traditional Rails app with Moment.js
JavaScript Best Practices: Tips & Tricks to Level Up Your Code