Understand the three basic parts of making HTTP requests in Elm: 1) Building the request, 2) Writing a JSON decoder, 3) Sending the request using the Elm runtime, and reacting to the results with messages.
HTTP requests are just pieces of data in Elm. When we want to send them, we hand them off to Elm's runtime, and it notifies us when the request is done. An important part of working with HTTP requests that fetch JSON is JSON decoding, the process by which we can turn untyped JSON data into Elm types that are safe to work with.
I'm going to use my IDE to install a new Elm package. Here I'm going to search for the HTTP package which comes from Elm Lang and install that. That's going to install the package for me like it would on the command line but I'm within Atom here. Atom is using the Elmjutsu plug-in to install that for me.
Now that I have the HTTP package, I can use autocomplete to import it in just a minute. The first thing I want to do is build a request to fetch some JSON from this URL right here. Let's go ahead and type in a type signature because the request we're going to build is actually just a piece of data. We'll use the Elm runtime later to send that data out to an actual HTTP request and handle the result.
Let's give it a name, products request. Then we'll put a colon to indicate that I'm about to enter the type signature. Products request is going to be of type request. I'm going to start typing request. There is nothing called request imported yet. I can see that the top result here is from the HTTP package.
I'm going to just press enter and let it get imported automatically for me by my editor plug-in called Elmjutsu.
This is going to be a request, and the request is of type list of product. I've got the product type already written out down here below. You can see that the product has a description that's a string, a photo that's a string, and an ID that's a string.
There's already a UI in place that uses these things. This is simulating maybe the fact that I could be working with a designer who's already mocked out the UI, given me some types, and now I'm implementing the backend part of it.
I'm going to build a request that's going to bring back a list of products. I can do that by entering a new line here, and then entering the same name as above, which the editor completed for me.
I'll start building the request. It's going to be a HTTP.get request. I'm going to start typing HTTP get, and I see here that the autocomplete suggests the get function for me. I'm going to hit enter, and it'll give me some placeholder parameters.
The first is a string which is going to be the URL we'll get. I'll wrap some quotes around it. I will copy this full URL and paste it inside of the quotes.
Next, I've got to give it a JSON decoder. A JSON decoder is a way of telling Elm how to take JSON, which is untyped data, and convert it into typed data that Elm understands, like this product. We'll build the product decoder in a second, but I can start this out by putting some parens here and then starting to type JSON list because I know I'm going to want a list of things.
The second option here in my autocomplete list is JSON.decode.list, and that looks right I'm going to hit enter. It'll import it for me. As you can see up here at the top and down here below, I've got JSON decode list. This is a function that takes another decoder because we're going to get a list of something.
We'll write the name of the decoder we're about to make, which is product decoder. Then I save the file, and you see everything jumps a little bit. That's because my IDE also has a plug-in that automatically formats my code for me called Elm format.
You can see I've got a little red triangle over here on the side. It says it cannot find the variable product decoder, and that's because we haven't made it yet. Product decoder will be the JSON decoder that teaches Elm how to take JSON and turn it into a product.
When we build decoders, we have to think about the shape of the JSON we're getting in and how that relates to the shape of the type that we want. Let's go look at the JSON that we get back from this URL. I'll copy it. I'll go over to Firefox, and I'll paste it in here.
We can see that I've got some products JSON here. I've got a product that has an ID which is a string, a name which is a string, and an image which is a string. Let's go look at the product here. The product has a description. That could correlate to the name field. It's got a photo which could correlate to the image field and the ID which matches up exactly.
We need a way to get this JSON into this type. That's how our decoder's going to help. Let's go ahead and make a new value here, and call it product decoder. Enter the colon to say we're going to give it a type. This type is going to be a JSON decoder.
There once again I'm going to use the autocomplete menu to import the right thing. JSON.decode.decoder is what we want. We're going to get a decoder of product. Here, we're going to have to do all the code necessary to build that decoder. Product decoder, this is how I specify what the value actually is going to be the correlates with this type signature.
The question is how do we write a JSON decoder? There's a library that's very useful for this, so I'm going to once again use my IDE to install a package. That's called pipeline. This is the only one that's called pipeline, No Red Ink slash Elm decode pipeline. This makes decoding pretty easy. I'm going to import it, and it auto-chooses the newest version by default. Let it download.
On the next line, I'll type the word pipe decode, just short for JSON pipeline.decode. There's only one function called that, so I'm going to use the autocomplete once again. It'll import the libraries necessary for me above and insert the decode function here.
The decode function is going to take the constructor for the type that we want to build from the JSON, in this case, the product. I'm going to type in here product.
An interesting thing about Elm is that when we create a type alias, we're also creating a constructor for this type. This implicitly creates a function called Product with a capital P that takes three arguments, a type string, string, string, that when we call product with three strings builds a product.
This is actually a type and a constructor. That's why we can pass that right here. We could JSON pipeline decode product.
Next, let's insert some comments to help us remember what we're about to do. We're going to go ahead and try to find a field on the JSON that'll match up with the description field in our product type. Then we'll try to find something for photo and ID.
Let's make three lines here. The first, we can say description is what we're trying to build. That correlates with the name field on the JSON. The next one is photo, and that correlates with the image field on the JSON. The third one is ID and it correlates with the ID field. That's the same.
Beneath these, we're going to use a pipeline operator. That's why this library is called JSON pipeline. This has got some pretty fancy types to make the syntax work. If you look at the types and get confused, don't worry. It's actually very simple to use.
We insert the pipeline which is a bar character followed by the right carat. It looks fancy in my editor. It looks like a triangle because I have fonts with ligatures enabled that turn two characters into one visual character. But really it's just a bar and a right carat. Then I'm going to type pipeline required. I'm going to start to type that and hit enter. Let my autocomplete work for me there.
I'm telling the pipeline decoder that I need an attribute to exist here, and I need it to be called name. I'm going to need this JSON that's coming in to have a name attribute. It's going to look up the name attribute, and then it's going to try to decode it with another JSON decoder.
We're going to look for a string there. I can just type JSON string. There I go. I get the JSON string decoder. Let's copy this line and let's paste it down below. Now we've ourselves most of the way there.
We're going to try to build the photo field which is being built positionally. Remember how I said before that product is a function that takes three arguments, one, two, three, and they're string, string, string? Those correlate with these lines right here, one, two, three.
All we're doing is saying pipeline required and then the field on the JSON we're looking for, which in this case is going to be image for the photo. This is going to try to get the image field, decode it to a string, and pass that into the photo parameter for the product constructor function.
We're going to copy this one more time. This time we're going to have it be ID which correlates with the ID field. If these were other types, I could use other decoders like int or bool for example, but since they're all strings, we'll just stick with that for now.
I'm going to save the file. It reformats for me into a pretty format, but I don't get any compiler errors. It looks like I've successfully built my product decoder and I've also successfully built my request now.
The next step is to actually send this request. It doesn't do much good sitting around on its own as data. What we're going to do is when we click this load hipster stuff button, we're going to go ahead and send that request. Now assuming the designer has built this with some messages already in place, let's look up that text, load hipster stuff.
Here it is in the UI. There's some text that has an on-click event. It fires off the load products message. Looks like that's the only message in the app currently.
I can go into my update function and find where load products happen. This isn't actually doing anything. We can see that if we click in the UI, it's doing nothing. We can see right here that it's just returning the model unmodified without any commands being fired at all.
The way we're going to send this HTTP request that we built is by turning that request into a command, handing it off to the Elm runtime, letting Elm actually do the HTTP request, and then pass messages back into us once again once that's done.
Let's get started by inserting a whitespace here. Let's go ahead and type HTTP send, hit enter, and you'll see that it's going to expect a function that takes a result HTTP error A and returns a message. Then it wants a request. I know what the request is. Let me just pass that in right now. It's called product request right there. That part's filled in.
I need this function that's going to take a result. A result is an interesting type that can either be successful or a failure. That's why it's got two things here. It's either going to be an HTTP error if it fails or it's going to be the type that the request has inside of it when it succeeds.
Let's keep these same parentheses here, but we'll insert into this res and then a right arrow. This creates an anonymous function where this a parameter, right arrow, and then there's the body. Again, the arrow looks fancy because I have ligatures set up, but it's just a dash and then a carat.
I will create a new line here. Here I'm going to do a case expression, case res of and a new line. Here we'll put the two different types that a response can be. It can either be OK and contain the product that we want to use or it can be an error and contain the HTTP error that we want to know about.
If we get products back, we're going to want to send a message to our runtime that says, "Hey, we've got some products." We know that there isn't one yet, but let's just make one up. We can fill it in later. We'll type one got products. We'll pass products into it. Then here down below, what do we do if there's a failed request? We want the user to see that too.
Let's make up a message maybe called show error and pass in a string, to string HTTP error, and save the file. Now you see we've got two little red triangles. Those are saying can't find variable got products and can't find variable show error.
We need to add new messages for these. We can do that by going all the way back up to message and say got product list of product and show error string. If we save here, we're going to get some errors down in the update function saying, "Oops, we haven't handled got products or show error yet." Let's go ahead and do that.
If we are getting the message got products product, then we can update the model. This is the model update syntax, model where products is a field on the model equal products. Then we put an exclamation point and a list afterward.
This is special syntax that Elm supports. It's just to make it easy inside the update function to say, "Here's the model on the left side of that exclamation point. On the right side is the list of commands to send."
Now we save the file. OK, looks like we're still missing show error. Let's do that too.
Show error error and the record update syntax, model where error equals error, then exclamation point and the list. If we save this, I've got an error field on my model already and a UI already set up to show that error, so I'm fine.
If I click this button, I should see something happening, but I'm still not now. Let's go take a look at our view. It looks like this is hard-coded, and they're not actually using the field on our model yet for products. But we do indeed have a product view function.
Let's go ahead and change this view to say if list.length of model.product is greater than zero, and let's wrap that in parens. We'll enter it so no HTML there at the moment. But right now I'll type else and we'll keep that old stuff there that says, "You're just one click away from some fun hipster stuff."
Here let's render the product. We can do that by typing list.map and a function that's going to render the products which is called product view, then the products themselves, model.products. Save and then we get an error.
Let's see what it is. It's saying the then branch has a type of list HTML message but the else is HTML message. That's because right here we are producing a list of products, and this is just HTML.
We can pretty easily solve that by wrapping this inside of its own div and putting all this code that builds up the code in parens. Now we'll save again and all those errors go away. If we click the button, we get some fun hipster stuff.
(ノ°ο°)ノ wootWOOT !!!