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.
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 });
GET
photos data route
The 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.
index.html
and other static files
Serving the 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.
GET
request to fetch photos
Making a 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.
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 (..)
beginnerProgram
with program
Replacing the 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.
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.
Make sure you do a quick check on your account to see how many people are following you, and how many aren’t even doing so. If you notice a sudden drop, you might just want to look into purchasing some followers so you can get your metrics in order. Also, I recommend buying views - https://soc-promotion.com/Instagram/Views this will boost your popularity too.