Task 3
As we saw at the end of the last task, the server actually returned some comments as a response, when we created a new comment. What we didn't see is that the server also has an endpoint to fetch all the comments on an article.
In this task we will make a request to the endpoint when the page loads, and display the comments on an article to the user. We will also update the list of comments when the user posts a new comment.
Task 3.1: Making a GET request
We start by preparing our model for the new state:
Similarly to the model for the entire page, we will create a custom type representing the state of the comments we are fetching. We could choose to do this in other ways, but we want to achieve the following things with our model:
Represent that fetching the comments either is in progress, has failed or has succeeded.
Allow for the page to display the article, regardless of whether fetching the comments failed.
Other priorities will result in another representation.
Create a new custom type in the page we have been working on (
src/Page/Article
), with the following definition:You will need to import the
Comment
type, using the following line:Next, add a field
comments
toSuccessModel
, with the typeCommentsState
, and follow the compilation errors until your code compiles.In this task we will save the view to the end, so we continue by making the request to the server. To do this, we start by making another message, for when fetching the comments is done. The message can be identical to
SavingCommentFinished
, except that you call itFetchingCommentsFinished
. AddFetchingCommentsFinished
toMsg
, and make the required changes to theupdate
function. You can start by returning( model, Cmd.none )
fromupdate
.Now that your code compiles with the new message, we can handle the message a bit more correctly in
update
. Instead of just returning directly( model, Cmd.none )
, add a case statement onmodel
to check that we are in theSuccess
state of the page.Then add another case statement inside the
Success
case to check whether theResult
we got in the message wasOk
orErr
. Set thecomments
field inSuccessModel
accordingly (you can just hard code an empty list in theOk
case for now).Next, we will create a function in the
Api
module, similar to thecreateCommentOnArticle
we added in Task 2.4. Name the functiongetComments
, with the following type signature:And use the
Http.get
function to make the request, and the URL to the comment endpoint is/api/article/{articleId}/comments
, where{articleId}
is the article ID of the article you just fetched.Http.get
is similarHttp.post
, except that it doesn't have abody
. You can read it's documentation here, and you can useHttp.expectWhatever
, like we did increateCommentOnArticle
.We will now use the function we just created, and we are going to make the request for comments when the
update
function receives the messageFetchedArticle
. ReplaceCmd.none
with the function call togetComments
in the case forFetchedArticle
.
In the browser, you should now see that the app makes a request to the comments endpoint. You can see it in both the Elm debugger, and in the Network tab in the browser.
Note: This isn't the most efficient place to make the request, since it will make the page wait for the article response before fetching the comments, instead of doing the two request simultaneously. In an optional task later, you can try to change the app to handle making the requests at the same time. But for now, we will go ahead with this strategy.
Task 3.2: Decoding the comment response
As you can see in the browser, the app now make a request to the comments endpoint, and the response will look something like this (if you are on the "Modelling in Elm" page):
We see that the endpoint returns a list of comments, and that each comment has a three fields: id
, username
, and text
, all of with are strings.
To get access to these comments, we are going to have to decode them. Simply put, decoding is Elm's way of guaranteeing that the JSON data we get from a server is actually in the shape that we expect.
To decode the list of comments, we are first going to have to make a decoder for the Comment
type. Comment
is already defined in the Comment
module in the file src/Comment.elm
, where you can take a look at it.
We can see that a Comment
is a one-constructor union type, with three fields: id
, username
, and text
, the first of which is of type CommentId
, while the remaining two are of type String
.
This lines up quite well with what we are seeing in the response from the server, except that the id
field is a CommentId
. But actually, we don't have to decode every field we receive in a JSON object, so we can start by removing the id
field, to make our job a bit easier.
Remove the
id
field from theCommentInfo
record.To make a decoder for
Comment
, we are going to need a couple of packages, so import add the following two lines to the imports:The decoder we are going to make, is going to have the type signature
Decoder Comment
. A decoder is sort of like a recipe for how the JSON should look. It is not function. But before we make ourComment
decoder, we are going to make a decoder forCommentInfo
.Paste in the following code at the bottom of the file:
Note: This isn't very easy to understand the first time you see it, and you really don't need to understand what's going on here to continue with the tasks. So read the brief explanation below, and move on to the next task.
The code above uses the fact that type aliases for records automatically get a constructor of the same name as the type alias (
CommentInfo
in this case), where the arguments to the constructor are in the same order as the fields in the type alias. So the constructor calledCommentInfo
is just a function with the type signature:The
required
lines below then pick out the values from the fields in the JSON object called "text" and "username", respectively, and decode the values in the those fields as strings (that's theJson.Decode.string
part). The two resulting strings are then passed to theCommentInfo
constructor in the order they are in, and the result is aDecoder CommentInfo
.This is a simplefied explanation, so if you're thinking "that's not entirely correct", then you are right. But it is sufficient for now.
We now have a
Decoder CommentInfo
, but what we ultimately need is aDecoder Comment
. To get that we are going to useJson.Decode.map
.map
has the following type signature:In our case, we want to end up with a
Decoder Comment
, sovalue
in the type signature is ourComment
.a
in this type signature is going to be ourCommentInfo
, since that is the decoder that we already have. If we write out the type signature again using our types, we get the following type signature:The second argument to
map
is going to becommentInfoDecoder
, so what we need is the first argument, a function that takes aCommentInfo
as an argument and returns aComment
. And actually, the one constructor in theComment
type is the function. It takes one argument, which is aCommentInfo
, and returns aComment
.Given all that, we can add the following decoder for
Comment
:To use the decoder outside the
Comment
module, we are going to have to expose it. Adddecoder
to the list exposed by theComment
module.Next, we are going to actually use the decoder to get the comments that the comments endpoint returns.
In the
Api
module, in thegetComments
function, replace the call toHttp.expectWhatever
with the following line (your code won't compile anymore, but we will fix that in the next steps):This line is saying that we no longer don't care about what the server returns, we now expect the endpoint to return JSON. We also say that the JSON should be decoded with decoder defined as
Json.Decode.list Comment.decoder
.Comment.decoder
is the decoder we just made, andJson.Decode.list
is a way to make a decoder for a list of a certain type. The decoder we pass toexpectJson
is therefore of typeDecoder (List Comment)
which is what we actually expect the endpoint to return.The compilation error we now get, says that the second argument to
expectJson
is not what the compiler expects. However, the second argument (the decoder) is exactly what we want to pass toexpectJson
. The thing we actually want to change is the type of themsg
argument. Change the type of the first argument, in the type signature ofgetComments
to the following (this will result in another compilation error):We have now changed what type of message the
getComments
function accepts, since a successful call to the comments endpoint now results in a list of comments.The compiler still complains, but this time the compilation error is in
src/Page/Article
, since we are now callinggetComments
with the wrong first argument. Fix this error by changingFetchingCommentsFinished
inMsg
to be of the type thatgetComments
expects.After fixing this, your code should compile. And if you check the Elm debugger in the browser, you will see that you actually receive a message with a list of comments!
You can now change the hard coding we did earlier in the
FetchingCommentsFinished
case inupdate
, so that we put the actual list of comments in the model, instead of always using the empty list.
If you check the Elm debugger in the browser now, you should see an actual list of comments in the model after the comments are finished loading.
Task 3.3: A view for the comments
Task 3.4: Updating the list of comments after a post
You can use the response from the POST request, to update the model.
Task 3.5: Nested comments
The comments you get in the response when posting a comment are nested: each comment has a field comments
, which is a new list of comments. Use the endpoint /api/article/{articleId}/nestedComments
, in Api.getComments
to get nested comments. Decode the comments with subcomments.
Last updated
Was this helpful?